From b148db8a61e0838d060dffb80709b72dd2a31a28 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 24 May 2026 17:56:01 -0400 Subject: [PATCH] NEO-70: Add implementation plan for craft HTTP + Bruno spine. Kickoff clarifications: CraftResult-only wire response; no manual QA doc (Bruno + README). --- docs/plans/NEO-70-implementation-plan.md | 144 +++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 docs/plans/NEO-70-implementation-plan.md diff --git a/docs/plans/NEO-70-implementation-plan.md b/docs/plans/NEO-70-implementation-plan.md new file mode 100644 index 0000000..e857a0f --- /dev/null +++ b/docs/plans/NEO-70-implementation-plan.md @@ -0,0 +1,144 @@ +# 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 + +- [ ] POST success returns structured **`CraftResult`** wire JSON matching engine outcome. +- [ ] Bruno spine: gather **`scrap_metal_bulk`** → **`refine_scrap_standard`** (or efficient path) → **`make_field_stim_mk0`** without manual inventory POST shortcuts. +- [ ] At least one deny path documented per major **`reasonCode`** (see Tests / Bruno below). +- [ ] 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 (`CraftRequest`, `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).