# 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).
