145 lines
12 KiB
Markdown
145 lines
12 KiB
Markdown
# 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).
|