From b7f29dcd6abe7015a0303f963f72a9c64a0502e8 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 24 May 2026 13:47:55 -0400 Subject: [PATCH] NEO-57: add implementation plan for resource node content + CI Kickoff clarifications: two-file catalog layout, maxGathers in content, differentiated yield quantities, decomposition docs merged from chore branch. --- docs/plans/NEO-57-implementation-plan.md | 129 +++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 docs/plans/NEO-57-implementation-plan.md diff --git a/docs/plans/NEO-57-implementation-plan.md b/docs/plans/NEO-57-implementation-plan.md new file mode 100644 index 0000000..c0f6617 --- /dev/null +++ b/docs/plans/NEO-57-implementation-plan.md @@ -0,0 +1,129 @@ +# NEO-57 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-57 | +| **Title** | E3.M1: Prototype ResourceNodeDef + yield tables + schemas + CI | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-57/e3m1-prototype-resourcenodedef-yield-tables-schemas-ci | +| **Module** | [E3.M1 — ResourceNodeAndGatherLoop](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) · Epic 3 Slice 2 · backlog **E3M1-01** | +| **Branch** | `NEO-57-e3m1-prototype-resourcenodedef-yield-schemas-ci` | +| **Precursor** | [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) — frozen six-item catalog (`scrap_metal_bulk`); [NEO-41](https://linear.app/neon-sprawl/issue/NEO-41) — prototype `resource_node` interact XP anchor (unchanged this story) | +| **Pattern** | [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) — row JSON Schema + catalog envelope + `scripts/validate_content.py` Slice gate | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **Catalog layout** | One file vs two files / schemas? | **Two files + two row schemas** — `prototype_resource_nodes.json` + `prototype_resource_yields.json`; mirrors backlog paths and E3M1-02 loader split. | **User:** two files + two row schemas. | +| **`maxGathers` in content** | Include on `ResourceNodeDef` now? | **Yes** — depletion contract is documented in module freeze; avoids schema churn before E3M1-04. | **User:** yes now. | +| **Yield quantities** | Same vs per-node quantities? | **Differentiated** (e.g. 1 / 2 / 3 / 5) — proves per-node yield tables without new item ids. | **User:** differentiated. | +| **Decomposition docs** | How to land E3M1 backlog + freeze table? | **Merge `chore/e3m1-slice2-decomposition`** into story branch first. | **User:** merge chore branch. | + +## Goal, scope, and out-of-scope + +**Goal:** Lock content shape and CI validation for **four** frozen `ResourceNodeDef` rows and **fixed** per-node yield rows before any server catalog load (E3M1-02). + +**In scope (from Linear + [E3M1-01](E3M1-prototype-backlog.md#e3m1-01--prototype-resourcenodedef--yield-tables--schemas--ci)):** + +- JSON Schemas: `resource-node-def.schema.json`, `resource-yield-row.schema.json` (single-row contracts). +- `content/resource-nodes/prototype_resource_nodes.json` — four frozen `nodeDefId` values aligned to interactable ids ([freeze table](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md#prototype-slice-2-freeze-e3m1-01)). +- `content/resource-nodes/prototype_resource_yields.json` — one yield row per node; **`itemId`** only **`scrap_metal_bulk`**; differentiated **`quantity`** per node. +- `scripts/validate_content.py` — schema validation, duplicate `nodeDefId`, exact four-node allowlist, **`gatherLens`** coverage (one per lens), cross-check yield **`itemId`** against loaded item catalog, exactly one yield row per known `nodeDefId`. +- Designer note already in E3.M1 module doc + `content/README.md` Slice 2 paragraph (from merged decomposition). + +**Out of scope (from Linear):** + +- Server loader, depletion store, inventory grants, HTTP, `PrototypeInteractableRegistry` new anchors (E3M1-05+), Bruno. + +## Acceptance criteria checklist + +- [ ] PR gate validates resource-node JSON against schema. +- [ ] Exactly four prototype node ids; duplicate `nodeDefId` fails CI. +- [ ] Every yield references a valid frozen item id (`scrap_metal_bulk`). +- [ ] Stable id list documented in module doc freeze box. + +## Technical approach + +1. **Row schema — `ResourceNodeDef` (`resource-node-def.schema.json`):** Draft 2020-12 object; `additionalProperties: false`. Required: `nodeDefId`, `displayName`, `gatherLens`, `maxGathers`. `nodeDefId` pattern `^[a-z][a-z0-9_]*$`. `gatherLens` enum: `consumer_salvage` \| `subsurface` \| `bio` \| `urban_bulk` (matches [gathering.md](../game-design/gathering.md#node-lenses-design-facing)). `maxGathers` integer ≥ 1. + +2. **Row schema — yield row (`resource-yield-row.schema.json`):** Required: `nodeDefId`, `itemId`, `quantity`. `quantity` integer ≥ 1. `itemId` uses same id pattern as items. + +3. **Catalog envelopes (mirror items):** + - `prototype_resource_nodes.json`: `{ "schemaVersion": 1, "nodes": [ … ] }` — four rows from freeze table; placeholder `displayName` strings OK. + - `prototype_resource_yields.json`: `{ "schemaVersion": 1, "yields": [ … ] }` — four rows, one per `nodeDefId`. + +4. **Frozen content values (prototype):** + + | `nodeDefId` | `gatherLens` | `maxGathers` | yield `quantity` | + |-------------|--------------|--------------|------------------| + | `prototype_resource_node_alpha` | `consumer_salvage` | `10` | `1` | + | `prototype_subsurface_vein_beta` | `subsurface` | `10` | `2` | + | `prototype_bio_mat_gamma` | `bio` | `10` | `3` | + | `prototype_urban_bulk_delta` | `urban_bulk` | `10` | `5` | + + All yields: `itemId` **`scrap_metal_bulk`**. `maxGathers` is uniform **10** for prototype (depletion behavior lands in E3M1-04; value is content-only until then). + +5. **`validate_content.py`:** + - Discover `content/resource-nodes/*_resource_nodes.json` and `*_resource_yields.json` (or fixed filenames above). + - Validate each node row / yield row against respective schemas. + - Track `seen_node_ids`; reject duplicates across node files. + - **`_prototype_slice2_resource_node_gate`:** ids must equal `PROTOTYPE_SLICE2_NODE_IDS`; `gatherLens` set must equal four lens enums exactly once each. + - Yield pass: reject duplicate `nodeDefId` in yields; every yield `nodeDefId` must exist in loaded nodes; every yield `itemId` must exist in item catalog (`PROTOTYPE_SLICE1_ITEM_IDS` / loaded item ids); prototype gate requires **only** `scrap_metal_bulk` as yield item id in Slice 2. + - Require **exactly one** yield row per frozen `nodeDefId`. + - Run resource-node validation **after** item catalogs succeed (reuse item id set from `_validate_item_catalogs`). + - Extend success summary line with resource-node file counts. + +6. **Docs (minimal delta beyond merged decomposition):** + - `content/README.md` — add `resource-nodes/` row to table + Slice 2 freeze pointer (if not already complete post-merge). + - `CT_M1_ContentValidationPipeline.md` — PR gate paragraph for resource-node catalogs. + - `documentation_and_implementation_alignment.md` — E3.M1 row note NEO-57 content landed when implementation completes. + +7. **No server/C#/client/Bruno changes** in this story. + +## Files to add + +| Path | Purpose | +|------|---------| +| `content/schemas/resource-node-def.schema.json` | JSON Schema for a single `ResourceNodeDef` row. | +| `content/schemas/resource-yield-row.schema.json` | JSON Schema for a single fixed-yield row. | +| `content/resource-nodes/prototype_resource_nodes.json` | Four-node prototype catalog (`schemaVersion` + `nodes`). | +| `content/resource-nodes/prototype_resource_yields.json` | Four yield rows (`schemaVersion` + `yields`). | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `scripts/validate_content.py` | Load/validate resource-node catalogs; duplicate `nodeDefId`; Slice 2 four-id + four-lens gates; yield↔item and yield↔node cross-checks; summary line. | +| `content/README.md` | Document `content/resource-nodes/`, schema paths, Slice 2 freeze (if merge left gaps). | +| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for resource-node + yield validation. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M1 / NEO-57 status after catalog lands. | + +**Already on branch (merge `chore/e3m1-slice2-decomposition`):** `docs/plans/E3M1-prototype-backlog.md`, `docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md` (freeze box), `docs/decomposition/modules/module_dependency_register.md` (E3.M1 note), `content/README.md` Slice 2 pointer. + +## Tests + +| Item | Coverage | +|------|----------| +| **CI / PR gate** | `.github/workflows/pr-gate.yml` runs `python scripts/validate_content.py` — extended script is the regression signal. | +| **Manual negative cases** | From repo root: (1) duplicate `nodeDefId` in nodes file → non-zero exit; (2) remove one frozen id → gate error; (3) invalid `gatherLens` → schema error; (4) yield references `not-a-prototype-item` → cross-check error; (5) second yield row for same `nodeDefId` → duplicate error; (6) wrong yield item id (not `scrap_metal_bulk`) → Slice 2 item gate error. | +| **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports resource-node catalogs + existing catalogs OK. | + +No dedicated pytest module (same as NEO-50 / NEO-33). No C# tests until E3M1-02 server loader. + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| **Interactable registry anchors** for beta/gamma/delta | **Defer to E3M1-05** — content ids frozen now; world X/Z added when interactables list grows. | **deferred** | +| **C# vs Python drift** | None until E3M1-02; duplicate `PROTOTYPE_SLICE2_*` constants in loader + script with “keep in sync” comment (NEO-50 pattern). | **adopted** | +| **Catalog growth beyond four nodes** | New Linear issue to relax `PROTOTYPE_SLICE2_NODE_IDS` and lens gate. | **adopted** | +| **Per-lens skill XP** | Stay **`salvage`** + **`activity`** until content pass; not in JSON this story. | **adopted** | + +## Decisions (kickoff) + +- **Two-file layout** with separate row schemas for nodes and yields. +- **`maxGathers`** on node defs in content now (`10` for all prototype rows). +- **Differentiated yield quantities** 1 / 2 / 3 / 5 by lens order in freeze table. +- **Decomposition docs** merged from `chore/e3m1-slice2-decomposition` on this branch before implementation commits.