neon-sprawl/docs/plans/NEO-57-implementation-plan.md

9.6 KiB

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 · Epic 3 Slice 2 · backlog E3M1-01
Branch NEO-57-e3m1-prototype-resourcenodedef-yield-schemas-ci
Precursor NEO-50 — frozen six-item catalog (scrap_metal_bulk); NEO-41 — prototype resource_node interact XP anchor (unchanged this story)
Pattern 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 schemasprototype_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):

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