11 KiB
NEO-58 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-58 |
| Title | E3.M1: Server resource-node catalog load (fail-fast) |
| Linear | https://linear.app/neon-sprawl/issue/NEO-58/e3m1-server-resource-node-catalog-load-fail-fast |
| Module | E3.M1 — ResourceNodeAndGatherLoop · Epic 3 Slice 2 · backlog E3M1-02 |
| Branch | NEO-58-e3m1-server-resource-node-catalog-load-fail-fast (rebased on main after NEO-57 merge PR #92) |
| Precursor | NEO-57 — content/resource-nodes/ + CI gates (merged to main via PR #92) |
| Pattern | NEO-51 — fail-fast catalog loader + ContentPathsOptions + eager Program.cs resolve; strict split before registry ticket (NEO-59) |
Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|---|---|---|---|
| Branch base | Stack on NEO-57 vs branch from main? |
Branch from NEO-57 — Linear blockedBy NEO-57; loader requires content + schemas from E3M1-01. |
User: branch from NEO-57 (stacked PR). |
| DI scope (E3M1-02 vs E3M1-03) | Register IResourceNodeDefinitionRegistry now? |
ResourceNodeCatalog singleton only — E3M1-03 (NEO-59) owns injectable registry; mirrors NEO-51 / NEO-52 split for items. |
User: catalog singleton only. |
| Item cross-check | How to validate yield itemId at startup? |
Pass ids from ItemDefinitionCatalog in the DI factory (items load first; no second disk read). |
User: cross-check from ItemDefinitionCatalog. |
| Runtime validation | In-process C# vs subprocess validate_content.py? |
In-process C# mirroring scripts/validate_content.py (NEO-51 / NEO-34 precedent). |
Adopted — no separate question. |
| Host boot tests | Loader unit tests only vs also WebApplicationFactory? |
Both — mirror ItemDefinitionCatalogLoaderTests (happy path, failure modes, Host_ShouldResolveCatalogFromDi, Host_ShouldFailStartup_WhenCatalogDirectoryInvalid). |
Adopted — NEO-51 precedent. |
Goal, scope, and out-of-scope
Goal: On host startup, load content/resource-nodes/*_resource_nodes.json and *_resource_yields.json with the same validation rules as CI (NEO-57), build an in-memory ResourceNodeCatalog, cross-check yield itemId values against the already-loaded item catalog, and refuse to listen when any file is missing, malformed, or fails the prototype Slice 2 gate.
In scope (from Linear + E3M1-02):
- Loader + catalog types under
server/NeonSprawl.Server/Game/Gathering/. - Configurable resource-nodes directory and row schema paths (
ContentPathsOptionsextension). - CI-parity validation:
schemaVersion, row schemas, duplicatenodeDefId, frozen four ids, fourgatherLens, one yield per node, yieldnodeDefIdexists in nodes, yielditemIdin item catalog + Slice 2scrap_metal_bulk-only gate. - DI registration of
ResourceNodeCatalogsingleton (factory depends onItemDefinitionCatalog); eager resolve inProgram.csafter item catalog. - Unit + host startup tests (AAA).
Out of scope (from Linear):
IResourceNodeDefinitionRegistry/ResourceNodeDefinitionRegistry(NEO-59, E3M1-03).- Per-instance depletion, HTTP (
GET …/resource-node-definitionsis NEO-60), interact wiring, Bruno, client.
Acceptance criteria checklist
- Server refuses boot when resource-node catalog invalid (missing dir, no
*_resource_nodes.json/*_resource_yields.json, bad JSON,schemaVersion≠ 1, schema violation, duplicatenodeDefId, Slice 2 gate failure, unknown yielditemId, orphan yieldnodeDefId). ResourceNodeCatalogresolves node def and yield row bynodeDefId(e.g.prototype_resource_node_alpha→ lens +maxGathers+ yield quantity).- Unit tests (AAA): loader happy path + failure modes; host resolves catalog on success; host fails startup on invalid catalog directory.
- Success log includes node count, yield count, catalog directory, and file counts (Information).
Technical approach
-
Game/Gathering/row DTOs —ResourceNodeDefRow(NodeDefId,DisplayName,GatherLens,MaxGathers);ResourceYieldRow(NodeDefId,ItemId,Quantity). -
ResourceNodeCatalogLoader.Load(resourceNodesDirectory, nodeSchemaPath, yieldSchemaPath, itemIds, logger)— two-pass load mirroringvalidate_content.py:- Enumerate
*_resource_nodes.jsonthen*_resource_yields.json(top-level only), ordered by path. - Nodes pass: per file
schemaVersion == 1, top-levelnodesarray; per row validateresource-node-def.schema.json; tracknodeDefId,gatherLens; reject duplicatenodeDefIdacross files. - Run
PrototypeSlice2ResourceNodeCatalogRules.TryGetSlice2NodeGateError(mirrorPROTOTYPE_SLICE2_NODE_IDS,PROTOTYPE_SLICE2_GATHER_LENSES). - Yields pass: per file
schemaVersion == 1, top-levelyieldsarray; per row validateresource-yield-row.schema.json; reject duplicate yieldnodeDefId; require yieldnodeDefIdin loaded nodes; requireitemId∈itemIds; run Slice 2 yield item gate (scrap_metal_bulkonly); require exactly one yield per frozennodeDefId. - Throw
InvalidOperationExceptionwith aggregated messages prefixedResource node catalog validation failed:.
- Enumerate
-
ResourceNodeCatalog— immutable snapshot:ResourceNodesDirectory,NodesById,YieldsByNodeDefId, counts;TryGetNode(nodeDefId, out ResourceNodeDefRow?),TryGetYield(nodeDefId, out ResourceYieldRow?). -
ResourceNodeCatalogPathResolution—TryDiscoverResourceNodesDirectory,ResolveResourceNodesDirectory,ResolveResourceNodeDefSchemaPath,ResolveResourceYieldRowSchemaPath(parentcontent/schemas/layout when unset). -
PrototypeSlice2ResourceNodeCatalogRules— frozen fournodeDefIds, fourgatherLens, yield item allowlist; sync comments toscripts/validate_content.pyPROTOTYPE_SLICE2_*. -
ResourceNodeCatalogServiceCollectionExtensions.AddResourceNodeCatalog— bindContentPathsOptions; registerResourceNodeCatalogsingleton factory that resolves paths, getsItemDefinitionCatalog.ById.KeysasFrozenSet<string>, calls loader. -
Program.cs—AddResourceNodeCatalog(configuration)afterAddItemDefinitionCatalog; afterBuild(),GetRequiredService<ResourceNodeCatalog>()alongside existing catalog eager resolves. -
Tests — temp-dir fixtures copying repo schemas; valid four-node + four-yield JSON matching prototype_resource_nodes.json; six-item item id set stub for cross-check; negative cases (duplicate id, missing yield, bad lens set, unknown
itemId, invalid directory);InMemoryWebApplicationFactory/WebApplicationFactoryhost tests mirroring items.
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefRow.cs |
Load-time node row DTO. |
server/NeonSprawl.Server/Game/Gathering/ResourceYieldRow.cs |
Load-time yield row DTO. |
server/NeonSprawl.Server/Game/Gathering/ResourceNodeCatalog.cs |
In-memory catalog + TryGetNode / TryGetYield. |
server/NeonSprawl.Server/Game/Gathering/ResourceNodeCatalogLoader.cs |
Disk I/O, schema validation, Slice 2 gates, item cross-check. |
server/NeonSprawl.Server/Game/Gathering/ResourceNodeCatalogPathResolution.cs |
Directory/schema discovery and config resolution. |
server/NeonSprawl.Server/Game/Gathering/PrototypeSlice2ResourceNodeCatalogRules.cs |
Frozen four ids / four lenses / yield item gate (sync comment to Python). |
server/NeonSprawl.Server/Game/Gathering/ResourceNodeCatalogServiceCollectionExtensions.cs |
AddResourceNodeCatalog DI registration. |
server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeCatalogTestPaths.cs |
Repo content/resource-nodes + schema discovery for tests. |
server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeCatalogLoaderTests.cs |
AAA loader + host startup tests. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs |
Add ResourceNodesDirectory, ResourceNodeDefSchemaPath, ResourceYieldRowSchemaPath under Content section. |
server/NeonSprawl.Server/Program.cs |
Register and eagerly resolve ResourceNodeCatalog at startup (after item catalog). |
server/README.md |
Document resource-node catalog load, config keys, fail-fast behavior, Slice 2 parity with CI. |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E3.M1 / NEO-58 server loader status when implementation completes. |
Tests
| Item | Coverage |
|---|---|
server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeCatalogLoaderTests.cs |
Add — AAA: Load happy path with repo-shaped JSON; failures for missing dir/schema, bad schemaVersion, schema violations, duplicate nodeDefId, incomplete Slice 2 ids, lens set mismatch, orphan yield nodeDefId, unknown itemId, duplicate yield nodeDefId, missing yield for node; Host_ShouldResolveCatalogFromDi; Host_ShouldFailStartup_WhenCatalogDirectoryInvalid. |
server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs |
Change if needed — ensure resource-node content is discoverable in test host (same ancestor walk as items). |
| CI / PR gate | Existing scripts/validate_content.py remains content regression; server tests guard C#/Python drift for Slice 2 constants. |
No Bruno or client tests (out of scope).
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
| Stacked PR on NEO-57 | NEO-57 merged (PR #92); NEO-58 rebased onto main. |
adopted |
| C# vs Python drift | Duplicate PROTOTYPE_SLICE2_* in PrototypeSlice2ResourceNodeCatalogRules with “keep in sync” comment (NEO-50 / NEO-57 pattern). |
adopted |
| Test factories without NEO-57 on main | NEO-58 branch includes NEO-57 content via stack; after NEO-57 merges, rebase drops dependency. | adopted |
| Registry consumers | Gather engine / HTTP use IResourceNodeDefinitionRegistry in NEO-59+; this story only exposes ResourceNodeCatalog for boot + direct test lookup. |
adopted |
Decisions (kickoff)
- Branch from NEO-57 for initial stacked PR; rebased onto
mainafter NEO-57 merged (PR #92). ResourceNodeCatalogsingleton only — deferIResourceNodeDefinitionRegistryto NEO-59.- Yield
itemIdcross-check usesItemDefinitionCatalogkeys passed into loader at DI registration time. - In-process validation + loader and host boot tests per NEO-51 precedent.