# E3.M2 — Prototype story backlog (RefinementAndRecipeExecution)

Working backlog for **Epic 3 — Slice 3** ([recipes and crafting pipeline](../decomposition/epics/epic_03_crafting_economy.md#epic-3-slice-3)). Decomposition and contracts: [E3.M2 — RefinementAndRecipeExecution](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md).

**Labels (Linear):** every issue **`E3.M2`**; add **`server`** / **`Story`** as listed per issue.

**Precursor (do not re-scope):** [NEO-42](https://linear.app/neon-sprawl/issue/NEO-42) ships **`RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine`** — **10** **`refine`** XP with **`sourceKind: activity`** on successful craft/refine completion. Slice 3 **calls** that helper from the real **`CraftResult`** success path; NEO-42 does **not** own recipe validation, inventory mutation, or craft HTTP.

**Upstream (must be landed):** [E3.M3](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) Slice 1 — frozen item catalog, **`IPlayerInventoryStore`**, **`PlayerInventoryOperations`**, inventory HTTP ([NEO-50](https://linear.app/neon-sprawl/issue/NEO-50)–[NEO-56](https://linear.app/neon-sprawl/issue/NEO-56)). [E3.M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) Slice 2 — gather → **`scrap_metal_bulk`** ([NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)–[NEO-64](https://linear.app/neon-sprawl/issue/NEO-64)). **E2.M2** grant path ([NEO-38](https://linear.app/neon-sprawl/issue/NEO-38)).

**Prototype craft spine (frozen in E3M2-01):** **eight** `RecipeDef` rows proving **gather → process → make** on the frozen six-item catalog — **two** process (**refine**) recipes, **six** make recipes covering every **`prototypeRole`** output except **`material`** / **`intermediate`** (those are inputs). All recipes use **`requiredSkillId: refine`** (no separate **Make** skill row in the prototype skill trio). Station tags, skill level gates, and batch-quantity POST are **out of scope** for Slice 3.

**Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the same branch as implementation work.

| Slug | Linear |
|------|--------|
| E3M2-01 | [NEO-65](https://linear.app/neon-sprawl/issue/NEO-65) |
| E3M2-02 | [NEO-66](https://linear.app/neon-sprawl/issue/NEO-66) |
| E3M2-03 | [NEO-67](https://linear.app/neon-sprawl/issue/NEO-67) |
| E3M2-04 | [NEO-68](https://linear.app/neon-sprawl/issue/NEO-68) |
| E3M2-05 | [NEO-69](https://linear.app/neon-sprawl/issue/NEO-69) |
| E3M2-06 | [NEO-70](https://linear.app/neon-sprawl/issue/NEO-70) |
| E3M2-07 | [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71) |

**Dependency graph in Linear:** E3M2-02 (NEO-66) blocked by E3M2-01 (NEO-65). E3M2-03 (NEO-67) blocked by E3M2-02 (NEO-66). E3M2-04 (NEO-68) blocked by E3M2-03 (NEO-67). E3M2-05 (NEO-69) blocked by E3M2-03 (NEO-67) and E3.M3 inventory store (NEO-54). E3M2-06 (NEO-70) blocked by E3M2-05 (NEO-69). E3M2-07 (NEO-71) blocked by E3M2-05 (NEO-69; may parallel E3M2-06).

**Board order:** each issue carries **estimate 1–7** matching slug order (E3M2-01 = 1 … E3M2-07 = 7). On the Epic 3 project board, use **Display options → Ordering → Estimate** (ascending) so Todo reads top-to-bottom in delivery order.

---

## Story order (recommended)

| Order | Slug | Depends on |
|-------|------|------------|
| 1 | **E3M2-01** | E3.M3 item catalog (NEO-50) |
| 2 | **E3M2-02** | E3M2-01 |
| 3 | **E3M2-03** | E3M2-02 |
| 4 | **E3M2-04** | E3M2-03 |
| 5 | **E3M2-05** | E3M2-03, E3.M3 inventory store (NEO-54+) |
| 6 | **E3M2-06** | E3M2-05 |
| 7 | **E3M2-07** | E3M2-05 |

**Downstream (separate modules):** [E7.M1](../decomposition/modules/E7_M1_QuestStateMachine.md) quest steps requiring crafted items; [E3.M4](../decomposition/modules/E3_M4_SinkAndDurabilityLifecycle.md) durability sinks; [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) bench/station placement later.

---

## Kickoff decisions (decomposition defaults)

| Topic | Decision | Rationale |
|-------|----------|-----------|
| Recipe count | **8** frozen defs (epic minimum 8–12) | Two refine paths + six make outputs without expanding the item catalog |
| Skill id | **`refine`** on every recipe | Prototype skill trio has no separate **Make** skill; matches [NEO-42](NEO-42-implementation-plan.md) |
| Skill level gates | **None** in prototype | `requiredSkillLevel` omitted or **0**; gates are a content follow-up |
| Station / bench tags | **None** in prototype | Craft always allowed via HTTP; zone/bench gating defers to [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) |
| Craft wire surface | **`POST /game/players/{id}/craft`** | Mirror [NEO-55](https://linear.app/neon-sprawl/issue/NEO-55) inventory POST pattern; no station interact in Slice 3 |
| Request shape | **`recipeId`** + optional **`quantity`** default **1** | One craft execution per POST; multi-output comes from recipe **`outputs`** |
| Atomic commit | Consume **all** inputs, then grant **all** outputs, then XP | Roll back input removal if output grant fails (mirror gather engine rollback) |
| XP | **`RefineActivitySkillXpGrant`** once per successful craft | NEO-42 already landed; E3M2-05 owns the call site |
| RNG / quality / failure | **None** — deterministic success when inputs + bag space allow | Soft craft failure and quality tiers are pre-production ([crafting.md](../game-design/crafting.md)) |
| Primary demo loop | Gather **`scrap_metal_bulk`** → **`refine_scrap_standard`** → **`make_field_stim_mk0`** | Proves combat-relevant consumable from world nodes |

---

### E3M2-01 — Prototype RecipeDef catalog + schemas + CI

**Goal:** Lock content shape and CI validation for **eight** frozen recipes before server load.

**In scope**

- `content/schemas/recipe-def.schema.json` and `content/schemas/recipe-io-row.schema.json` (input/output row shape).
- `content/recipes/prototype_recipes.json` with stable **`id`** values (see module doc freeze table).
- `scripts/validate_content.py`: schema validation, duplicate ids, exact eight-recipe allowlist, **`recipeKind`** coverage (**`process`** + **`make`**), cross-check that every **`itemId`** in inputs/outputs exists in `content/items/`, every **`requiredSkillId`** exists in `content/skills/`.
- Designer note in [E3_M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) + `content/README.md`.

**Out of scope**

- Server loader, craft engine, HTTP.

**Acceptance criteria**

- [x] PR gate validates recipe JSON against schema.
- [x] Exactly eight prototype recipe ids; duplicate `id` fails CI.
- [x] Every input/output references a valid frozen item id; every recipe references **`refine`**.
- [x] Stable id list documented in module doc freeze box.

**Landed ([NEO-65](https://linear.app/neon-sprawl/issue/NEO-65)):** [`content/recipes/prototype_recipes.json`](../../content/recipes/prototype_recipes.json), [`recipe-def.schema.json`](../../content/schemas/recipe-def.schema.json), [`recipe-io-row.schema.json`](../../content/schemas/recipe-io-row.schema.json), CI gates in [`validate_content.py`](../../scripts/validate_content.py); plan [NEO-65-implementation-plan.md](NEO-65-implementation-plan.md).

---

### E3M2-02 — Server recipe catalog load (fail-fast)

**Goal:** Disk → host: startup load of `content/recipes/*.json` with CI-parity validation.

**Kickoff note (NEO-66):** Duplicate **`PROTOTYPE_SLICE3_RECIPE_IDS`** (and Slice 3 gate rules) in C# with an explicit **“keep in sync with `scripts/validate_content.py`”** comment — mirror [NEO-51](../../plans/NEO-51-implementation-plan.md) **`PrototypeSlice1ItemCatalogRules`** / **`PROTOTYPE_SLICE1_ITEM_IDS`**.

**In scope**

- Loader + catalog types under `server/NeonSprawl.Server/Game/Crafting/` (path TBD in plan).
- Fail-fast on malformed files, duplicate ids, unknown item ids (cross-check **`IItemDefinitionRegistry`**), unknown skill ids (cross-check **`ISkillDefinitionRegistry`**).
- Unit tests (AAA) for loader happy path and failure modes.

**Out of scope**

- Per-player craft state, HTTP, inventory mutation.

**Acceptance criteria**

- [x] Server refuses boot when recipe catalog invalid (mirror E3.M1 / E3.M3 catalog behavior).
- [x] Catalog resolves `RecipeDef` and I/O rows by recipe `id`.

**Landed ([NEO-66](https://linear.app/neon-sprawl/issue/NEO-66)):** `server/NeonSprawl.Server/Game/Crafting/` — `RecipeDefinitionCatalogLoader`, `RecipeDefinitionCatalog`, `PrototypeSlice3RecipeCatalogRules`; plan [NEO-66-implementation-plan.md](NEO-66-implementation-plan.md).

---

### E3M2-03 — Recipe definition registry + DI

**Goal:** Injectable `IRecipeDefinitionRegistry` consumed by craft engine and HTTP.

**In scope**

- `RecipeDefinitionRegistry` implementation + DI registration.
- Unit tests (AAA): lookup, unknown id, input/output metadata, **`recipeKind`**.

**Out of scope**

- HTTP, persistence.

**Acceptance criteria**

- [x] Host resolves registry from DI; unknown `recipeId` distinguishable from valid ids in tests.

**Landed ([NEO-67](https://linear.app/neon-sprawl/issue/NEO-67)):** `IRecipeDefinitionRegistry` + `RecipeDefinitionRegistry` in `server/NeonSprawl.Server/Game/Crafting/`; plan [NEO-67-implementation-plan.md](NEO-67-implementation-plan.md).

---

### E3M2-04 — GET world recipe-definitions

**Goal:** Versioned read-only catalog for Bruno, client preview, and tooling — mirror [NEO-53](https://linear.app/neon-sprawl/issue/NEO-53) item-definitions pattern.

**In scope**

- `GET /game/world/recipe-definitions` + DTOs + API tests (AAA).
- Bruno folder `bruno/neon-sprawl-server/recipe-definitions/`.
- `server/README.md` section.

**Out of scope**

- Per-player craft history or cooldown state.

**Acceptance criteria**

- [x] GET returns all eight prototype recipes with schema version field and input/output summary fields needed for QA.
- [x] Bruno happy path documented.

**Landed ([NEO-68](https://linear.app/neon-sprawl/issue/NEO-68)):** `GET /game/world/recipe-definitions` — `RecipeDefinitionsWorldApi` + DTOs in `server/NeonSprawl.Server/Game/Crafting/`; plan [NEO-68-implementation-plan.md](NEO-68-implementation-plan.md); manual QA [`NEO-68`](../manual-qa/NEO-68.md); Bruno `bruno/neon-sprawl-server/recipe-definitions/`.

---

### E3M2-05 — CraftResult engine (consume inputs, grant outputs, refine XP)

**Goal:** Single server operation that resolves **`CraftResult`**: validate recipe + inventory, atomically remove inputs and add outputs via **`PlayerInventoryOperations`**, award **`refine`** XP via **`RefineActivitySkillXpGrant`** on success.

**In scope**

- `CraftOperations` / `CraftResult` types in `server/NeonSprawl.Server/Game/Crafting/`.
- Stable deny reason codes: `unknown_recipe`, `insufficient_materials`, `inventory_full`, `invalid_quantity` (if quantity validated).
- Rollback input removal when output grant fails.
- Unit tests (AAA) with stub inventory + progression stores; NEO-42 grant invoked on success path.
- Document **`CraftResult`** shape in module doc (server-internal until promoted to wire).

**Out of scope**

- Craft HTTP (E3M2-06).
- Client HUD.
- Skill level gates, station checks, craft failure RNG.

**Acceptance criteria**

- [x] Successful craft removes configured inputs and adds outputs deterministically.
- [x] Insufficient materials returns stable `reasonCode` with **no** inventory change.
- [x] Full bag on output returns stable `reasonCode`; inputs are **not** consumed (or are rolled back).
- [x] **`refine`** skill XP increases by **10** on success (NEO-42).
- [x] **`make_prototype_armor`** respects equipment slot rules from E3.M3 when bag is full but equip slot empty.

**Landed ([NEO-69](https://linear.app/neon-sprawl/issue/NEO-69)):** `CraftOperations.TryCraft` + `CraftResult` in `server/NeonSprawl.Server/Game/Crafting/`; plan [NEO-69-implementation-plan.md](NEO-69-implementation-plan.md); [server README — Craft engine (NEO-69)](../../server/README.md#craft-engine-neo-69).

---

### E3M2-06 — Craft HTTP + Bruno gather→refine→make spine

**Goal:** Versioned **`POST /game/players/{id}/craft`** wired to craft engine; Bruno documents full prototype loop from gather through combat-relevant output.

**In scope**

- `POST /game/players/{id}/craft` + DTOs + API tests (AAA).
- Known-player gate via `IPositionStateStore` (NEO-37 / NEO-55 pattern).
- Bruno `bruno/neon-sprawl-server/craft/` with spine: seed or gather **`scrap_metal_bulk`** → **`refine_scrap_standard`** → **`make_field_stim_mk0`** → GET inventory + GET skill-progression shows **`refine`** XP delta.
- Bruno deny paths: unknown recipe, insufficient materials, inventory full on output.
- `server/README.md`; integration tests (AAA) where practical.

**Out of scope**

- Godot craft UI — **[Epic 3 Slice 5](E3S5-client-prototype-backlog.md#e3s5-03--client-craft-ui--recipe-list-e3m2)** ([NEO-74](https://linear.app/neon-sprawl/issue/NEO-74)).
- Station interact / world bench placement.

**Acceptance criteria**

- [x] POST success returns structured **`CraftResult`** (or HTTP envelope) matching engine outcome.
- [x] Bruno spine completes without manual inventory POST shortcuts (gather interact allowed for scrap).
- [x] At least one deny path documented per major `reasonCode`.
- [x] Epic 3 Slice 3 acceptance: player completes **gather → refine → usable item** (`field_stim_mk0` consumable).

**Landed ([NEO-70](https://linear.app/neon-sprawl/issue/NEO-70)):** **`POST /game/players/{id}/craft`** — `PlayerCraftApi` + `CraftWireDtos`; Bruno `bruno/neon-sprawl-server/craft/`; plan [NEO-70-implementation-plan.md](NEO-70-implementation-plan.md); [server README — Craft HTTP (NEO-70)](../../server/README.md#craft-engine-neo-69-and-craft-http-neo-70).

---

### E3M2-07 — `item_crafted` / `craft_failed` telemetry hook sites

**Goal:** Comment-only hooks on craft success and structured deny for future E9.M1 catalog events.

**In scope**

- Hook placement in **`CraftOperations`** (success + deny before commit — engine-only, mirror NEO-64 gather pattern).
- `TODO(E9.M1)` comments; no production logging.
- README + module doc pointer; note **`time-to-first-craft`** is a future metric derived from these events + session context (no separate hook in prototype).

**Out of scope**

- Telemetry ingest, dashboards.
- HTTP-layer duplicate hooks if engine owns the contract.

**Acceptance criteria**

- [x] Hook sites documented in code and `server/README.md`.
- [x] Matches Epic 3 Slice 3 telemetry vocabulary in [epic_03](../decomposition/epics/epic_03_crafting_economy.md#epic-3-slice-3).

**Landed ([NEO-71](https://linear.app/neon-sprawl/issue/NEO-71)):** comment-only **`item_crafted`** / **`craft_failed`** hook sites in **`CraftOperations.TryCraft`**; plan [NEO-71-implementation-plan.md](NEO-71-implementation-plan.md); [server README — Craft telemetry hooks (NEO-71)](../../server/README.md#craft-telemetry-hooks-neo-71).

---

## After this backlog

- Epic 3 **prototype server** gather/craft spine complete when **E3M2-07** lands; **playable client loop** completes with **[Epic 3 Slice 5](E3S5-client-prototype-backlog.md)** ([NEO-72](https://linear.app/neon-sprawl/issue/NEO-72)–[NEO-75](https://linear.app/neon-sprawl/issue/NEO-75)).
- Slice 4 (**E3.M4** + **E3.M5**) sinks/policy remains pre-production.
- Track delivery in Linear; keep `blockedBy` links synchronized if scope changes.
- For each issue kickoff, add `docs/plans/{NEO-XX}-implementation-plan.md` per [story-kickoff](../../.cursor/rules/story-kickoff.md).
- Add `docs/manual-qa/{NEO-XX}.md` when craft HTTP lands (E3M2-06+) — **[NEO-70](https://linear.app/neon-sprawl/issue/NEO-70) waived** manual QA at kickoff (Bruno + README only).

## Related docs

- [E3_M2_RefinementAndRecipeExecution.md](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md)
- [E3M1-prototype-backlog.md](E3M1-prototype-backlog.md)
- [E3M3-prototype-backlog.md](E3M3-prototype-backlog.md)
- [epic_03_crafting_economy.md](../decomposition/epics/epic_03_crafting_economy.md)
- [crafting.md](../game-design/crafting.md)
- [NEO-42 implementation plan](NEO-42-implementation-plan.md)
