neon-sprawl/docs/plans/NEO-70-implementation-plan.md

145 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# NEO-70 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-70 |
| **Title** | E3.M2: Craft HTTP + Bruno gather→refine→make spine |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-70/e3m2-craft-http-bruno-gatherrefinemake-spine |
| **Module** | [E3.M2 — RefinementAndRecipeExecution](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) · Epic 3 Slice 3 · backlog **E3M2-06** |
| **Branch** | `NEO-70-craft-http-bruno-gather-refine-make-spine` |
| **Precursor** | [NEO-69](https://linear.app/neon-sprawl/issue/NEO-69) — `CraftOperations.TryCraft` + `CraftResult` (**Done** on `main`) |
| **Pattern** | [NEO-55](https://linear.app/neon-sprawl/issue/NEO-55) — `POST /game/players/{id}/inventory` (known-player gate, schemaVersion, 200 + structured deny) |
| **Client counterpart** | [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74) — E3S5-03 craft UI + recipe list (blocked by this issue; server-only in this story) |
| **Blocks** | [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74) craft UI HTTP dependency |
## Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **POST response envelope** | Promote `CraftResult` 1:1 vs include inventory echo? | **CraftResult fields only**`schemaVersion` **1**, `success`, `reasonCode`, `inputsConsumed`, `outputsGranted`, `xpGrantSummary`; separate GET inventory / skill-progression for Bruno spine (E3_M2 module doc wire table). | **User:** CraftResult fields only. |
| **Manual QA doc** | Add `docs/manual-qa/NEO-70.md`? | **Yes** — E3M2-06 backlog calls for manual QA when craft HTTP lands (NEO-68 pattern). | **User:** **No** — server-only story, no Godot; Bruno + README sufficient (NEO-55 pattern). |
No other blocking decisions — request shape (`recipeId` + optional `quantity` default **1**), route (`POST /game/players/{id}/craft`), known-player gate, and HTTP status mapping follow [E3M2 kickoff defaults](E3M2-prototype-backlog.md#kickoff-decisions-decomposition-defaults) and NEO-55/NEO-38 precedent.
## Goal, scope, and out-of-scope
**Goal:** Versioned **`POST /game/players/{id}/craft`** wired to **`CraftOperations.TryCraft`**; Bruno documents the full prototype loop **gather → refine → make** through combat-relevant **`field_stim_mk0`** without inventory POST shortcuts.
**In scope (from Linear + [E3M2-06](E3M2-prototype-backlog.md#e3m2-06--craft-http--bruno-gatherrefinemake-spine)):**
- **`POST /game/players/{id}/craft`** + wire DTOs + API integration tests (AAA).
- Known-player gate via **`IPositionStateStore`** (NEO-37 / NEO-55 pattern).
- Bruno **`bruno/neon-sprawl-server/craft/`** — full spine + deny paths for major craft **`reasonCode`** values.
- **`server/README.md`** craft HTTP subsection; module doc + alignment table updates.
**Out of scope (from Linear + backlog):**
- Godot craft UI — **[NEO-74](https://linear.app/neon-sprawl/issue/NEO-74)**.
- Station interact / world bench placement.
- **`docs/manual-qa/NEO-70.md`** (kickoff decision — Bruno + README only).
- Telemetry hook sites ([NEO-71](https://linear.app/neon-sprawl/issue/NEO-71)).
## Acceptance criteria checklist
- [x] POST success returns structured **`CraftResult`** wire JSON matching engine outcome.
- [x] Bruno spine: gather **`scrap_metal_bulk`** → **`refine_scrap_standard`** (or efficient path) → **`make_field_stim_mk0`** without manual inventory POST shortcuts.
- [x] At least one deny path documented per major **`reasonCode`** (see Tests / Bruno below).
- [x] Epic 3 Slice 3 acceptance: player completes **gather → refine → usable item** (`field_stim_mk0` consumable).
## Technical approach
1. **Route:** **`POST /game/players/{id}/craft`** — trim **`id`**; **404** when empty/unknown position (NEO-55 gate).
2. **Request (`PlayerCraftRequest`, `schemaVersion` **1**):**
- **`recipeId`** (required, non-empty after trim).
- **`quantity`** (optional, default **1** when omitted or null) — passed to **`CraftOperations.TryCraft`**.
3. **Response (`CraftResponse`, `schemaVersion` **1**)** — promote NEO-69 envelope (kickoff decision):
- **`success`**: `true` on commit, `false` on rule deny.
- **`reasonCode`**: `null` on success; **`CraftReasonCodes`** string on deny.
- **`inputsConsumed`**: `{ itemId, quantity }[]` (empty on deny).
- **`outputsGranted`**: `{ itemId, quantity }[]` (empty on deny).
- **`xpGrantSummary`**: `{ skillId, amount, sourceKind }` on success; omitted or `null` on deny.
4. **HTTP status mapping (NEO-55 / NEO-38 precedent):**
- **400** — null body, **`schemaVersion`** mismatch, empty/missing **`recipeId`**.
- **404** — unknown player (position gate) or **`CraftReasonCodes.InventoryStoreMissing`** / **`ProgressionStoreMissing`** when store missing (mirror inventory POST store-missing → 404).
- **200** + JSON — engine deny with **`success: false`** + **`reasonCode`** + empty I/O lists.
- **200** + JSON — success with full envelope.
5. **Implementation:** New **`PlayerCraftApi.cs`** + **`CraftWireDtos.cs`** in `Game/Crafting/` (map request → **`CraftOperations.TryCraft`**, map **`CraftResult`** → response). Wire **`app.MapPlayerCraftApi()`** from **`Program.cs`** after **`MapPlayerInventoryApi()`**.
6. **Bruno (`bruno/neon-sprawl-server/craft/`):**
- **`Post craft gather refine make spine.bru`** — pre-request script (axios): move + interact on **four** prototype resource nodes to accumulate **≥11** **`scrap_metal_bulk`** without inventory POST (delta **5** + gamma **3** + beta **2** + alpha **1**); **`POST craft`** **`refine_scrap_efficient`** (10→2 **`refined_plate_stock`**); **`POST craft`** **`make_field_stim_mk0`**; assert **`success` true**; optional follow-up GET inventory + GET skill-progression asserts **`field_stim_mk0`** qty **≥1** and **`refine`** XP increased.
- **`Post craft deny unknown recipe.bru`** — **`recipeId`** `unknown_recipe_id`**`success` false**, **`unknown_recipe`**.
- **`Post craft deny insufficient materials.bru`** — empty player (no gather seed) → **`insufficient_materials`**.
- **`Post craft deny inventory full.bru`** — pre-request fills bag via inventory helper or repeated adds, then craft with output that cannot fit → **`inventory_full`** (may use **`make_prototype_armor`** or a make recipe after filling bag slots).
- **`Post craft deny invalid quantity.bru`** — **`quantity: 0`** → **`invalid_quantity`**.
- **`folder.bru`** — match sibling folders.
7. **Docs (on land):** Expand **`server/README.md`** craft section with POST route, request/response JSON, curl examples, reason codes. Update [E3_M2](E3_M2_RefinementAndRecipeExecution.md) **Implementation snapshot** — mark **`CraftResult`** promoted to wire; [documentation_and_implementation_alignment.md](documentation_and_implementation_alignment.md) E3.M2 row; E3M2-06 checkboxes in [E3M2-prototype-backlog.md](E3M2-prototype-backlog.md). Cross-link **NEO-74** per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md).
## Files to add
| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Crafting/CraftWireDtos.cs` | Versioned POST request/response + I/O row + XP summary JSON DTOs (`schemaVersion` 1). |
| `server/NeonSprawl.Server/Game/Crafting/PlayerCraftApi.cs` | `MapPlayerCraftApi` — POST route, position gate, engine dispatch, JSON mapping. |
| `server/NeonSprawl.Server.Tests/Game/Crafting/PlayerCraftApiTests.cs` | AAA HTTP integration: success, denies, 400/404, schema mismatch. |
| `bruno/neon-sprawl-server/craft/folder.bru` | Bruno folder metadata. |
| `bruno/neon-sprawl-server/craft/Post craft gather refine make spine.bru` | Full gather→refine→make spine (no inventory POST). |
| `bruno/neon-sprawl-server/craft/Post craft deny unknown recipe.bru` | Deny **`unknown_recipe`**. |
| `bruno/neon-sprawl-server/craft/Post craft deny insufficient materials.bru` | Deny **`insufficient_materials`**. |
| `bruno/neon-sprawl-server/craft/Post craft deny inventory full.bru` | Deny **`inventory_full`**. |
| `bruno/neon-sprawl-server/craft/Post craft deny invalid quantity.bru` | Deny **`invalid_quantity`**. |
| `docs/plans/NEO-70-implementation-plan.md` | This plan. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Program.cs` | Register **`MapPlayerCraftApi()`** alongside other player mutation APIs. |
| `server/README.md` | Document craft POST route, request/response shapes, curl examples, reason codes; remove “HTTP deferred to NEO-70” note. |
| `server/NeonSprawl.Server/Game/Crafting/CraftResult.cs` | Update XML remark — wire promotion landed in NEO-70. |
| `docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md` | **Implementation snapshot** — craft HTTP + wire **`CraftResult`** contract. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M2 row — note NEO-70 craft POST when landed. |
| `docs/plans/E3M2-prototype-backlog.md` | E3M2-06 acceptance checkboxes + landed note when complete. |
## Tests
| Test file | What it covers |
|-----------|----------------|
| `server/NeonSprawl.Server.Tests/Game/Crafting/PlayerCraftApiTests.cs` | **404** unknown player. **400** bad **`schemaVersion`**, empty **`recipeId`**. **Success:** seed inventory via store helper (test-only), POST **`refine_scrap_standard`**, assert **`success` true**, **`inputsConsumed`** / **`outputsGranted`**, **`xpGrantSummary`** (`refine`, **10**, `activity`), **`reasonCode` null**. **Deny:** **`unknown_recipe`**, **`insufficient_materials`**, **`invalid_quantity`**, **`inventory_full`** — each **200**, **`success` false**, stable **`reasonCode`**, empty I/O lists; inventory unchanged where applicable. **AAA** per [csharp-style](../../.cursor/rules/csharp-style.md). |
Bruno scripts cover end-to-end gather→craft spine and manual dev-server verification. Store-missing codes (**`inventory_store_missing`**, **`progression_store_missing`**) covered by API tests with test doubles if practical; otherwise documented as 404 paths in README (not Bruno — requires broken store).
Major **`reasonCode`** coverage matrix:
| `reasonCode` | Automated API test | Bruno |
|--------------|-------------------|-------|
| `unknown_recipe` | yes | yes |
| `insufficient_materials` | yes | yes |
| `invalid_quantity` | yes | yes |
| `inventory_full` | yes | yes |
| `inventory_store_missing` | API test or README note | no |
| `progression_store_missing` | defer to engine tests (NEO-69) | no |
## Open questions / risks
| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| **Bruno scrap totals for spine** | Use **four** distinct prototype nodes (11 scrap) + **`refine_scrap_efficient`** so **`make_field_stim_mk0`** has 2× **`refined_plate_stock`** + 1 scrap without inventory POST. | **adopted** |
| **Node depletion** | Spine Bruno must hit **different** `nodeDefId` rows (one interact each); document node order in Bruno `docs` block. | **adopted** |
| **No manual QA doc** | User chose Bruno + README only; Epic 3 Slice 3 “player completes loop” verified via Bruno spine + API tests. | **adopted** |
| **Client not in this story** | Cross-link **NEO-74**; Bruno-only verification is not prototype-complete per full-stack rule. | **adopted** |
| **NEO-71 telemetry** | No hook comments in NEO-70 — NEO-71 owns engine hook placement. | **adopted** |
## Decisions (kickoff)
- **CraftResult-only wire response** — no inventory echo (user confirmed).
- **No `docs/manual-qa/NEO-70.md`** — Bruno + README only (user confirmed; no Godot in scope).
- **HTTP deny = 200 + `success: false`** for rule denies; **400** for contract errors; **404** for unknown player / store missing (NEO-55 precedent).
- **Optional `quantity` defaults to 1** when omitted (E3M2 backlog default).