diff --git a/.cursor/rules/code-review-agent.md b/.cursor/rules/code-review-agent.md index a2ec9de..09e7304 100644 --- a/.cursor/rules/code-review-agent.md +++ b/.cursor/rules/code-review-agent.md @@ -50,6 +50,7 @@ Work through what applies to the diff (skip irrelevant sections briefly). 6. **Observability** — Logging/metrics where failures would be hard to diagnose (server/runtime code). 7. **Docs** — README or plan updates when behavior or run instructions change. 8. **Plan & decomposition alignment** — Per **Plan and decomposition documentation** above: relevant `docs/plans/` and module/policy docs cited; implementation checked against them; **Documentation checked** section in the saved review file. +9. **Full-stack (player-visible work)** — Per [full-stack-epic-decomposition](full-stack-epic-decomposition.md): if the PR claims a **prototype slice** or **player-facing feature** is complete, verify a **`client`** Linear issue exists in the backlog and manual QA covers **Godot** (not Bruno-only). Missing client counterpart → **should fix**; claiming epic slice done without client → **blocking** when merge message or plan says “prototype complete.” ## Code review document (required) diff --git a/.cursor/rules/docs-review-agent.md b/.cursor/rules/docs-review-agent.md index 2d0ef56..d9d1cf7 100644 --- a/.cursor/rules/docs-review-agent.md +++ b/.cursor/rules/docs-review-agent.md @@ -41,8 +41,7 @@ Work through what applies (skip irrelevant briefly). 5. **Implementation hooks** — IDs, modules (E2.M1, …), open vs agreed; what’s missing for tickets. 6. **Links & anchors** — Broken paths; fragile `#anchors` (prefer stable HTML `id` where needed). 7. **Next steps** — Obvious follow-up artifacts or epic/plan updates. - -## Review document (required) +8. **Full-stack backlogs** — For `docs/plans/*backlog*.md`: every player-visible story has a **`client`** Linear counterpart per [full-stack-epic-decomposition](full-stack-epic-decomposition.md); flag “optional follow-up” / “future client” without NEO-XX as **should fix**. **Every** invocation must produce a **saved markdown file** under `docs/reviews/`, not only chat (same spirit as [code-review-agent](code-review-agent.md)). diff --git a/.cursor/rules/full-stack-epic-decomposition.md b/.cursor/rules/full-stack-epic-decomposition.md new file mode 100644 index 0000000..2dd11c2 --- /dev/null +++ b/.cursor/rules/full-stack-epic-decomposition.md @@ -0,0 +1,78 @@ +--- +description: Epics deliver playable game features — prototype backlogs must pair server + client stories; forbid Bruno-only "optional UI" deferrals. +alwaysApply: true +--- + +# Full-stack epic decomposition (CRITICAL) + +Neon Sprawl is a **game**, not a headless server. Prototype backlogs and epic slices must ship **Godot presentation** for every **player-visible** feature. + +## When this rule applies + +- Writing or editing **`docs/plans/*backlog*.md`**, **`docs/plans/E*S*-client*.md`**, or **implementation slices** in **`docs/decomposition/epics/epic_*.md`** +- Creating **Linear issues** during decomposition (not at ad-hoc kickoff without client counterparts) +- Reviewing whether an epic/module slice is **“decomposed”** or **“prototype complete”** + +Reference: [decomposition README — Full-stack epics](../../docs/decomposition/README.md#full-stack-epics), [prototype backlog template](../../docs/plans/_prototype-backlog-template.md). + +## Canonical pattern + +Mirror **Epic 1** ([E1M3-prototype-backlog](../../docs/plans/E1M3-prototype-backlog.md)): server contract story immediately followed by client HUD/wiring (e.g. NEO-23 → NEO-24). + +For large epics, a dedicated **client slice** file is OK ([E3S5-client-prototype-backlog.md](../../docs/plans/E3S5-client-prototype-backlog.md)) — but **client Linear issues must exist in the same decomposition pass** as server issues, with `blockedBy` links. + +## Story classification + +| Type | Client story required? | Examples | +|------|------------------------|----------| +| **Infrastructure** | **No** | JSON Schema, CI gates, server catalog load, DB migrations, comment-only telemetry hooks | +| **Player-visible** | **Yes** | Inventory, gather/craft, quests, zone/PvP warnings, combat telegraphs, skill/perk UI, any AC with *player*, *visible*, *HUD*, *select*, *complete*, *understand* | +| **Capstone** | **Yes** | “Playable loop in Godot without Bruno” (e.g. NEO-75) | + +Tag **infrastructure-only** stories explicitly in the backlog so agents do not skip UI for the wrong reasons. + +## Mandatory backlog contents + +When creating `docs/plans/E*M*-prototype-backlog.md` (copy [\_prototype-backlog-template.md](../../docs/plans/_prototype-backlog-template.md)): + +1. **Server + client story tables** — every player-visible slug lists **`Client counterpart: NEO-XX`** or points to a client slice slug. +2. **Linear issues created together** — all server **and** client issues + **`blockedBy`** in one decomposition pass; label client issues **`client`** + module id (`E3.M1`, etc.). +3. **Capstone client story** — at least one issue proving the slice is **playable in Godot** (manual QA in `docs/manual-qa/{KEY}.md`), unless the entire epic slice is infrastructure-only (rare). +4. **Epic DoD updated** — prototype Definition of Done says **Godot**, not Bruno-only (see [epic_03 example](../../docs/decomposition/epics/epic_03_crafting_economy.md)). + +## Forbidden language + +Do **not** use in backlogs or Linear descriptions for player-visible work: + +- “Godot HUD (**optional follow-up**)” +- “for manual QA and **future client**” (without a linked **NEO-XX** client issue) +- “client UI deferred” without a **created** client issue id + +Use **Out of scope** only for genuinely non-prototype UX (e.g. drag-drop inventory, final art pass). + +## Bruno vs Godot + +- **Bruno** — proves HTTP contracts and server authority. +- **Godot** — proves [vision prototype gates](../../neon_sprawl_vision.plan.md) (fun, readability, progression visibility). + +Do **not** mark a **player-facing** epic slice **complete** when only Bruno passes. + +## Kickoff and implementation + +- **Story kickoff** for a **server** issue on a player-visible path: plan must **cross-link** the **client counterpart** Linear issue (even if not started). See [story-kickoff](story-kickoff.md). +- **Manual QA** ([planning-implementation-docs](planning-implementation-docs.md)): player-visible **client** stories require `docs/manual-qa/{KEY}.md` with **Godot** steps, not API-only checks. +- **Client scripts:** thin `main.gd`, HTTP client nodes — [godot-client-script-organization](godot-client-script-organization.md). + +## Decomposition complete checklist + +Before calling an epic slice **decomposed**: + +- [ ] Every player-visible acceptance criterion maps to a **`client`** Linear issue +- [ ] No forbidden deferral language in backlog +- [ ] Capstone or client slice issue exists for playable verification +- [ ] Epic / module docs and [documentation_and_implementation_alignment.md](../../docs/decomposition/modules/documentation_and_implementation_alignment.md) mention client backlog +- [ ] [module_dependency_register.md](../../docs/decomposition/modules/module_dependency_register.md) note updated when issues are created + +## Code review + +Reviewers (human or [code-review-agent](code-review-agent.md)) **should fix** or **block** claims that a **player-facing** prototype slice is done when backlog has **no client issue** or manual QA is Bruno-only. diff --git a/.cursor/rules/story-kickoff.md b/.cursor/rules/story-kickoff.md index 673a534..0427e56 100644 --- a/.cursor/rules/story-kickoff.md +++ b/.cursor/rules/story-kickoff.md @@ -58,6 +58,8 @@ When you use **`AskQuestion`** for kickoff blocking decisions, follow **all** of - Do not add or change source, Godot scenes/project, game data pipelines, or automated tests until step 4 is done **and** the user has moved past planning in chat (e.g. explicit go-ahead to implement). +**Full-stack:** If the story is **server-only** but **player-visible** (HTTP API without Godot yet), the kickoff plan must **cross-link** the **client counterpart** Linear issue from the module backlog ([full-stack-epic-decomposition](full-stack-epic-decomposition.md)). Do not treat Bruno-only verification as prototype-complete. + ## 4. Planning document - Add **`docs/plans/{LINEAR_ISSUE_ID}-implementation-plan.md`** (example: `docs/plans/NEO-5-implementation-plan.md`). Create `docs/plans/` if it does not exist. diff --git a/AGENTS.md b/AGENTS.md index bc1fa47..ae388bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ Optional **personas** for Cursor. Enable by **@ mentioning** the rule in chat or | **Code review** | [`.cursor/rules/code-review-agent.md`](.cursor/rules/code-review-agent.md) | PR / diff / pre-merge review; **always writes** `docs/reviews/YYYY-MM-DD-{slug}.md` with **Documentation checked** vs `docs/plans/` and `docs/decomposition/modules/`; **when suggestions are fixed, strike through those bullets + `Done.` in that file** ([planning-implementation-docs](.cursor/rules/planning-implementation-docs.md)); short chat pointer | | **Docs review** | [`.cursor/rules/docs-review-agent.md`](.cursor/rules/docs-review-agent.md) | Coherence / links / dev-guide fitness for `docs/` (especially `docs/game-design/` + decomposition); **writes** `docs/reviews/YYYY-MM-DD-{slug}.md`; **when suggestions are fixed, strike through + `Done.` in that file** ([planning-implementation-docs](.cursor/rules/planning-implementation-docs.md)); use when working in **documents** or @ mention | -Project-wide conventions live under [`.cursor/rules/`](.cursor/rules/) (many are `alwaysApply`). **Planning / implementation decisions** must be written into `docs/plans/` (and related docs)—[`.cursor/rules/planning-implementation-docs.md`](.cursor/rules/planning-implementation-docs.md). **Manual QA checklists** for user-visible ticketed work are generated during implementation at `docs/manual-qa/{KEY}.md` (same rule). **Automated tests (AAA, mandatory):** **C#** — every new or changed **`[Fact]` / `[Theory]`** method in **`*Tests.cs`** must use full **Arrange → Act → Assert**: the three **`// Arrange` / `// Act` / `// Assert`** labels, **Act** = behavior under test only, **Assert** = all expectations including **`ReadFromJsonAsync`** (etc.) for verification (no blank line required after the phase comments) — [csharp-style](.cursor/rules/csharp-style.md#unit-and-integration-tests-arrange-act-assert), [testing-expectations](.cursor/rules/testing-expectations.md); snippets **`xut`** / **`xutc`**. **GdUnit (`client/test/`)** — same AAA with **`# Arrange` / `# Act` / `# Assert`** — [gdscript-style](.cursor/rules/gdscript-style.md#gdunit-test-layout-aaa). **Godot client layout** (thin `main.gd`, split by concern): [`.cursor/rules/godot-client-script-organization.md`](.cursor/rules/godot-client-script-organization.md). **Git:** on Linear/story work, agents **commit as they go** on the story branch (kickoff plan, coherent batches)—**do not wait** for the user to say “commit”; **never** `git push` unless the user asks — [`.cursor/rules/commit-and-review.md`](.cursor/rules/commit-and-review.md). **Open PR:** when the user asks, use the **GitHub MCP** **`create_pull_request`** tool (`user-github`) with a title starting **`NEO-123:`** ([commit-and-review](.cursor/rules/commit-and-review.md)); **do not use `gh` CLI for GitHub operations in this repo**. **Story lifecycle:** kickoff [`.cursor/rules/story-kickoff.md`](.cursor/rules/story-kickoff.md) — blocking decisions need a **Recommend:** preamble in chat **before** `AskQuestion`, plus **(recommended)** on the chosen option label; `AskQuestion` alone is a violation. After merge / end of story — `checkout main`, `pull`, delete local story branch — [`.cursor/rules/story-end.md`](.cursor/rules/story-end.md). **Linear:** on **end story**, use **`AskQuestion`** multiple choice for **In Test** / **Done** / **Skip** when Agent mode supports it ([story-end](.cursor/rules/story-end.md) §4a); otherwise ask in prose. Wait for the choice (or the same message naming the state) **before** `save_issue`; do not guess. **PR / push text:** no “Made-with: Cursor” boilerplate (same file). +Project-wide conventions live under [`.cursor/rules/`](.cursor/rules/) (many are `alwaysApply`). **Full-stack epics:** prototype backlogs must pair **server + client** Linear issues for player-visible features — [`.cursor/rules/full-stack-epic-decomposition.md`](.cursor/rules/full-stack-epic-decomposition.md), template [`docs/plans/_prototype-backlog-template.md`](docs/plans/_prototype-backlog-template.md). **Planning / implementation decisions** must be written into `docs/plans/` (and related docs)—[`.cursor/rules/planning-implementation-docs.md`](.cursor/rules/planning-implementation-docs.md). **Manual QA checklists** for user-visible ticketed work are generated during implementation at `docs/manual-qa/{KEY}.md` (same rule). **Automated tests (AAA, mandatory):** **C#** — every new or changed **`[Fact]` / `[Theory]`** method in **`*Tests.cs`** must use full **Arrange → Act → Assert**: the three **`// Arrange` / `// Act` / `// Assert`** labels, **Act** = behavior under test only, **Assert** = all expectations including **`ReadFromJsonAsync`** (etc.) for verification (no blank line required after the phase comments) — [csharp-style](.cursor/rules/csharp-style.md#unit-and-integration-tests-arrange-act-assert), [testing-expectations](.cursor/rules/testing-expectations.md); snippets **`xut`** / **`xutc`**. **GdUnit (`client/test/`)** — same AAA with **`# Arrange` / `# Act` / `# Assert`** — [gdscript-style](.cursor/rules/gdscript-style.md#gdunit-test-layout-aaa). **Godot client layout** (thin `main.gd`, split by concern): [`.cursor/rules/godot-client-script-organization.md`](.cursor/rules/godot-client-script-organization.md). **Git:** on Linear/story work, agents **commit as they go** on the story branch (kickoff plan, coherent batches)—**do not wait** for the user to say “commit”; **never** `git push` unless the user asks — [`.cursor/rules/commit-and-review.md`](.cursor/rules/commit-and-review.md). **Open PR:** when the user asks, use the **GitHub MCP** **`create_pull_request`** tool (`user-github`) with a title starting **`NEO-123:`** ([commit-and-review](.cursor/rules/commit-and-review.md)); **do not use `gh` CLI for GitHub operations in this repo**. **Story lifecycle:** kickoff [`.cursor/rules/story-kickoff.md`](.cursor/rules/story-kickoff.md) — blocking decisions need a **Recommend:** preamble in chat **before** `AskQuestion`, plus **(recommended)** on the chosen option label; `AskQuestion` alone is a violation. After merge / end of story — `checkout main`, `pull`, delete local story branch — [`.cursor/rules/story-end.md`](.cursor/rules/story-end.md). **Linear:** on **end story**, use **`AskQuestion`** multiple choice for **In Test** / **Done** / **Skip** when Agent mode supports it ([story-end](.cursor/rules/story-end.md) §4a); otherwise ask in prose. Wait for the choice (or the same message naming the state) **before** `save_issue`; do not guess. **PR / push text:** no “Made-with: Cursor” boilerplate (same file). **Commits tied to a Linear issue** must start the subject with the **issue id** and a colon (e.g. `NEO-8: …`). Branch naming and exceptions (`chore:` when there is no ticket) — [`.cursor/rules/linear-git-naming.md`](.cursor/rules/linear-git-naming.md). diff --git a/content/README.md b/content/README.md index 4141897..4b6ca19 100644 --- a/content/README.md +++ b/content/README.md @@ -8,6 +8,7 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with ** | [`skills/`](skills/) | Skill catalogs; each row matches [`schemas/skill-def.schema.json`](schemas/skill-def.schema.json) — **stable `id`**, **`allowedXpSourceKinds`** for [E2.M1](../docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md) / [E2.M2](../docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md) | | [`mastery/`](mastery/) | Mastery catalogs; each file matches [`schemas/mastery-catalog.schema.json`](schemas/mastery-catalog.schema.json) — **stable `branchId` / `perkId`**, tier gates via skill level ([E2.M3](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md)) | | [`items/`](items/) | Item catalogs; each row matches [`schemas/item-def.schema.json`](schemas/item-def.schema.json) — **stable `id`**, **`stackMax`**, **`inventorySlotKind`** for [E3.M3](../docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) | +| [`recipes/`](recipes/) | Recipe catalogs; each row matches [`schemas/recipe-def.schema.json`](schemas/recipe-def.schema.json) — **stable `id`**, **`recipeKind`**, **`requiredSkillId`**, inputs/outputs for [E3.M2](../docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) | | [`resource-nodes/`](resource-nodes/) | Resource node + yield catalogs; node rows match [`schemas/resource-node-def.schema.json`](schemas/resource-node-def.schema.json), yield rows match [`schemas/resource-yield-row.schema.json`](schemas/resource-yield-row.schema.json) — **stable `nodeDefId`**, **`gatherLens`**, **`maxGathers`** for [E3.M1](../docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) | **Prototype Slice 1 (NEO-33):** CI expects **exactly three** skill rows with ids **`salvage`**, **`refine`**, **`intrusion`** (gather + process + tech). Each row must declare **`allowedXpSourceKinds`** (non-empty); see [E2.M1 — Designer note: `allowedXpSourceKinds`](../docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md#designer-note-allowedxpsourcekinds-and-grant-call-sites) for what each kind means for grant wiring. @@ -16,6 +17,8 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with ** **Prototype Slice 1 — items (NEO-50):** CI expects **exactly six** item rows with ids **`scrap_metal_bulk`**, **`refined_plate_stock`**, **`field_stim_mk0`**, **`survey_drone_kit`**, **`contract_handoff_token`**, **`prototype_armor_shell`** — one row per **`prototypeRole`** (`material` through `equip_stub`). **Do not rename** ids after ship—change **`displayName`** only. See [E3.M3 — Designer note](../docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md#designer-note-stack-slot-and-v1-scope) and [freeze table](../docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md#prototype-slice-1-freeze-neo-50). +**Prototype Slice 3 — recipes (E3M2-01):** CI expects **exactly eight** recipe ids aligned to [E3.M2 recipe freeze](../docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md#prototype-slice-3-freeze-e3m2-01) — at least one **`process`** and one **`make`** row; all **`requiredSkillId`** values **`refine`**; every input/output **`itemId`** from the frozen item catalog. **Do not rename** recipe `id` after ship. See [E3M2-prototype-backlog.md](../docs/plans/E3M2-prototype-backlog.md). + **Prototype Slice 4 (NEO-45):** CI expects **exactly one** `MasteryTrack` for **`salvage`** only (`refine` / `intrusion` have no mastery rows yet). Tier 1 @ skill level **2** is a **mutually exclusive** branch pick (`scrap_efficiency` vs `bulk_haul`) with **no** perks; tier 2 @ level **4** unlocks one perk per branch path. **Do not rename** `branchId` / `perkId` after ship—change `displayName` only. See [E2.M3 — Designer note](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#designer-note-tiers-branches-and-level-gates) and [freeze table](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#prototype-slice-4-freeze--salvage-flagship-neo-45). Server load path and hot-reload policy are TBD; keep files versioned here as the source of truth. diff --git a/content/recipes/prototype_recipes.json b/content/recipes/prototype_recipes.json new file mode 100644 index 0000000..9702ae7 --- /dev/null +++ b/content/recipes/prototype_recipes.json @@ -0,0 +1,87 @@ +{ + "schemaVersion": 1, + "recipes": [ + { + "id": "refine_scrap_standard", + "displayName": "Refine Scrap (Standard)", + "recipeKind": "process", + "requiredSkillId": "refine", + "inputs": [{ "itemId": "scrap_metal_bulk", "quantity": 5 }], + "outputs": [{ "itemId": "refined_plate_stock", "quantity": 1 }] + }, + { + "id": "refine_scrap_efficient", + "displayName": "Refine Scrap (Efficient)", + "recipeKind": "process", + "requiredSkillId": "refine", + "inputs": [{ "itemId": "scrap_metal_bulk", "quantity": 10 }], + "outputs": [{ "itemId": "refined_plate_stock", "quantity": 2 }] + }, + { + "id": "make_field_stim_mk0", + "displayName": "Make Field Stim Mk0", + "recipeKind": "make", + "requiredSkillId": "refine", + "inputs": [ + { "itemId": "refined_plate_stock", "quantity": 2 }, + { "itemId": "scrap_metal_bulk", "quantity": 1 } + ], + "outputs": [{ "itemId": "field_stim_mk0", "quantity": 1 }] + }, + { + "id": "make_field_stim_batch", + "displayName": "Make Field Stim (Batch)", + "recipeKind": "make", + "requiredSkillId": "refine", + "inputs": [ + { "itemId": "refined_plate_stock", "quantity": 5 }, + { "itemId": "scrap_metal_bulk", "quantity": 3 } + ], + "outputs": [{ "itemId": "field_stim_mk0", "quantity": 3 }] + }, + { + "id": "make_prototype_armor", + "displayName": "Make Prototype Armor Shell", + "recipeKind": "make", + "requiredSkillId": "refine", + "inputs": [ + { "itemId": "refined_plate_stock", "quantity": 8 }, + { "itemId": "scrap_metal_bulk", "quantity": 5 } + ], + "outputs": [{ "itemId": "prototype_armor_shell", "quantity": 1 }] + }, + { + "id": "make_armor_quick", + "displayName": "Make Armor (Quick)", + "recipeKind": "make", + "requiredSkillId": "refine", + "inputs": [ + { "itemId": "refined_plate_stock", "quantity": 4 }, + { "itemId": "scrap_metal_bulk", "quantity": 3 } + ], + "outputs": [{ "itemId": "prototype_armor_shell", "quantity": 1 }] + }, + { + "id": "make_survey_drone_kit", + "displayName": "Make Survey Drone Kit", + "recipeKind": "make", + "requiredSkillId": "refine", + "inputs": [ + { "itemId": "refined_plate_stock", "quantity": 3 }, + { "itemId": "scrap_metal_bulk", "quantity": 2 } + ], + "outputs": [{ "itemId": "survey_drone_kit", "quantity": 1 }] + }, + { + "id": "make_contract_token", + "displayName": "Make Contract Handoff Token", + "recipeKind": "make", + "requiredSkillId": "refine", + "inputs": [ + { "itemId": "refined_plate_stock", "quantity": 2 }, + { "itemId": "scrap_metal_bulk", "quantity": 1 } + ], + "outputs": [{ "itemId": "contract_handoff_token", "quantity": 1 }] + } + ] +} diff --git a/content/schemas/recipe-def.schema.json b/content/schemas/recipe-def.schema.json new file mode 100644 index 0000000..3592c37 --- /dev/null +++ b/content/schemas/recipe-def.schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://neon-sprawl.local/schemas/recipe-def.json", + "title": "RecipeDef", + "description": "Single recipe row for catalogs (e.g. content/recipes/*_recipes.json). IDs are stable forever—rename display in displayName only. See docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md.", + "type": "object", + "additionalProperties": false, + "required": ["id", "displayName", "recipeKind", "requiredSkillId", "inputs", "outputs"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Immutable recipe key for craft HTTP, quests, telemetry, and catalog lookup." + }, + "displayName": { + "type": "string", + "minLength": 1, + "description": "Player-facing label; safe to change without migrating character data." + }, + "recipeKind": { + "type": "string", + "description": "process = refine/intermediate; make = finished good.", + "enum": ["process", "make"] + }, + "requiredSkillId": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "SkillDef id gating craft execution (prototype Slice 3: refine on all rows)." + }, + "inputs": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "https://neon-sprawl.local/schemas/recipe-io-row.json" + } + }, + "outputs": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "https://neon-sprawl.local/schemas/recipe-io-row.json" + } + }, + "requiredSkillLevel": { + "type": "integer", + "minimum": 0, + "description": "Optional level gate stub (unused in prototype Slice 3 rows)." + }, + "stationTag": { + "type": "string", + "minLength": 1, + "description": "Optional station/bench requirement stub (unused in prototype Slice 3)." + } + } +} diff --git a/content/schemas/recipe-io-row.schema.json b/content/schemas/recipe-io-row.schema.json new file mode 100644 index 0000000..c2b9fb4 --- /dev/null +++ b/content/schemas/recipe-io-row.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://neon-sprawl.local/schemas/recipe-io-row.json", + "title": "RecipeIoRow", + "description": "Single input or output row on a RecipeDef (content/recipes/*_recipes.json).", + "type": "object", + "additionalProperties": false, + "required": ["itemId", "quantity"], + "properties": { + "itemId": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Item def id from content/items (prototype Slice 3: frozen six-item catalog)." + }, + "quantity": { + "type": "integer", + "minimum": 1, + "description": "Units consumed (input) or granted (output) per craft execution." + } + } +} diff --git a/docs/decomposition/README.md b/docs/decomposition/README.md index e8bd4ec..e8a3a7d 100644 --- a/docs/decomposition/README.md +++ b/docs/decomposition/README.md @@ -20,6 +20,19 @@ This workspace contains detailed planning artifacts derived from the finalized m 3. Use stable IDs (`E1`, `E1.M1`, etc.) for core game epics; use **`CT.M1`**-style IDs for [content tooling](tooling/internal_authoring.md) so tooling stays out of the E1–E9 sequence. 4. Every new slice must reference telemetry and validation where applicable. +## Full-stack epics + +**Epics deliver playable game features**, not server-only vertical slices. Enforced by **[`.cursor/rules/full-stack-epic-decomposition.md`](../../.cursor/rules/full-stack-epic-decomposition.md)** (`alwaysApply`). + +When decomposing a prototype backlog: + +1. Start from **[`docs/plans/_prototype-backlog-template.md`](../plans/_prototype-backlog-template.md)**. +2. **Pair server and client stories** where the feature is player-visible (mirror Epic 1: NEO-23 + NEO-24). +3. **Create all Linear issues and `blockedBy` links in the same decomposition pass** — including client slices — not as undocumented “optional follow-ups.” +4. **Bruno / manual QA** prove contracts; **Godot** proves the prototype fun/readability gates in [vision](../../neon_sprawl_vision.plan.md). + +Epic 3 established a dedicated client slice file: [E3S5-client-prototype-backlog.md](../plans/E3S5-client-prototype-backlog.md) (NEO-72–NEO-75). Apply the same pattern to other epics (e.g. planned E2 client slice for skill/perk HUD). + ## Linear alignment Engineering work is tracked as **issues** in **Linear** (team **NEO**), grouped under **projects** that mirror epics and modules. **Do not** recreate a Jira-style **Feature** issue tier—module- or slice-level grouping is **labels on the issue** only. @@ -50,6 +63,6 @@ Template for new epics or major revisions: [epics/_epic_template.md](epics/_epic ## Next Actions -1. Expand slices into **Linear issues** under epic/module **projects**; tag **module / slice** scope with **labels** (not a duplicate Feature tier). Keep module IDs aligned with the master plan. +1. Expand slices into **Linear issues** under epic/module **projects** when writing a **prototype backlog** — copy **[`_prototype-backlog-template.md`](../plans/_prototype-backlog-template.md)**; follow **[full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md)**. **Create all issues and `blockedBy` links in the same decomposition pass**, not at story kickoff. Set **estimate 1…N** on each issue to match slug execution order so the project board can sort by **Estimate** (ascending). Tag **`server`** / **`client`** and module scope with **labels**. Keep module IDs aligned with the master plan. 2. Populate `modules/module_dependency_register.md` as contracts stabilize. 3. Refine `milestones/phase_execution_plan.md` with dates and owners when execution starts. diff --git a/docs/decomposition/epics/_epic_template.md b/docs/decomposition/epics/_epic_template.md index d8f0f3a..c5738df 100644 --- a/docs/decomposition/epics/_epic_template.md +++ b/docs/decomposition/epics/_epic_template.md @@ -5,6 +5,11 @@ - Master epic reference: `neon_sprawl_vision.plan.md` - Related modules: `` +## Ownership and success (Level 1) + +- **Ownership focus:** +- **Success criteria:** — **playable in the client** for prototype slices, not server/Bruno-only proof. + ## Objective @@ -20,25 +25,29 @@ ## Implementation Slices (Backlog-Ready) + + + ### Slice 1 - -- Scope: +- Scope (server / content): +- Scope (client): - Dependencies: - Acceptance criteria: - - - - -- Telemetry hooks: - - + - Server: + - Client (Godot): +- Telemetry hooks: +- **Linear backlog:** `docs/plans/…` — create **server + client** issues in one pass; label client issues **`client`**. ### Slice 2 - -- Scope: -- Dependencies: +- Scope (server / content): … +- Scope (client): … +- Dependencies: … - Acceptance criteria: - - - - -- Telemetry hooks: - - + - Server: … + - Client (Godot): … +- Telemetry hooks: … ## Risks and Mitigations @@ -47,7 +56,8 @@ ## Definition of Done -- Functional behavior validated. +- Prototype slices meet pass/fail gates **in the Godot client**, not Bruno-only ([vision — Prototype Scope](../../../neon_sprawl_vision.plan.md)). +- **Server + client** Linear backlogs exist for every player-visible slice ([full-stack decomposition rule](../../../.cursor/rules/full-stack-epic-decomposition.md)). - Dependencies documented and stable. - Telemetry events emitted and visible. - No blocker defects for target phase. diff --git a/docs/decomposition/epics/epic_03_crafting_economy.md b/docs/decomposition/epics/epic_03_crafting_economy.md index 04252a9..9729ca0 100644 --- a/docs/decomposition/epics/epic_03_crafting_economy.md +++ b/docs/decomposition/epics/epic_03_crafting_economy.md @@ -7,8 +7,8 @@ ## Ownership and success (Level 1) -- **Ownership focus:** Economy Design + Content Engineering -- **Success criteria:** Crafting is a primary progression lane with healthy supply-demand loops and consistent item relevance. +- **Ownership focus:** Economy Design + Content Engineering + **Client presentation (Godot)** +- **Success criteria:** Crafting is a primary progression lane with healthy supply-demand loops and consistent item relevance — **playable in the client**, not server/Bruno-only proof. ## Objective @@ -82,6 +82,20 @@ Implement gathering from world nodes, item/inventory representation, recipe exec - Player completes gather to refine to usable item used in combat or quest. - Telemetry hooks: `item_crafted`, `craft_failed` with reason, time-to-first-craft. + + +### Slice 5 - Client economy integration (prototype) + +- Scope: Godot clients + HUD for **E3.M3** inventory, **E3.M1** gather feedback, **E3.M2** craft UI; playable **gather → refine → make** session in the client (vision prototype gates). +- Dependencies: Slice 1–3 server APIs landed ([NEO-55](https://linear.app/neon-sprawl/issue/NEO-55), [NEO-63](https://linear.app/neon-sprawl/issue/NEO-63), [NEO-70](https://linear.app/neon-sprawl/issue/NEO-70)); [E2.M2](https://linear.app/neon-sprawl/issue/NEO-37) skill-progression GET for XP visibility. +- Acceptance criteria: + - Inventory HUD reflects server state after gather and craft. + - Player completes **gather → refine → usable item** in Godot without Bruno. + - **Salvage** / **refine** skill progression visible after gather/craft (Epic 2 Slice 3 integration AC). +- Telemetry hooks: reuse Slice 1–3 server hook sites; client dev **`print`** / HUD feedback only until E9.M1 ingest. + +**Linear backlog:** [E3S5-client-prototype-backlog.md](../../plans/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 - Sinks and economy policy (pre-production) @@ -101,5 +115,6 @@ Implement gathering from world nodes, item/inventory representation, recipe exec ## Definition of Done -- Prototype gather-craft loop meets plan pass/fail gates. -- Pre-production adds sinks and policy with dashboard visibility. +- Prototype gather-craft loop meets plan pass/fail gates **in the Godot client** (Slice 5), not Bruno-only. +- Slices 1–3 + Slice 5 together satisfy [vision prototype in-scope features](../../../neon_sprawl_vision.plan.md) for crafting loop and progression visibility. +- Pre-production adds sinks and policy with dashboard visibility (Slice 4). diff --git a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md index 5848fe0..4aeeaf0 100644 --- a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md +++ b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md @@ -47,7 +47,9 @@ Content tooling **Slice 1** — schemas + CI; **Slice 4** — server parity hard **Item catalogs ([NEO-50](https://linear.app/neon-sprawl/issue/NEO-50)):** the same script validates each row in `content/items/*_items.json` against [`content/schemas/item-def.schema.json`](../../../content/schemas/item-def.schema.json), rejects **duplicate `id`** across files, and (Slice 1) enforces the **frozen six-item** id set plus **exactly one row per `prototypeRole`** (`material` through `equip_stub`). -**Resource node catalogs ([NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)):** the same script validates `content/resource-nodes/*_resource_nodes.json` rows against [`content/schemas/resource-node-def.schema.json`](../../../content/schemas/resource-node-def.schema.json) and `*_resource_yields.json` rows against [`content/schemas/resource-yield-row.schema.json`](../../../content/schemas/resource-yield-row.schema.json); rejects **duplicate `nodeDefId`**; (Slice 2) enforces the **frozen four-node** id set, **one row per `gatherLens`**, exactly **one yield row per node**, yield **`itemId`** cross-check against item catalogs, and **only `scrap_metal_bulk`** as yield item in prototype. Extend the script when additional catalogs and schemas ship. +**Resource node catalogs ([NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)):** the same script validates `content/resource-nodes/*_resource_nodes.json` rows against [`content/schemas/resource-node-def.schema.json`](../../../content/schemas/resource-node-def.schema.json) and `*_resource_yields.json` rows against [`content/schemas/resource-yield-row.schema.json`](../../../content/schemas/resource-yield-row.schema.json); rejects **duplicate `nodeDefId`**; (Slice 2) enforces the **frozen four-node** id set, **one row per `gatherLens`**, exactly **one yield row per node**, yield **`itemId`** cross-check against item catalogs, and **only `scrap_metal_bulk`** as yield item in prototype. + +**Recipe catalogs ([NEO-65](https://linear.app/neon-sprawl/issue/NEO-65)):** the same script validates each row in `content/recipes/*_recipes.json` against [`content/schemas/recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) (I/O rows via [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json)), rejects **duplicate `id`** across files, cross-checks every input/output **`itemId`** against item catalogs and every **`requiredSkillId`** against skill catalogs, and (Slice 3) enforces the **frozen eight-recipe** id set, at least one **`process`** and one **`make`** row, and **`requiredSkillId: refine`** on every row. Extend the script when additional catalogs and schemas ship. **Decomposition vocabulary:** the same workflow runs [`scripts/check_decomposition_language.py`](../../../scripts/check_decomposition_language.py) (stdlib only) over `docs/decomposition/**/*.md` to catch wording that treats **combat** as a leveled **`SkillDef`** line instead of **gig** progression (see script docstring for patterns). Adjust the script if a future doc needs a documented exception. diff --git a/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md b/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md index 5c27c9c..17d7760 100644 --- a/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md +++ b/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md @@ -102,7 +102,7 @@ Epic 3 **Slice 2** — [gather nodes and outputs](../epics/epic_03_crafting_econ **Resource node instance depletion store (NEO-61):** **`IResourceNodeInstanceStore`** + **`ResourceNodeInstanceOperations`** — lazy-init remaining gathers from catalog **`maxGathers`**, atomic decrement, **`node_depleted`** / **`unknown_node`** reason codes; Postgres **`V006`** or in-memory fallback. Plan: [NEO-61 implementation plan](../../plans/NEO-61-implementation-plan.md). -**Linear backlog (decomposed):** [E3M1-prototype-backlog.md](../../plans/E3M1-prototype-backlog.md) — **E3M1-01** [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57) through **E3M1-08** [NEO-64](https://linear.app/neon-sprawl/issue/NEO-64). +**Linear backlog (decomposed):** [E3M1-prototype-backlog.md](../../plans/E3M1-prototype-backlog.md) — **E3M1-01** [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57) through **E3M1-08** [NEO-64](https://linear.app/neon-sprawl/issue/NEO-64). **Client (Slice 5):** gather feedback — [E3S5-client-prototype-backlog.md](../../plans/E3S5-client-prototype-backlog.md) **E3S5-02** [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73). ## Source anchors diff --git a/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md b/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md index fb88b73..668acf0 100644 --- a/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md +++ b/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md @@ -7,12 +7,14 @@ | **Module ID** | E3.M2 | | **Epic** | [Epic 3 — Crafting Economy](../epics/epic_03_crafting_economy.md) | | **Stage target** | Prototype | -| **Status** | Planned (see [dependency register](module_dependency_register.md)) | +| **Status** | In Progress — Slice 3 backlog [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md): E3M2-01 [NEO-65](https://linear.app/neon-sprawl/issue/NEO-65) landed; E3M2-02–E3M2-07 open (see [dependency register](module_dependency_register.md)) | ## Implementation snapshot **Prep (NEO-42):** Server ships **`RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine`** + **`RefineSkillXpConstants`** under `server/NeonSprawl.Server/Game/Skills/` — delegates to **`SkillProgressionGrantOperations.TryApplyGrant`** with **`refine`** + **`activity`** (prototype **10** XP), same persistence and **`allowedXpSourceKinds`** rules as **`POST …/skill-progression`**. **No** `CraftRequest` / **`CraftResult`** route yet; when this module adds authoritative craft/refine success, call that helper once on success ([NEO-42 implementation plan](../../plans/NEO-42-implementation-plan.md); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42)). +**E3M2-01 (NEO-65):** Frozen eight-recipe catalog in [`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); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65 plan](../../plans/NEO-65-implementation-plan.md)). **No** server loader yet — E3M2-02 (NEO-66) owns fail-fast boot load. + ## Purpose Implements the processing and crafting pipeline: raw-to-refined flows and recipe execution with server-authoritative inventory mutations. Bridges gathered materials ([E3.M1](E3_M1_ResourceNodeAndGatherLoop.md)) and item definitions ([E3.M3](E3_M3_ItemizationAndInventorySchema.md)) into usable outputs for combat, quests, and economy loops. @@ -42,9 +44,39 @@ Implements the processing and crafting pipeline: raw-to-refined flows and recipe - **E6.M4** — [RewardParityEnforcer](E6_M4_RewardParityEnforcer.md): PvP ↔ PvE/crafting equivalents involving craft outputs. - **Epic 2 integration** — Callers award crafting XP via [E2.M2](E2_M2_XpAwardAndLevelEngine.md) at activity sites. +## Designer note: prototype recipe shape + +Each **`RecipeDef`** row under `content/recipes/*.json` declares: + +- **`recipeKind`** — **`process`** (refine / intermediate) or **`make`** (finished good). +- **`requiredSkillId`** — prototype uses **`refine`** for all eight rows (no separate Make skill in the skill trio). +- **`inputs`** / **`outputs`** — arrays of `{ itemId, quantity }` referencing only frozen [E3.M3](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) item ids. +- **`requiredSkillLevel`** — **omitted** in Slice 3 (no level gates until a deliberate content pass). + +**Prototype v1:** no station tags, craft failure RNG, or quality rolls — deterministic success when inputs and bag/equipment capacity allow. + +## Prototype Slice 3 freeze (E3M2-01) + +The **first shipped eight-recipe spine** under `content/recipes/*.json` is **frozen** for downstream references (craft HTTP, Bruno spine, quest objectives) until a deliberate migration or follow-up issue expands the roster. + +| `RecipeDef.id` | `recipeKind` | Primary inputs → output | Demo role | +|----------------|--------------|-------------------------|-----------| +| **`refine_scrap_standard`** | `process` | 5× **`scrap_metal_bulk`** → 1× **`refined_plate_stock`** | Default refine after gather | +| **`refine_scrap_efficient`** | `process` | 10× **`scrap_metal_bulk`** → 2× **`refined_plate_stock`** | Branching refine efficiency | +| **`make_field_stim_mk0`** | `make` | 2× **`refined_plate_stock`** + 1× **`scrap_metal_bulk`** → 1× **`field_stim_mk0`** | Primary gather→refine→combat consumable | +| **`make_field_stim_batch`** | `make` | 5× **`refined_plate_stock`** + 3× **`scrap_metal_bulk`** → 3× **`field_stim_mk0`** | Batch make variant | +| **`make_prototype_armor`** | `make` | 8× **`refined_plate_stock`** + 5× **`scrap_metal_bulk`** → 1× **`prototype_armor_shell`** | Equip-stub output | +| **`make_armor_quick`** | `make` | 4× **`refined_plate_stock`** + 3× **`scrap_metal_bulk`** → 1× **`prototype_armor_shell`** | Alternate armor path (branching) | +| **`make_survey_drone_kit`** | `make` | 3× **`refined_plate_stock`** + 2× **`scrap_metal_bulk`** → 1× **`survey_drone_kit`** | Utility output | +| **`make_contract_token`** | `make` | 2× **`refined_plate_stock`** + 1× **`scrap_metal_bulk`** → 1× **`contract_handoff_token`** | Quest token output | + +**Rules:** Do **not** rename these `id` values—change **`displayName`** only, or add a new `id` in a follow-up issue. CI ([`scripts/validate_content.py`](../../../scripts/validate_content.py)) enforces **exactly** these eight ids, at least one **`process`** and one **`make`** row, **`requiredSkillId: refine`** on every row, input/output **`itemId`** cross-check against the frozen item catalog, and duplicate-`id` rejection across all recipe JSON files. Implementation: [NEO-65 plan](../../plans/NEO-65-implementation-plan.md). + ## Related implementation slices -See Epic 3 **Slice 3 — Recipes and crafting pipeline**: gather → refine → item used in combat or quest; telemetry `item_crafted`, `craft_failed`, time-to-first-craft. +Epic 3 **Slice 3** — gather → refine → item used in combat or quest; telemetry `item_crafted`, `craft_failed`, time-to-first-craft. + +**Linear backlog (decomposed):** [E3M2-prototype-backlog.md](../../plans/E3M2-prototype-backlog.md) — **E3M2-01** [NEO-65](https://linear.app/neon-sprawl/issue/NEO-65) (content + CI) through **E3M2-07** [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71) (telemetry hooks). **Client (Slice 5):** craft UI — [E3S5-client-prototype-backlog.md](../../plans/E3S5-client-prototype-backlog.md) **E3S5-03** [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74). ## Risks and telemetry diff --git a/docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md b/docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md index 2665878..b823d21 100644 --- a/docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md +++ b/docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md @@ -74,7 +74,7 @@ Epic 3 **Slice 1** — MVP inventory; `item_created`, transfer failures. **Inventory telemetry hooks (NEO-56):** comment-only **`item_created`** (successful add) and **`inventory_transfer_denied`** + **`reasonCode`** anchors in **`PlayerInventoryOperations`**; [server README — Player inventory (NEO-56)](../../../server/README.md#player-inventory-neo-54-store-neo-55-http). Plan: [NEO-56 implementation plan](../../plans/NEO-56-implementation-plan.md); manual QA [`NEO-56`](../../manual-qa/NEO-56.md). -**Linear backlog (decomposed):** [E3M3-prototype-backlog.md](../../plans/E3M3-prototype-backlog.md) — **E3M3-01** [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) (content + CI) through **E3M3-07** [NEO-56](https://linear.app/neon-sprawl/issue/NEO-56) (telemetry hooks). +**Linear backlog (decomposed):** [E3M3-prototype-backlog.md](../../plans/E3M3-prototype-backlog.md) — **E3M3-01** [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) (content + CI) through **E3M3-07** [NEO-56](https://linear.app/neon-sprawl/issue/NEO-56) (telemetry hooks). **Client (Slice 5):** inventory HUD — [E3S5-client-prototype-backlog.md](../../plans/E3S5-client-prototype-backlog.md) **E3S5-01** [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72). ## Source anchors diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index 1fc6e3e..ce80e62 100644 --- a/docs/decomposition/modules/documentation_and_implementation_alignment.md +++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md @@ -53,8 +53,9 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not | E2.M1 | In Progress | **NEO-33 landed:** frozen prototype trio **`salvage`** / **`refine`** / **`intrusion`** in [`content/skills/prototype_skills.json`](../../../content/skills/prototype_skills.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) enforce schema, duplicate `id`, exact trio ids, and category coverage; designer note on **`allowedXpSourceKinds`** in [E2_M1](E2_M1_SkillDefinitionRegistry.md) + [`content/README.md`](../../../content/README.md). **NEO-34 landed:** fail-fast server load of `content/skills/*.json` at startup — `server/NeonSprawl.Server/Game/Skills/` ([NEO-34](../../plans/NEO-34-implementation-plan.md)); config + discovery in [server README — Skill catalog](../../../server/README.md#skill-catalog-contentskills-neo-34). **NEO-35 landed:** `ISkillDefinitionRegistry` / `SkillDefinitionRegistry` + DI ([NEO-35](../../plans/NEO-35-implementation-plan.md)). **NEO-36 landed:** versioned **`GET /game/world/skill-definitions`** ([NEO-36](../../plans/NEO-36-implementation-plan.md)) — `SkillDefinitionsWorldApi` + `SkillDefinitionsListDtos` in `server/NeonSprawl.Server/Game/Skills/`; Bruno `bruno/neon-sprawl-server/skill-definitions/`; manual QA [`NEO-36`](../../manual-qa/NEO-36.md). **E2.M2 consumer (NEO-38 landed):** grant path validates `skillId` + `sourceKind` vs catalog **`allowedXpSourceKinds`** on **`POST …/skill-progression`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); see [server README — Skill progression grant](../../../server/README.md#skill-progression-grant-neo-38) and [E2_M2](E2_M2_XpAwardAndLevelEngine.md). | [NEO-33](../../plans/NEO-33-implementation-plan.md), [NEO-34](../../plans/NEO-34-implementation-plan.md), [NEO-35](../../plans/NEO-35-implementation-plan.md), [NEO-36](../../plans/NEO-36-implementation-plan.md); [E2_M1](E2_M1_SkillDefinitionRegistry.md); [module_dependency_register.md](module_dependency_register.md) **E2.M1 note**; `server/NeonSprawl.Server/Game/Skills/`; [`docs/manual-qa/NEO-36.md`](../../manual-qa/NEO-36.md); [server README — Skill definitions (NEO-36)](../../../server/README.md#skill-definitions-neo-36) | | E2.M3 | In Progress | **NEO-45 landed:** prototype **`salvage`** mastery catalog + CI gates (see [NEO-45 plan](../../plans/NEO-45-implementation-plan.md)). **NEO-46 landed:** fail-fast server load under `server/NeonSprawl.Server/Game/Mastery/` — `MasteryCatalogLoader`, `IMasteryCatalogRegistry`, cross-check vs `ISkillDefinitionRegistry`, Slice 4 + **`tierIndex`** 1..N gate; see [NEO-46 plan](../../plans/NEO-46-implementation-plan.md). **NEO-47 landed:** `PerkUnlockEngine`, `IPlayerPerkStateStore` + **`V004`**, level-up hook in skill XP grants; see [NEO-47 plan](../../plans/NEO-47-implementation-plan.md). **NEO-49 landed:** comment-only **`perk_unlock`** telemetry hook site in [`PerkUnlockEngine.TryUnlockPerks`](../../../server/NeonSprawl.Server/Game/Mastery/PerkUnlockEngine.cs) ([NEO-49 plan](../../plans/NEO-49-implementation-plan.md), [`NEO-49` manual QA](../../manual-qa/NEO-49.md)); [server README — Perk unlock telemetry (NEO-49)](../../../server/README.md#perk-unlock-engine-and-telemetry-hooks-neo-47-neo-49). **NEO-48 landed:** **`GET`/`POST /game/players/{id}/perk-state`** — `PerkStateApi` + DTOs in `Game/Mastery/` ([NEO-48](../../plans/NEO-48-implementation-plan.md), [`NEO-48` manual QA](../../manual-qa/NEO-48.md)); [server README — Perk state (NEO-48)](../../../server/README.md#perk-state-neo-48); Bruno `bruno/neon-sprawl-server/perk-state/`. | [NEO-45](../../plans/NEO-45-implementation-plan.md), [NEO-46](../../plans/NEO-46-implementation-plan.md), [NEO-47](../../plans/NEO-47-implementation-plan.md), [NEO-48](../../plans/NEO-48-implementation-plan.md), [NEO-49](../../plans/NEO-49-implementation-plan.md), [E2M3-pre-production-backlog](../../plans/E2M3-pre-production-backlog.md), [E2_M3](E2_M3_MasteryAndPerkUnlocks.md) | | E2.M2 | In Progress | **NEO-37 landed:** versioned **`GET /game/players/{id}/skill-progression`** ([NEO-37](../../plans/NEO-37-implementation-plan.md)) — read model for every registered skill; known-player gate via `IPositionStateStore`; [server README — Skill progression snapshot (NEO-37)](../../../server/README.md#skill-progression-snapshot-neo-37); manual QA [`NEO-37`](../../manual-qa/NEO-37.md). **NEO-38 landed:** **`POST`** same path — grant apply, persistence (`IPlayerSkillProgressionStore`, `V003` migration), structured denies + **`levelUps`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); manual QA [`NEO-38`](../../manual-qa/NEO-38.md); Bruno `bruno/neon-sprawl-server/skill-progression/`; [server README — Skill progression grant (NEO-38)](../../../server/README.md#skill-progression-grant-neo-38). **NEO-39 landed:** data-driven `LevelCurve` content + schema + CI validation (`*_level_curve.json`) with fail-fast startup schema checks; progression GET/POST level resolution now uses `ISkillLevelCurve` content-backed thresholds ([NEO-39](../../plans/NEO-39-implementation-plan.md)); manual QA [`NEO-39`](../../manual-qa/NEO-39.md). **NEO-40 landed:** comment-only telemetry hook sites in [`SkillProgressionSnapshotApi.cs`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs) on **`POST …/skill-progression`** for future **`xp_grant`** / **`level_up`** ([NEO-40](../../plans/NEO-40-implementation-plan.md), [`NEO-40` manual QA](../../manual-qa/NEO-40.md)). **NEO-41 landed:** gather prototype — **`POST …/interact`** with **`kind: resource_node`** grants **`salvage`** + **`activity`** (10 XP) via [`SkillProgressionGrantOperations`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantOperations.cs) ([NEO-41](../../plans/NEO-41-implementation-plan.md), [`NEO-41` manual QA](../../manual-qa/NEO-41.md)); [server README — Interaction](../../../server/README.md#interaction-neo-9). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** / **`RefineSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack; **E3.M2** must invoke on craft/refine success ([NEO-42](../../plans/NEO-42-implementation-plan.md), [`NEO-42` manual QA](../../manual-qa/NEO-42.md)); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42). **NEO-43 landed (prep):** **`MissionRewardSkillXpGrant`** / **`MissionRewardSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack with fixed **`sourceKind: mission_reward`**; **E7.M2** must invoke from quest hand-in ([NEO-43](../../plans/NEO-43-implementation-plan.md), [`NEO-43` manual QA](../../manual-qa/NEO-43.md)); [server README — Mission / quest reward (NEO-43)](../../../server/README.md#mission--quest-reward--skill-xp-neo-43). **Slice 3 still open:** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). See [epic_02 — E2.M2 + Slice 3](../epics/epic_02_skills_and_progression.md). | [NEO-37](../../plans/NEO-37-implementation-plan.md), [NEO-38](../../plans/NEO-38-implementation-plan.md), [NEO-39](../../plans/NEO-39-implementation-plan.md), [NEO-40](../../plans/NEO-40-implementation-plan.md), [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-43](../../plans/NEO-43-implementation-plan.md), [E2_M2](E2_M2_XpAwardAndLevelEngine.md); label **`E2.M2`** on NEO-37–NEO-41, NEO-42–NEO-44 | -| E3.M1 | Ready | **NEO-41 landed (prototype, superseded on interact by NEO-63):** inline XP on **`resource_node`** interact. **NEO-57–NEO-61** — content, registry, HTTP defs, depletion store (see prior alignment). **NEO-62 landed:** **`GatherOperations`** + **`GatherResult`**. **NEO-63 landed:** **`POST …/interact`** **`resource_node`** → gather engine; **four** **`PrototypeInteractableRegistry`** anchors; Bruno gather → inventory + depletion deny ([NEO-63](../../plans/NEO-63-implementation-plan.md), [`NEO-63` manual QA](../../manual-qa/NEO-63.md)); [server README — Gather via interact (NEO-63)](../../../server/README.md#gather-via-interact-neo-63). **NEO-64 landed:** comment-only **`resource_gathered`** / **`gather_node_depleted`** hook sites in **`GatherOperations.TryGather`** ([NEO-64](../../plans/NEO-64-implementation-plan.md), [`NEO-64` manual QA](../../manual-qa/NEO-64.md)); Epic 3 Slice 2 backlog **E3M1-01**–**E3M1-08** complete. | [NEO-41](../../plans/NEO-41-implementation-plan.md) … [NEO-64](../../plans/NEO-64-implementation-plan.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Gathering/`, `Game/Interaction/` | -| E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **NEO-53 landed:** **`GET /game/world/item-definitions`** — `ItemDefinitionsWorldApi` + DTOs in `Game/Items/` ([NEO-53](../../plans/NEO-53-implementation-plan.md), [`NEO-53` manual QA](../../manual-qa/NEO-53.md)); [server README — Item definitions (NEO-53)](../../../server/README.md#item-definitions-neo-53); Bruno `bruno/neon-sprawl-server/item-definitions/`. **NEO-54 landed:** **`IPlayerInventoryStore`** + **`PlayerInventoryOperations`** — 24 bag + 1 equipment slots, stack rules, `V005` migration ([NEO-54](../../plans/NEO-54-implementation-plan.md)). **NEO-55 landed:** **`GET`/`POST /game/players/{id}/inventory`** — `PlayerInventoryApi` + DTOs in `Game/Items/` ([NEO-55](../../plans/NEO-55-implementation-plan.md)); [server README — Player inventory (NEO-54 store, NEO-55 HTTP)](../../../server/README.md#player-inventory-neo-54-store-neo-55-http); Bruno `bruno/neon-sprawl-server/inventory/`. **NEO-56 landed:** comment-only **`item_created`** / **`inventory_transfer_denied`** telemetry hook sites in [`PlayerInventoryOperations`](../../../server/NeonSprawl.Server/Game/Items/PlayerInventoryOperations.cs) ([NEO-56](../../plans/NEO-56-implementation-plan.md), [`NEO-56` manual QA](../../manual-qa/NEO-56.md)); no E9.M1 ingest. | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [NEO-53](../../plans/NEO-53-implementation-plan.md), [NEO-54](../../plans/NEO-54-implementation-plan.md), [NEO-55](../../plans/NEO-55-implementation-plan.md), [NEO-56](../../plans/NEO-56-implementation-plan.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) | +| E3.M1 | Ready | **NEO-41 landed (prototype, superseded on interact by NEO-63):** inline XP on **`resource_node`** interact. **NEO-57–NEO-61** — content, registry, HTTP defs, depletion store (see prior alignment). **NEO-62 landed:** **`GatherOperations`** + **`GatherResult`**. **NEO-63 landed:** **`POST …/interact`** **`resource_node`** → gather engine; **four** **`PrototypeInteractableRegistry`** anchors; Bruno gather → inventory + depletion deny ([NEO-63](../../plans/NEO-63-implementation-plan.md), [`NEO-63` manual QA](../../manual-qa/NEO-63.md)); [server README — Gather via interact (NEO-63)](../../../server/README.md#gather-via-interact-neo-63). **NEO-64 landed:** comment-only **`resource_gathered`** / **`gather_node_depleted`** hook sites in **`GatherOperations.TryGather`** ([NEO-64](../../plans/NEO-64-implementation-plan.md), [`NEO-64` manual QA](../../manual-qa/NEO-64.md)); Epic 3 Slice 2 backlog **E3M1-01**–**E3M1-08** complete. **Slice 5 client (planned):** gather feedback HUD [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md) … [NEO-64](../../plans/NEO-64-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Gathering/`, `Game/Interaction/` | +| E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **NEO-53 landed:** **`GET /game/world/item-definitions`** — `ItemDefinitionsWorldApi` + DTOs in `Game/Items/` ([NEO-53](../../plans/NEO-53-implementation-plan.md), [`NEO-53` manual QA](../../manual-qa/NEO-53.md)); [server README — Item definitions (NEO-53)](../../../server/README.md#item-definitions-neo-53); Bruno `bruno/neon-sprawl-server/item-definitions/`. **NEO-54 landed:** **`IPlayerInventoryStore`** + **`PlayerInventoryOperations`** — 24 bag + 1 equipment slots, stack rules, `V005` migration ([NEO-54](../../plans/NEO-54-implementation-plan.md)). **NEO-55 landed:** **`GET`/`POST /game/players/{id}/inventory`** — `PlayerInventoryApi` + DTOs in `Game/Items/` ([NEO-55](../../plans/NEO-55-implementation-plan.md)); [server README — Player inventory (NEO-54 store, NEO-55 HTTP)](../../../server/README.md#player-inventory-neo-54-store-neo-55-http); Bruno `bruno/neon-sprawl-server/inventory/`. **NEO-56 landed:** comment-only **`item_created`** / **`inventory_transfer_denied`** telemetry hook sites in [`PlayerInventoryOperations`](../../../server/NeonSprawl.Server/Game/Items/PlayerInventoryOperations.cs) ([NEO-56](../../plans/NEO-56-implementation-plan.md), [`NEO-56` manual QA](../../manual-qa/NEO-56.md)); no E9.M1 ingest. **Slice 5 client (planned):** inventory HUD [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [NEO-53](../../plans/NEO-53-implementation-plan.md), [NEO-54](../../plans/NEO-54-implementation-plan.md), [NEO-55](../../plans/NEO-55-implementation-plan.md), [NEO-56](../../plans/NEO-56-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) | +| E3.M2 | In Progress | **NEO-65 landed:** frozen prototype eight-recipe catalog in [`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); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65](../../plans/NEO-65-implementation-plan.md)). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** — craft/refine success should award **`refine`** XP via shared NEO-38 grant path; no craft route yet. **Slice 3 backlog in Linear:** [NEO-66](https://linear.app/neon-sprawl/issue/NEO-66) → [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71); [E3M2-prototype-backlog.md](../../plans/E3M2-prototype-backlog.md) — **E3M2-02**–**E3M2-07** open. Upstream: E3.M1 **Ready**, E3.M3 inventory landed. **Slice 5 client (planned):** craft UI [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74); capstone playable loop [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-65](../../plans/NEO-65-implementation-plan.md), [NEO-66](https://linear.app/neon-sprawl/issue/NEO-66) … [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75), [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M2](E3_M2_RefinementAndRecipeExecution.md) | --- diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index 1977c75..11f28d0 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -52,6 +52,10 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen **E3.M3 note:** Epic 3 **Slice 1** backlog in Linear ([Epic 3 — Crafting, Gathering, and Itemization Economy](https://linear.app/neon-sprawl/project/epic-3-crafting-gathering-and-itemization-economy-65785ed05bc2)): [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) → [NEO-56](https://linear.app/neon-sprawl/issue/NEO-56); label **`E3.M3`**. See [E3M3-prototype-backlog.md](../../plans/E3M3-prototype-backlog.md), [E3_M3_ItemizationAndInventorySchema.md](E3_M3_ItemizationAndInventorySchema.md). **NEO-50** (content + CI), **NEO-51** (server fail-fast load), **NEO-52** (`IItemDefinitionRegistry` + DI), **NEO-53** (`GET /game/world/item-definitions`), **NEO-54** (inventory store + stack/slot rules engine), **NEO-55** (`GET`/`POST /game/players/{id}/inventory`), and **NEO-56** (comment-only `item_created` / `inventory_transfer_denied` telemetry hook sites in `PlayerInventoryOperations`) — Epic 3 Slice 1 backlog **E3M3-01**–**E3M3-07** complete; register row **In Progress** until Slice 2+; later slices update the alignment table as they land. +**E3.M2 note:** Epic 3 **Slice 3** backlog in Linear ([Epic 3 — Crafting, Gathering, and Itemization Economy](https://linear.app/neon-sprawl/project/epic-3-crafting-gathering-and-itemization-economy-65785ed05bc2)): [NEO-65](https://linear.app/neon-sprawl/issue/NEO-65) → [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71); label **`E3.M2`**. See [E3M2-prototype-backlog.md](../../plans/E3M2-prototype-backlog.md), [E3_M2_RefinementAndRecipeExecution.md](E3_M2_RefinementAndRecipeExecution.md). **NEO-42** (refine XP prep helper) is **Done**; **NEO-65**–**NEO-71** created at decomposition. Upstream: E3.M1 **Ready**, E3.M3 inventory landed. + +**E3 Slice 5 (client) note:** Epic 3 **client economy integration** — [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) → [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75); labels **`client`** + module ids. See [E3S5-client-prototype-backlog.md](../../plans/E3S5-client-prototype-backlog.md), [epic_03 Slice 5](../epics/epic_03_crafting_economy.md#epic-3-slice-5). Required for prototype **playable** gather→craft loop (not Bruno-only). + ### Epic 4 — World Topology | Module ID | Module Name | Depends On | Contract Needed | Phase Required | Status | diff --git a/docs/plans/E2M3-pre-production-backlog.md b/docs/plans/E2M3-pre-production-backlog.md index b4dd546..1440829 100644 --- a/docs/plans/E2M3-pre-production-backlog.md +++ b/docs/plans/E2M3-pre-production-backlog.md @@ -117,7 +117,7 @@ Working backlog for **Epic 2 — Slice 4** ([mastery and pacing](../decompositio **Out of scope** -- Godot HUD (optional future issue). +- Godot HUD — decompose per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md) (planned **E2 client slice**; skill/perk UI not covered by E3 Slice 5 alone). - Gameplay stat application. **Acceptance criteria** diff --git a/docs/plans/E3M1-prototype-backlog.md b/docs/plans/E3M1-prototype-backlog.md index 16eda22..435524d 100644 --- a/docs/plans/E3M1-prototype-backlog.md +++ b/docs/plans/E3M1-prototype-backlog.md @@ -214,7 +214,7 @@ Working backlog for **Epic 3 — Slice 2** ([gather nodes and outputs](../decomp **Out of scope** -- Godot gather HUD (optional follow-up). +- Godot gather HUD — **[Epic 3 Slice 5](E3S5-client-prototype-backlog.md#e3s5-02--client-gather-feedback-on-interact-e3m1)** ([NEO-73](https://linear.app/neon-sprawl/issue/NEO-73)). - New gather-specific HTTP route. **Acceptance criteria** @@ -254,7 +254,8 @@ Working backlog for **Epic 3 — Slice 2** ([gather nodes and outputs](../decomp ## After this backlog -- Decompose **[E3.M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md)** Slice 3 (recipes + craft HTTP) once E3M1-06+ land and inventory holds gathered materials. +- **[E3.M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md)** Slice 3 decomposed — [E3M2-prototype-backlog.md](E3M2-prototype-backlog.md) ([NEO-65](https://linear.app/neon-sprawl/issue/NEO-65)–[NEO-71](https://linear.app/neon-sprawl/issue/NEO-71)). +- **Epic 3 Slice 5 (client)** — [E3S5-client-prototype-backlog.md](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)); gather HUD is **E3S5-02** [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73). - 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 interact + inventory surfaces are testable (E3M1-07+). diff --git a/docs/plans/E3M2-prototype-backlog.md b/docs/plans/E3M2-prototype-backlog.md new file mode 100644 index 0000000..9c8ff47 --- /dev/null +++ b/docs/plans/E3M2-prototype-backlog.md @@ -0,0 +1,244 @@ +# 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** + +- [ ] Server refuses boot when recipe catalog invalid (mirror E3.M1 / E3.M3 catalog behavior). +- [ ] Catalog resolves `RecipeDef` and I/O rows by recipe `id`. + +--- + +### 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** + +- [ ] Host resolves registry from DI; unknown `recipeId` distinguishable from valid ids in tests. + +--- + +### 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** + +- [ ] GET returns all eight prototype recipes with schema version field and input/output summary fields needed for QA. +- [ ] Bruno happy path documented. + +--- + +### 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** + +- [ ] Successful craft removes configured inputs and adds outputs deterministically. +- [ ] Insufficient materials returns stable `reasonCode` with **no** inventory change. +- [ ] Full bag on output returns stable `reasonCode`; inputs are **not** consumed (or are rolled back). +- [ ] **`refine`** skill XP increases by **10** on success (NEO-42). +- [ ] **`make_prototype_armor`** respects equipment slot rules from E3.M3 when bag is full but equip slot empty. + +--- + +### 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** + +- [ ] POST success returns structured **`CraftResult`** (or HTTP envelope) matching engine outcome. +- [ ] Bruno spine completes without manual inventory POST shortcuts (gather interact allowed for scrap). +- [ ] At least one deny path documented per major `reasonCode`. +- [ ] Epic 3 Slice 3 acceptance: player completes **gather → refine → usable item** (`field_stim_mk0` consumable). + +--- + +### 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** + +- [ ] Hook sites documented in code and `server/README.md`. +- [ ] Matches Epic 3 Slice 3 telemetry vocabulary in [epic_03](../decomposition/epics/epic_03_crafting_economy.md#epic-3-slice-3). + +--- + +## 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+). + +## 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) diff --git a/docs/plans/E3M3-prototype-backlog.md b/docs/plans/E3M3-prototype-backlog.md index 68b21d2..a207ff2 100644 --- a/docs/plans/E3M3-prototype-backlog.md +++ b/docs/plans/E3M3-prototype-backlog.md @@ -162,7 +162,7 @@ Working backlog for **Epic 3 — Slice 1** ([items and inventory MVP](../decompo **Out of scope** -- Godot HUD (optional follow-up). +- Godot HUD — **[Epic 3 Slice 5](E3S5-client-prototype-backlog.md#e3s5-01--client-inventory-snapshot-hud-e3m3)** ([NEO-72](https://linear.app/neon-sprawl/issue/NEO-72)). - Gather node depletion ([E3.M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md)). **Acceptance criteria** @@ -197,6 +197,8 @@ Working backlog for **Epic 3 — Slice 1** ([items and inventory MVP](../decompo ## After this backlog - **[E3.M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md)** Slice 2 decomposed — [E3M1-prototype-backlog.md](E3M1-prototype-backlog.md) ([NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)–[NEO-64](https://linear.app/neon-sprawl/issue/NEO-64)). +- **[E3.M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md)** Slice 3 decomposed — [E3M2-prototype-backlog.md](E3M2-prototype-backlog.md) ([NEO-65](https://linear.app/neon-sprawl/issue/NEO-65)–[NEO-71](https://linear.app/neon-sprawl/issue/NEO-71)). +- **Epic 3 Slice 5 (client)** — [E3S5-client-prototype-backlog.md](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)); inventory HUD is **E3S5-01** [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72). - 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 HTTP surfaces land (E3M3-06+). diff --git a/docs/plans/E3S5-client-prototype-backlog.md b/docs/plans/E3S5-client-prototype-backlog.md new file mode 100644 index 0000000..2a536ba --- /dev/null +++ b/docs/plans/E3S5-client-prototype-backlog.md @@ -0,0 +1,171 @@ +# Epic 3 — Slice 5: Client economy integration (prototype) + +Working backlog for **Epic 3 — Slice 5** ([client economy integration](../decomposition/epics/epic_03_crafting_economy.md#epic-3-slice-5)). Completes the **full stack** for gather → inventory → craft in the **Godot client** — not Bruno-only proof. + +**Policy (from decomposition):** Epics deliver **playable game features**, not server-only vertical slices. Slices 1–3 landed server + content + Bruno; **Slice 5** is the required client half. Slice 4 (E3.M4/M5 sinks) remains pre-production. + +**Labels (Linear):** every issue carries module label(s) **`E3.M1`** / **`E3.M2`** / **`E3.M3`** as listed; add **`client`** + **`Story`**. + +**Pattern:** Mirror [E1.M3 client pairing](../plans/E1M3-prototype-backlog.md#e1m3-02--client-tab-target--lock-ui-synced-to-server) — thin HTTP client nodes, `main.gd` wiring, HUD labels, GdUnit doubles, `docs/manual-qa/{KEY}.md`. + +**Upstream (server must be landed):** + +| Module | Minimum server stories | +|--------|-------------------------| +| **E3.M3** | [NEO-55](https://linear.app/neon-sprawl/issue/NEO-55) inventory GET/POST; [NEO-53](https://linear.app/neon-sprawl/issue/NEO-53) item defs (display names) | +| **E3.M1** | [NEO-63](https://linear.app/neon-sprawl/issue/NEO-63) interact → gather; [NEO-60](https://linear.app/neon-sprawl/issue/NEO-60) resource-node defs (optional preview) | +| **E3.M2** | [NEO-68](https://linear.app/neon-sprawl/issue/NEO-68) recipe defs GET; [NEO-70](https://linear.app/neon-sprawl/issue/NEO-70) craft POST | +| **E2.M2** | [NEO-37](https://linear.app/neon-sprawl/issue/NEO-37) skill-progression GET (XP visibility) | + +**Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the same branch as implementation work. + +| Slug | Linear | +|------|--------| +| E3S5-01 | [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) | +| E3S5-02 | [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) | +| E3S5-03 | [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74) | +| E3S5-04 | [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) | + +**Dependency graph in Linear:** E3S5-02 (NEO-73) blocked by E3S5-01 (NEO-72) and [NEO-63](https://linear.app/neon-sprawl/issue/NEO-63). E3S5-03 (NEO-74) blocked by E3S5-01 (NEO-72), [NEO-68](https://linear.app/neon-sprawl/issue/NEO-68), and [NEO-70](https://linear.app/neon-sprawl/issue/NEO-70). E3S5-04 (NEO-75) blocked by E3S5-02 (NEO-73) and E3S5-03 (NEO-74). + +**Board order:** estimates **1–4** matching slug order. On the Epic 3 project board, sort by **Estimate** (ascending). + +--- + +## Story order (recommended) + +| Order | Slug | Depends on | +|-------|------|------------| +| 1 | **E3S5-01** | [NEO-55](https://linear.app/neon-sprawl/issue/NEO-55), [NEO-53](https://linear.app/neon-sprawl/issue/NEO-53) | +| 2 | **E3S5-02** | E3S5-01, [NEO-63](https://linear.app/neon-sprawl/issue/NEO-63) | +| 3 | **E3S5-03** | E3S5-01, [NEO-68](https://linear.app/neon-sprawl/issue/NEO-68), [NEO-70](https://linear.app/neon-sprawl/issue/NEO-70) | +| 4 | **E3S5-04** | E3S5-02, E3S5-03 | + +--- + +## Kickoff decisions (decomposition defaults) + +| Topic | Decision | Rationale | +|-------|----------|-----------| +| HUD fidelity | **Prototype debug panels** (Labels / simple lists) — not final art | Matches NEO-24/NEO-28 cast/target HUD pattern; polish is pre-production | +| Inventory refresh | **GET after mutating actions** (interact gather, craft POST) | Server is source of truth; no client-side stack math | +| Craft UI scope | **List all prototype recipes** + single **Craft** button per row | No station placement, batch quantity slider, or drag-drop in Slice 5 | +| Skill XP display | **Salvage + refine levels/XP** on HUD; refresh after gather/craft | Epic 2 Slice 3 AC + vision **progression gate** | +| Gather wire | **Reuse `POST …/interact`** — no new gather HTTP on client | E3.M1 prototype contract; extend `interaction_request_client.gd` | +| Authority | Client **displays only**; all mutations via existing server routes | [client_server_authority.md](../decomposition/modules/client_server_authority.md) | + +--- + +### E3S5-01 — Client inventory snapshot HUD (E3.M3) + +**Goal:** Godot **`GET /game/players/{id}/inventory`** client + readable bag/equipment panel; boot hydrate + manual refresh entrypoint for later stories. + +**In scope** + +- `inventory_client.gd` (or equivalent): GET snapshot, parse v1 envelope, emit signal with slots/stacks. +- HUD: `InventoryLabel` or scroll-friendly list under `UICanvas` showing **bag** + **equipment** rows (item id, quantity, slot kind). +- Optional: resolve **`displayName`** via cached **`GET /game/world/item-definitions`** (NEO-53) for readable labels. +- GdUnit: HTTP double, parse happy path + error path. +- `client/README.md` section; input binding for **refresh** (e.g. **I**) if useful for QA. +- `docs/manual-qa/NEO-72.md`. + +**Out of scope** + +- Drag-drop, split stacks, equip UX polish. +- POST inventory mutations from UI (craft/gather stories own refresh triggers). + +**Acceptance criteria** + +- [ ] Boot (or first open) shows current server inventory for `dev_player_id`. +- [ ] HUD lists all non-empty bag slots and equipment stub slot from GET body. +- [ ] Failed GET surfaces visible error/warning (match interaction client pattern). + +--- + +### E3S5-02 — Client gather feedback on interact (E3.M1) + +**Goal:** After successful **resource_node** interact, player **sees** inventory change and **salvage** XP movement — not Output-panel-only proof. + +**In scope** + +- Extend gather interact path: on `allowed=true` for resource nodes, call **inventory refresh** + **skill-progression GET** (`salvage` row). +- Feedback line: e.g. `GatherFeedbackLabel` — “+N scrap_metal_bulk” (derive delta from before/after inventory or document server-truth refresh-only copy). +- Map **`node_depleted`** deny to readable HUD message (not silent). +- Wire all four prototype resource node ids (not only `prototype_resource_node_alpha`) if interact keys or targeting supports them; document bindings. +- GdUnit: mock interact 200 → refresh invoked. +- `docs/manual-qa/NEO-73.md`. + +**Out of scope** + +- New gather HTTP route; node respawn visuals; gather VFX. + +**Acceptance criteria** + +- [ ] In-range interact on fresh node updates inventory HUD without manual refresh. +- [ ] Depleted node shows **`node_depleted`** (or agreed copy) on HUD. +- [ ] **Salvage** skill level or XP visible and changes after successful gather (Epic 2 Slice 3 visibility). + +--- + +### E3S5-03 — Client craft UI + recipe list (E3.M2) + +**Goal:** Player selects a prototype recipe and **`POST /game/players/{id}/craft`** from Godot; sees success/deny and updated inventory + **refine** XP. + +**In scope** + +- `recipe_definitions_client.gd`: GET world recipe defs (NEO-68). +- `craft_client.gd`: POST `{ recipeId, quantity? }`; parse success/deny + `reasonCode`. +- HUD: simple recipe list (id / displayName, inputs summary); **Craft** per row for qty **1**. +- On success: refresh inventory + skill-progression (**refine**); show feedback line. +- Deny paths documented in manual QA: insufficient materials, bag full, unknown recipe. +- GdUnit: POST payload shape, deny parsing. +- `docs/manual-qa/NEO-74.md`. + +**Out of scope** + +- Station/bench world objects; craft quantity spinner; recipe filtering by skill level. + +**Acceptance criteria** + +- [ ] Player can **`refine_scrap_standard`** and **`make_field_stim_mk0`** from UI when materials allow. +- [ ] Denied craft shows **`reasonCode`** on HUD; inventory unchanged (verify via GET). +- [ ] **Refine** skill XP visible and increases on successful craft. + +--- + +### E3S5-04 — Playable gather→refine→make loop in client (capstone) + +**Goal:** Prove Epic 3 prototype acceptance **in Godot**: gather **`scrap_metal_bulk`** → **`refine_scrap_standard`** → **`make_field_stim_mk0`** without Bruno; satisfies vision **crafting loop** + **progression gate** smoke test. + +**In scope** + +- `main.gd` integration checklist: interact → inventory → craft panel flow documented in **`client/README.md`**. +- **`docs/manual-qa/NEO-75.md`**: numbered session script (move to node, gather ×N, refine, make stim, verify consumable in bag). +- Optional: toggle/collapse HUD sections so prototype scene stays usable (document layout). +- Cross-link Epic 3 Slice 5 complete in module docs + alignment register when done. + +**Out of scope** + +- Quest UI (E7); combat consumable use (E5 follow-up); art pass. + +**Acceptance criteria** + +- [ ] Manual QA checklist completable by a human in one session with server + client running. +- [ ] **`field_stim_mk0`** quantity ≥ 1 in inventory at end of script. +- [ ] Epic 3 Slice 3 acceptance re-read: **player** (not Bruno) completes gather → refine → usable item. + +--- + +## After this backlog + +- Epic 3 **prototype** is **full-stack complete** for gather/craft/inventory (Slices 1–3 server + Slice 5 client). +- Slice 4 (**E3.M4** + **E3.M5**) sinks/policy remains **pre-production**. +- Apply the same **server + client pairing** pattern when decomposing other epics (see [decomposition README](../decomposition/README.md#full-stack-epics)). +- Track delivery in Linear; keep `blockedBy` synchronized if scope changes. + +## Related docs + +- [epic_03_crafting_economy.md](../decomposition/epics/epic_03_crafting_economy.md) +- [E3M1-prototype-backlog.md](E3M1-prototype-backlog.md), [E3M2-prototype-backlog.md](E3M2-prototype-backlog.md), [E3M3-prototype-backlog.md](E3M3-prototype-backlog.md) +- [client_server_authority.md](../decomposition/modules/client_server_authority.md) +- [neon_sprawl_vision.plan.md — Prototype Scope](../../neon_sprawl_vision.plan.md) diff --git a/docs/plans/NEO-65-implementation-plan.md b/docs/plans/NEO-65-implementation-plan.md new file mode 100644 index 0000000..70625d0 --- /dev/null +++ b/docs/plans/NEO-65-implementation-plan.md @@ -0,0 +1,129 @@ +# NEO-65 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-65 | +| **Title** | E3.M2: Prototype RecipeDef catalog + schemas + CI | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-65/e3m2-prototype-recipedef-catalog-schemas-ci | +| **Module** | [E3.M2 — RefinementAndRecipeExecution](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) · Epic 3 Slice 3 · backlog **E3M2-01** | +| **Branch** | `NEO-65-e3m2-prototype-recipedef-catalog-schemas-ci` | +| **Upstream** | [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) — frozen six-item catalog; prototype skill trio includes **`refine`**. | +| **Pattern** | [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) / [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57) — row JSON Schema + catalog envelope + `scripts/validate_content.py` Slice gate | +| **Blocks** | [NEO-66](https://linear.app/neon-sprawl/issue/NEO-66) — server recipe catalog load | + +## Kickoff clarifications + +**No blocking clarifications needed.** Linear AC, [E3M2-prototype-backlog — E3M2-01](E3M2-prototype-backlog.md#e3m2-01--prototype-recipedef-catalog--schemas--ci), [E3.M2 kickoff decisions](E3M2-prototype-backlog.md#kickoff-decisions-decomposition-defaults), and the [Slice 3 freeze table](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md#prototype-slice-3-freeze-e3m2-01) fully specify recipe count, ids, I/O quantities, `recipeKind`, and `requiredSkillId`. Schema shape follows NEO-50 / NEO-57 precedent: + +| Topic | Agent recommendation | Status | +|--------|----------------------|--------| +| **Catalog envelope** | `{ "schemaVersion": 1, "recipes": [ … ] }` in `content/recipes/prototype_recipes.json` (mirror items / resource-nodes). | **Adopted** (NEO-50 precedent) | +| **`displayName` on `RecipeDef`** | **Required** — mirror `ItemDef`; supports E3M2-04 GET without schema churn. | **Adopted** (NEO-50 / NEO-57 precedent) | +| **Optional forward-compat stubs** | Optional `requiredSkillLevel` (int ≥ 0) and `stationTag` (string) on `recipe-def.schema.json`; **omitted** on all eight prototype rows (Slice 3 has no gates / stations). | **Adopted** (NEO-50 stub pattern) | +| **`requiredSkillLevel` in prototype rows** | **Omitted** — decomposition default; no level gates in Slice 3. | **Adopted** (backlog kickoff table) | + +## Goal, scope, and out-of-scope + +**Goal:** Lock content shape and CI validation for **eight** frozen `RecipeDef` rows before server load (NEO-66+). + +**In scope (from Linear + [E3M2-01](E3M2-prototype-backlog.md#e3m2-01--prototype-recipedef-catalog--schemas--ci)):** + +- `content/schemas/recipe-def.schema.json` and `content/schemas/recipe-io-row.schema.json`. +- `content/recipes/prototype_recipes.json` — eight frozen ids (freeze table below). +- `scripts/validate_content.py` — schema validation, duplicate `id`, exact eight-recipe allowlist, `recipeKind` coverage (**`process`** + **`make`**), cross-check every input/output **`itemId`** against item catalogs, every **`requiredSkillId`** against skill catalogs (prototype gate: all **`refine`**). +- Designer note in **E3.M2** module doc + `content/README.md` Slice 3 paragraph. +- **CT.M1** PR gate paragraph for recipe catalogs. + +**Out of scope (from Linear):** + +- Server loader, craft engine, HTTP (NEO-66+). + +## Acceptance criteria checklist + +- [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. + +## Technical approach + +1. **Row schema — I/O row (`recipe-io-row.schema.json`):** Draft 2020-12 object; `additionalProperties: false`. Required: `itemId`, `quantity`. `itemId` pattern `^[a-z][a-z0-9_]*$`. `quantity` integer ≥ 1. + +2. **Row schema — `RecipeDef` (`recipe-def.schema.json`):** Draft 2020-12 object; `additionalProperties: false`. Required: `id`, `displayName`, `recipeKind`, `requiredSkillId`, `inputs`, `outputs`. `id` pattern `^[a-z][a-z0-9_]*$`. `recipeKind` enum: `process` \| `make`. `requiredSkillId` string (same id pattern). `inputs` / `outputs`: arrays with `minItems: 1`; each element validates against `recipe-io-row.schema.json` (via `$ref`). Optional stubs (unused in prototype rows): `requiredSkillLevel` (int ≥ 0), `stationTag` (string). + +3. **Catalog file:** `content/recipes/prototype_recipes.json` with `schemaVersion: 1` and eight rows from the freeze table; placeholder `displayName` strings acceptable. + +4. **Frozen content values (prototype Slice 3):** + + | `RecipeDef.id` | `recipeKind` | inputs → outputs | + |----------------|--------------|------------------| + | `refine_scrap_standard` | `process` | 5× `scrap_metal_bulk` → 1× `refined_plate_stock` | + | `refine_scrap_efficient` | `process` | 10× `scrap_metal_bulk` → 2× `refined_plate_stock` | + | `make_field_stim_mk0` | `make` | 2× `refined_plate_stock` + 1× `scrap_metal_bulk` → 1× `field_stim_mk0` | + | `make_field_stim_batch` | `make` | 5× `refined_plate_stock` + 3× `scrap_metal_bulk` → 3× `field_stim_mk0` | + | `make_prototype_armor` | `make` | 8× `refined_plate_stock` + 5× `scrap_metal_bulk` → 1× `prototype_armor_shell` | + | `make_armor_quick` | `make` | 4× `refined_plate_stock` + 3× `scrap_metal_bulk` → 1× `prototype_armor_shell` | + | `make_survey_drone_kit` | `make` | 3× `refined_plate_stock` + 2× `scrap_metal_bulk` → 1× `survey_drone_kit` | + | `make_contract_token` | `make` | 2× `refined_plate_stock` + 1× `scrap_metal_bulk` → 1× `contract_handoff_token` | + + All rows: `requiredSkillId: "refine"`. No `requiredSkillLevel` or `stationTag` on prototype rows. + +5. **`validate_content.py`:** + - Add `RECIPE_SCHEMA`, `RECIPE_IO_ROW_SCHEMA`, `RECIPES_DIR`; discover `content/recipes/*_recipes.json`. + - `_validate_recipe_catalogs`: envelope `schemaVersion: 1`, top-level `recipes` array; validate each row against `recipe-def.schema.json`; track `seen_ids`; reject duplicates across files. + - Post-schema cross-checks per row: every I/O `itemId` ∈ loaded item ids; `requiredSkillId` ∈ loaded skill ids. + - **`_prototype_slice3_recipe_gate`:** ids must equal `PROTOTYPE_SLICE3_RECIPE_IDS`; `recipeKind` set must include **`process`** and **`make`**; every row `requiredSkillId` must be **`refine`** (prototype Slice 3 contract). + - Run recipe validation **after** skill + item catalogs succeed (reuse `seen_ids` / `item_seen_ids`). + - Require recipe schema files in `main()` startup checks; extend success summary with recipe file + id counts. + - Keep `PROTOTYPE_SLICE3_RECIPE_IDS` in sync with [E3.M2 freeze table](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md#prototype-slice-3-freeze-e3m2-01) and future NEO-66 server loader. + +6. **Docs:** + - `content/README.md` — ensure `recipes/` table row + Slice 3 freeze paragraph (may already exist on branch from decomposition prep). + - `E3_M2_RefinementAndRecipeExecution.md` — ensure freeze table + CI rules line (may already exist on branch). + - `CT_M1_ContentValidationPipeline.md` — recipe catalog PR gate paragraph. + - `documentation_and_implementation_alignment.md` — E3.M2 row → **In Progress** / NEO-65. + - `E3M2-prototype-backlog.md` — E3M2-01 checkboxes when implementation completes. + +7. **No server/C#/client changes** in this story. + +## Files to add + +| Path | Purpose | +|------|---------| +| `content/schemas/recipe-io-row.schema.json` | JSON Schema for a single recipe input/output row. | +| `content/schemas/recipe-def.schema.json` | JSON Schema for a single `RecipeDef` row. | +| `content/recipes/prototype_recipes.json` | Prototype eight-recipe catalog (`schemaVersion` + `recipes` array). | +| `docs/plans/NEO-65-implementation-plan.md` | This plan. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `scripts/validate_content.py` | Load/validate recipe catalogs; duplicate `id`; Slice 3 eight-id + `recipeKind` + `refine` gates; I/O item + skill cross-checks; success summary. | +| `content/README.md` | Document `content/recipes/`, schema paths, Slice 3 freeze (active catalog). | +| `docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md` | Confirm freeze table + CI enforcement note; link to plan when complete. | +| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for recipe catalog validation. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M2 / NEO-65 status after catalog lands. | +| `docs/plans/E3M2-prototype-backlog.md` | E3M2-01 checkboxes + landed note when complete. | + +## Tests + +| Item | Coverage | +|------|----------| +| **CI / PR gate** | `.github/workflows/pr-gate.yml` already runs `python scripts/validate_content.py` — extended script is the regression signal. | +| **Manual negative cases** | From repo root after implementation: (1) duplicate recipe `id` → non-zero exit; (2) remove one frozen id → gate error; (3) unknown `itemId` in inputs → cross-check error; (4) `requiredSkillId: "salvage"` → Slice 3 gate error; (5) break schema (empty `outputs`) → schema error. | +| **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports recipe catalog + existing catalogs OK. | + +No dedicated pytest module in-repo today (same as NEO-50 / NEO-57). + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| **Catalog growth** | Slice 3 gate requires **exactly** the eight frozen ids; expanding the roster needs a follow-up issue to change `PROTOTYPE_SLICE3_RECIPE_IDS`. | **adopted** | +| **C# vs Python drift** | None until NEO-66; document “keep in sync” constant names in both places when server load lands. | **adopted** | +| **Branch carries E3M2 decomposition prep** | Commit decomposition doc deltas (freeze table, backlog, README) with implementation or as first doc batch on this branch. | **done** — prep landed in prior commits; catalog + CI in this story. | + +None blocking. diff --git a/docs/plans/_prototype-backlog-template.md b/docs/plans/_prototype-backlog-template.md new file mode 100644 index 0000000..957d613 --- /dev/null +++ b/docs/plans/_prototype-backlog-template.md @@ -0,0 +1,118 @@ +# E?.M? — Prototype story backlog () + + + + +Working backlog for **Epic ? — Slice ?** ([slice anchor](../decomposition/epics/epic_??_*.md#…)). Decomposition and contracts: [E?.M? — ModuleName](../decomposition/modules/E?_M?_*.md). + +**Full-stack policy:** Epics deliver **playable game features**. Every **player-visible** story has a **`client`** Linear issue created in **this same decomposition pass** — not an undocumented follow-up. Reference: [E1M3 (paired server+client)](../plans/E1M3-prototype-backlog.md), [E3S5 (client slice)](../plans/E3S5-client-prototype-backlog.md). + +**Labels (Linear):** module label **`E?.M?`** on every issue; add **`server`** or **`client`** + **`Story`** as listed. + +**Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the story branch when work starts. + +| Slug | Layer | Linear | +|------|-------|--------| +| E?M?-01 | server | [NEO-??](https://linear.app/neon-sprawl/issue/NEO-??) | +| E?M?-01-client | client | [NEO-??](https://linear.app/neon-sprawl/issue/NEO-??) | +| … | … | … | +| E?M?-NN-capstone | client | [NEO-??](https://linear.app/neon-sprawl/issue/NEO-??) | + +**Dependency graph in Linear:** document `blockedBy` for each issue (client stories blocked by their server API dependencies). + +**Board order:** estimates **1…N** matching slug execution order; sort Epic project by **Estimate** (ascending). + +--- + +## Story order (recommended) + +| Order | Slug | Layer | Depends on | +|-------|------|-------|------------| +| 1 | **E?M?-01** | server | … | +| 2 | **E?M?-01-client** | client | E?M?-01 | +| … | … | … | … | + +--- + +## Kickoff decisions (decomposition defaults) + +| Topic | Decision | Rationale | +|-------|----------|-----------| +| HUD fidelity | Prototype Labels / debug panels (not final art) | NEO-24 / NEO-28 precedent | +| Authority | Client displays only; mutations via server routes | [client_server_authority.md](../decomposition/modules/client_server_authority.md) | + +--- + +### E?M?-01 — + +**Goal:** … + +**In scope** + +- … + +**Out of scope** + +- Godot UI — **client counterpart [NEO-??](#)** (not optional; separate issue) + +**Acceptance criteria** + +- [ ] … + +**Client counterpart:** [NEO-??](https://linear.app/neon-sprawl/issue/NEO-??) — + +--- + +### E?M?-01-client — + +**Goal:** Godot … + +**In scope** + +- `client/scripts/*_client.gd`, `main.gd` wiring, HUD under `UICanvas` +- GdUnit tests with HTTP doubles ([testing-expectations.md](../../.cursor/rules/testing-expectations.md)) +- `docs/manual-qa/NEO-??.md` — **Godot** steps (server + client running) + +**Out of scope** + +- … + +**Acceptance criteria** + +- [ ] Player-visible outcome in Godot (not Output-panel-only) +- [ ] Manual QA checklist exercisable without Bruno + +--- + +### E?M?-NN-capstone — Playable loop in client + +**Goal:** Prove epic slice acceptance **in Godot** without Bruno. + +**In scope** + +- `docs/manual-qa/NEO-??.md` session script +- `client/README.md` integration notes + +**Acceptance criteria** + +- [ ] Vision prototype gate / epic AC satisfied by a human in Godot + +--- + +## After this backlog + +- Track delivery in Linear; keep `blockedBy` synchronized. +- **Do not** mark module **prototype complete** until **client** stories (and capstone if any) are **Done** or explicitly deferred with a **created** follow-up issue id. +- For each issue kickoff: `docs/plans/{NEO-XX}-implementation-plan.md` per [story-kickoff](../../.cursor/rules/story-kickoff.md). + +## Decomposition complete checklist + +- [ ] Every player-visible AC has a **`client`** Linear issue +- [ ] No “optional follow-up” / “future client” without NEO-XX +- [ ] Epic DoD mentions **Godot** (not Bruno-only) +- [ ] [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) updated + +## Related docs + +- [epic_??_*.md](../decomposition/epics/) +- [neon_sprawl_vision.plan.md — Prototype Scope](../../neon_sprawl_vision.plan.md) diff --git a/docs/reviews/2026-05-24-NEO-65.md b/docs/reviews/2026-05-24-NEO-65.md new file mode 100644 index 0000000..3393795 --- /dev/null +++ b/docs/reviews/2026-05-24-NEO-65.md @@ -0,0 +1,64 @@ +# Code review — NEO-65 prototype RecipeDef catalog + schemas + CI + +**Date:** 2026-05-24 +**Scope:** Branch `NEO-65-e3m2-prototype-recipedef-catalog-schemas-ci` · commits `0556d5f`–`b4f27b4` vs `origin/main` +**Base:** `origin/main` + +## Verdict + +**Approve with nits** + +## Summary + +NEO-65 delivers the **E3M2-01** infrastructure slice: `recipe-def` / `recipe-io-row` JSON Schemas, eight frozen rows in `content/recipes/prototype_recipes.json`, and `scripts/validate_content.py` gates mirroring NEO-50 / NEO-57 (schema, duplicate `id`, exact allowlist, `recipeKind` coverage, item + skill cross-checks, Slice 3 `refine`-only contract). Docs close out the plan, CT.M1 PR-gate paragraph, E3.M2 freeze table, alignment register row, and E3M2-01 backlog checkboxes. The branch also carries Epic 3 **full-stack decomposition** prep (E3M2/E3S5 backlogs, `.cursor/rules/full-stack-epic-decomposition.md`) — appropriate context for Slice 3 but orthogonal to NEO-65 runtime risk. No server, client, or C# changes; CI script is the regression signal. Negative gates verified locally (duplicate id, unknown item, wrong skill, seven-recipe roster). + +## Documentation checked + +| Document | Result | +|----------|--------| +| [`docs/plans/NEO-65-implementation-plan.md`](../plans/NEO-65-implementation-plan.md) | **Matches** — schemas, catalog, validate_content gates, doc touches, acceptance checklist complete; out-of-scope (server/HTTP) respected. | +| [`docs/plans/E3M2-prototype-backlog.md`](../plans/E3M2-prototype-backlog.md) | **Matches** — E3M2-01 checkboxes + landed note; infrastructure-only (no client counterpart required for this slug). | +| [`docs/plans/E3S5-client-prototype-backlog.md`](../plans/E3S5-client-prototype-backlog.md) | **Matches** — craft UI **NEO-74** / capstone **NEO-75** exist for player-visible Slice 3 work (full-stack policy satisfied for epic, not this ticket). | +| [`docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md`](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) | **Matches** — freeze table, designer note, CI rules, backlog links; header **Status** **In Progress** + NEO-65 implementation snapshot (addressed post-review). | +| [`docs/decomposition/modules/CT_M1_ContentValidationPipeline.md`](../decomposition/modules/CT_M1_ContentValidationPipeline.md) | **Matches** — recipe catalog PR gate paragraph. | +| [`docs/decomposition/modules/documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) | **Matches** — E3.M2 row **In Progress** with NEO-65 landed note; E3.M1/E3.M3 cross-link Slice 5 client issues. | +| [`docs/decomposition/modules/module_dependency_register.md`](../decomposition/modules/module_dependency_register.md) | **Partially matches** — E3.M2 **note** and E3 Slice 5 note updated; table **Status** column still **Planned** (consistent with E3.M3 pattern until slice completes — optional bump when NEO-66 lands). | +| [`content/README.md`](../../content/README.md) | **Matches** — `recipes/` table row + Slice 3 freeze paragraph. | +| [`docs/decomposition/modules/client_server_authority.md`](../decomposition/modules/client_server_authority.md) | **N/A** — content/CI only. | +| Manual QA | **N/A** — plan defers to CI + ad-hoc negative CLI checks (same as NEO-50 / NEO-57); no `docs/manual-qa/NEO-65.md` required for infrastructure-only content. | + +Register/tracking: alignment table E3.M2 **In Progress** is appropriate after E3M2-01; register row **Planned** until Slice 3 server stories land is acceptable if intentional. + +## Blocking issues + +None. + +## Suggestions + +1. ~~**E3.M2 module doc status + snapshot** — [`E3_M2_RefinementAndRecipeExecution.md`](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) header **Status** is still **Planned** while [`documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) lists **In Progress**. Bump header to **In Progress** and add an **Implementation snapshot** bullet for NEO-65 (catalog + CI), mirroring E3.M1/E3.M3 landed notes.~~ **Done.** Header **In Progress** + NEO-65 snapshot bullet added. + +2. ~~**NEO-66 sync reminder** — When the server loader lands, duplicate `PROTOTYPE_SLICE3_RECIPE_IDS` in C# with an explicit “keep in sync with `validate_content.py`” comment (already noted in the plan; worth repeating in the NEO-66 kickoff plan).~~ **Done.** Kickoff note added under E3M2-02 in [`E3M2-prototype-backlog.md`](../plans/E3M2-prototype-backlog.md) (NEO-66 plan not yet authored). + +## Nits + +- Nit: `_validate_recipe_catalogs` runs I/O `itemId` cross-check even when a duplicate `id` is detected (inside the `if isinstance(rid, str)` block but outside the `else`). Harmless — surfaces more errors per run; matches pragmatic CI style elsewhere. + +- Nit: Branch mixes **chore** full-stack policy commits with NEO-65 implementation — fine for sequencing, but PR description should call out both so reviewers know the policy/docs delta is intentional prep, not scope creep. + +## Verification + +```bash +cd /home/don/neon-sprawl +pip install -r scripts/requirements-content.txt +python3 scripts/validate_content.py +# content OK: … 1 recipe catalog file(s), … 8 unique recipe id(s) +``` + +Manual negative cases (from plan — verified during review): + +- Duplicate recipe `id` → exit 1 +- Unknown `itemId` in inputs → exit 1 +- `requiredSkillId: salvage` → exit 1 (Slice 3 gate) +- Remove one frozen id (seven recipes) → exit 1 + +PR gate (`.github/workflows/pr-gate.yml`) already invokes `validate_content.py` — no additional test project required for this story. diff --git a/scripts/validate_content.py b/scripts/validate_content.py index b32abe8..2a62fe6 100644 --- a/scripts/validate_content.py +++ b/scripts/validate_content.py @@ -9,6 +9,7 @@ Validates: - item catalogs: content/items/*_items.json rows vs content/schemas/item-def.schema.json (NEO-50) - resource node catalogs: content/resource-nodes/*_resource_nodes.json vs resource-node-def.schema.json (NEO-57) - resource yield catalogs: content/resource-nodes/*_resource_yields.json vs resource-yield-row.schema.json (NEO-57) +- recipe catalogs: content/recipes/*_recipes.json rows vs content/schemas/recipe-def.schema.json (NEO-65) """ from __future__ import annotations @@ -18,6 +19,7 @@ import sys from pathlib import Path from jsonschema import Draft202012Validator +from referencing import Registry, Resource REPO_ROOT = Path(__file__).resolve().parent.parent SKILL_SCHEMA = REPO_ROOT / "content/schemas/skill-def.schema.json" @@ -26,10 +28,13 @@ MASTERY_SCHEMA = REPO_ROOT / "content/schemas/mastery-catalog.schema.json" ITEM_SCHEMA = REPO_ROOT / "content/schemas/item-def.schema.json" RESOURCE_NODE_SCHEMA = REPO_ROOT / "content/schemas/resource-node-def.schema.json" RESOURCE_YIELD_ROW_SCHEMA = REPO_ROOT / "content/schemas/resource-yield-row.schema.json" +RECIPE_IO_ROW_SCHEMA = REPO_ROOT / "content/schemas/recipe-io-row.schema.json" +RECIPE_SCHEMA = REPO_ROOT / "content/schemas/recipe-def.schema.json" SKILLS_DIR = REPO_ROOT / "content/skills" MASTERY_DIR = REPO_ROOT / "content/mastery" ITEMS_DIR = REPO_ROOT / "content/items" RESOURCE_NODES_DIR = REPO_ROOT / "content/resource-nodes" +RECIPES_DIR = REPO_ROOT / "content/recipes" # Slice 1 prototype lock (NEO-33): exact ids + category coverage after schema passes. PROTOTYPE_SLICE1_SKILL_IDS = frozenset({"salvage", "refine", "intrusion"}) @@ -67,6 +72,22 @@ PROTOTYPE_SLICE2_GATHER_LENSES = frozenset( ) PROTOTYPE_SLICE2_YIELD_ITEM_IDS = frozenset({"scrap_metal_bulk"}) +# Slice 3 prototype lock (NEO-65): exact recipe ids + recipeKind coverage after schema passes. +# Keep in sync with E3.M2 freeze table and future NEO-66 server loader. +PROTOTYPE_SLICE3_RECIPE_IDS = frozenset( + { + "refine_scrap_standard", + "refine_scrap_efficient", + "make_field_stim_mk0", + "make_field_stim_batch", + "make_prototype_armor", + "make_armor_quick", + "make_survey_drone_kit", + "make_contract_token", + } +) +PROTOTYPE_SLICE3_REFINE_SKILL_ID = "refine" + def _prototype_slice1_gate(seen_ids: dict[str, str], id_to_category: dict[str, str]) -> str | None: """Return a human-readable error if Slice 1 contract fails, else None.""" @@ -551,6 +572,126 @@ def _validate_resource_yield_catalogs( return errors, frozenset(seen_node_ids.keys()), frozenset(yield_item_ids) +def _recipe_def_validator() -> Draft202012Validator: + """Build a validator that resolves recipe-def $ref to recipe-io-row schema.""" + io_schema = json.loads(RECIPE_IO_ROW_SCHEMA.read_text(encoding="utf-8")) + def_schema = json.loads(RECIPE_SCHEMA.read_text(encoding="utf-8")) + registry = Registry().with_resources( + [ + (def_schema["$id"], Resource.from_contents(def_schema)), + (io_schema["$id"], Resource.from_contents(io_schema)), + ] + ) + return Draft202012Validator(def_schema, registry=registry) + + +def _prototype_slice3_recipe_gate( + seen_ids: dict[str, str], + id_to_kind: dict[str, str], + id_to_skill: dict[str, str], +) -> str | None: + """Return a human-readable error if Slice 3 recipe contract fails, else None.""" + ids = frozenset(seen_ids.keys()) + if ids != PROTOTYPE_SLICE3_RECIPE_IDS: + return ( + "error: prototype Slice 3 expects exactly recipe ids " + f"{sorted(PROTOTYPE_SLICE3_RECIPE_IDS)!r}, got {sorted(ids)!r}" + ) + kinds = set(id_to_kind.values()) + if "process" not in kinds or "make" not in kinds: + return ( + "error: prototype Slice 3 requires at least one process and one make recipe; " + f"recipeKind values seen: {sorted(kinds)!r}" + ) + for rid, skill_id in id_to_skill.items(): + if skill_id != PROTOTYPE_SLICE3_REFINE_SKILL_ID: + return ( + f"error: {rid!r} requiredSkillId must be {PROTOTYPE_SLICE3_REFINE_SKILL_ID!r}, " + f"got {skill_id!r}" + ) + return None + + +def _validate_recipe_catalogs( + *, + recipe_files: list[Path], + recipe_validator: Draft202012Validator, + known_item_ids: frozenset[str], + known_skill_ids: frozenset[str], +) -> tuple[int, dict[str, str], dict[str, str], dict[str, str]]: + """Validate recipe JSON files. Returns (error_count, seen_ids, id_to_kind, id_to_skill).""" + errors = 0 + seen_ids: dict[str, str] = {} + id_to_kind: dict[str, str] = {} + id_to_skill: dict[str, str] = {} + + for path in recipe_files: + rel = str(path.relative_to(REPO_ROOT)) + data = json.loads(path.read_text(encoding="utf-8")) + schema_version = data.get("schemaVersion") + if schema_version != 1: + print(f"error: {rel}: expected schemaVersion 1, got {schema_version!r}", file=sys.stderr) + errors += 1 + continue + + recipes = data.get("recipes") + if not isinstance(recipes, list): + print(f"error: {rel}: expected top-level 'recipes' array", file=sys.stderr) + errors += 1 + continue + + for i, row in enumerate(recipes): + if not isinstance(row, dict): + print(f"error: {rel}: recipes[{i}] must be an object", file=sys.stderr) + errors += 1 + continue + row_schema_errors = 0 + for err in sorted(recipe_validator.iter_errors(row), key=lambda e: e.path): + loc = ".".join(str(p) for p in err.path) or "(root)" + print(f"error: {rel} recipes[{i}] {loc}: {err.message}", file=sys.stderr) + row_schema_errors += 1 + errors += 1 + rid = row.get("id") + if isinstance(rid, str) and row_schema_errors == 0: + prev = seen_ids.get(rid) + if prev: + print(f"error: duplicate recipe id {rid!r} in {prev} and {rel}", file=sys.stderr) + errors += 1 + else: + seen_ids[rid] = rel + recipe_kind = row.get("recipeKind") + if isinstance(recipe_kind, str): + id_to_kind[rid] = recipe_kind + skill_id = row.get("requiredSkillId") + if isinstance(skill_id, str): + id_to_skill[rid] = skill_id + if skill_id not in known_skill_ids: + print( + f"error: {rel} recipes[{i}]: requiredSkillId {skill_id!r} " + f"is not a known SkillDef id (known: {sorted(known_skill_ids)!r})", + file=sys.stderr, + ) + errors += 1 + + for io_key in ("inputs", "outputs"): + io_rows = row.get(io_key) + if not isinstance(io_rows, list): + continue + for j, io_row in enumerate(io_rows): + if not isinstance(io_row, dict): + continue + item_id = io_row.get("itemId") + if isinstance(item_id, str) and item_id not in known_item_ids: + print( + f"error: {rel} recipes[{i}].{io_key}[{j}]: itemId {item_id!r} " + "is not in item catalogs", + file=sys.stderr, + ) + errors += 1 + + return errors, seen_ids, id_to_kind, id_to_skill + + def main() -> int: if not SKILL_SCHEMA.is_file(): print(f"error: missing schema {SKILL_SCHEMA}", file=sys.stderr) @@ -570,6 +711,12 @@ def main() -> int: if not RESOURCE_YIELD_ROW_SCHEMA.is_file(): print(f"error: missing schema {RESOURCE_YIELD_ROW_SCHEMA}", file=sys.stderr) return 1 + if not RECIPE_IO_ROW_SCHEMA.is_file(): + print(f"error: missing schema {RECIPE_IO_ROW_SCHEMA}", file=sys.stderr) + return 1 + if not RECIPE_SCHEMA.is_file(): + print(f"error: missing schema {RECIPE_SCHEMA}", file=sys.stderr) + return 1 skill_schema = json.loads(SKILL_SCHEMA.read_text(encoding="utf-8")) skill_validator = Draft202012Validator(skill_schema) @@ -583,6 +730,7 @@ def main() -> int: resource_node_validator = Draft202012Validator(resource_node_schema) resource_yield_schema = json.loads(RESOURCE_YIELD_ROW_SCHEMA.read_text(encoding="utf-8")) resource_yield_validator = Draft202012Validator(resource_yield_schema) + recipe_validator = _recipe_def_validator() if not SKILLS_DIR.is_dir(): print(f"error: missing directory {SKILLS_DIR}", file=sys.stderr) @@ -630,6 +778,15 @@ def main() -> int: print(f"error: no *_resource_yields.json files under {RESOURCE_NODES_DIR}", file=sys.stderr) return 1 + if not RECIPES_DIR.is_dir(): + print(f"error: missing directory {RECIPES_DIR}", file=sys.stderr) + return 1 + + recipe_files = sorted(RECIPES_DIR.glob("*_recipes.json")) + if not recipe_files: + print(f"error: no *_recipes.json files under {RECIPES_DIR}", file=sys.stderr) + return 1 + seen_ids: dict[str, str] = {} id_to_category: dict[str, str] = {} errors = 0 @@ -803,6 +960,21 @@ def main() -> int: print(slice2_yield_err, file=sys.stderr) return 1 + recipe_errors, recipe_seen_ids, id_to_kind, id_to_skill = _validate_recipe_catalogs( + recipe_files=recipe_files, + recipe_validator=recipe_validator, + known_item_ids=frozenset(item_seen_ids.keys()), + known_skill_ids=frozenset(seen_ids.keys()), + ) + if recipe_errors: + print(f"content validation failed with {recipe_errors} error(s)", file=sys.stderr) + return 1 + + slice3_recipe_err = _prototype_slice3_recipe_gate(recipe_seen_ids, id_to_kind, id_to_skill) + if slice3_recipe_err: + print(slice3_recipe_err, file=sys.stderr) + return 1 + print( "content OK: " f"{len(skill_files)} skill catalog file(s), " @@ -811,9 +983,11 @@ def main() -> int: f"{len(item_files)} item catalog file(s), " f"{len(node_files)} resource node catalog file(s), " f"{len(yield_files)} resource yield catalog file(s), " + f"{len(recipe_files)} recipe catalog file(s), " f"{len(seen_ids)} unique skill id(s), " f"{len(item_seen_ids)} unique item id(s), " f"{len(node_seen_ids)} unique nodeDefId(s), " + f"{len(recipe_seen_ids)} unique recipe id(s), " f"{len(track_skill_ids)} mastery track(s)" ) return 0