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.pull/92/head
parent
17daf9a9ca
commit
b7f29dcd6a
|
|
@ -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.
|
||||
Loading…
Reference in New Issue