12 KiB
NEO-66 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-66 |
| Title | E3.M2: Server recipe catalog load (fail-fast) |
| Linear | https://linear.app/neon-sprawl/issue/NEO-66/e3m2-server-recipe-catalog-load-fail-fast |
| Module | E3.M2 — RefinementAndRecipeExecution · Epic 3 Slice 3 · backlog E3M2-02 |
| Branch | NEO-66-e3m2-server-recipe-catalog-load-fail-fast |
| Precursor | NEO-65 — frozen eight-recipe catalog + CI gates (Done on main) |
| Pattern | NEO-51 — fail-fast catalog loader + ContentPathsOptions + eager Program.cs resolve; strict split before registry ticket (NEO-67) |
| Blocks | NEO-67 — recipe definition registry + DI |
Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|---|---|---|---|
| NEO-66 vs NEO-67 (DI scope) | Ship IRecipeDefinitionRegistry on this story, or only load + RecipeDefinitionCatalog? |
Strict split — NEO-66: loader + RecipeDefinitionCatalog + eager fail-fast boot; NEO-67 adds injectable IRecipeDefinitionRegistry (E3M2-03). Rationale: E3M2 backlog separates E3M2-02/03; mirrors NEO-51/NEO-52 and NEO-58/NEO-59. |
User: RecipeDefinitionCatalog singleton only (kickoff). |
| Cross-check dependencies | How to validate I/O itemId and requiredSkillId at startup? |
Pass ids from ItemDefinitionCatalog and SkillDefinitionCatalog in the DI factory (items + skills load first; no second disk read). Rationale: NEO-58 / NEO-46 load-time precedent. |
Adopted — NEO-58 precedent; no separate question. |
| Runtime validation | In-process C# vs subprocess validate_content.py? |
In-process C# mirroring scripts/validate_content.py (Draft 2020-12 + Slice 3 recipe gate). |
Adopted — NEO-51 / NEO-34 precedent. |
| Startup failure tests | Loader unit tests only, or also WebApplicationFactory host boot failure? |
Both — mirror ItemDefinitionCatalogLoaderTests / ResourceNodeCatalogLoaderTests. |
Adopted — NEO-51 precedent. |
| Lookup API on NEO-66 | How does “catalog resolves RecipeDef and I/O rows by recipe id” land without NEO-67? |
RecipeDefinitionCatalog.ById (read-only map) + TryGetRecipe(string id, out RecipeDefRow row); inputs/outputs embedded on RecipeDefRow. |
Adopted |
Goal, scope, and out-of-scope
Goal: On host startup, load all content/recipes/*_recipes.json catalogs (validated in CI per NEO-65), build an in-memory RecipeDefinitionCatalog, cross-check every I/O itemId against the already-loaded item catalog and every requiredSkillId against the skill catalog, and refuse to listen when any file is missing, malformed, has duplicate id, or fails the prototype Slice 3 gate.
In scope (from Linear + E3M2-02):
- Loader + catalog types under
server/NeonSprawl.Server/Game/Crafting/. - Configurable recipes directory and schema paths (
ContentPathsOptionsextension). - CI-parity validation:
schemaVersion, row schemas (recipe-def+$reftorecipe-io-row), duplicateid, cross-check item + skill ids, Slice 3 eight-id +recipeKind+refinegate. - DI registration of
RecipeDefinitionCatalogsingleton (factory depends onItemDefinitionCatalog+SkillDefinitionCatalog); eager resolve inProgram.csafter item + skill catalogs. - Unit + host startup tests (AAA).
Out of scope (from Linear):
- Per-player craft state, HTTP, inventory mutation.
IRecipeDefinitionRegistry/RecipeDefinitionRegistry(NEO-67, E3M2-03).
Acceptance criteria checklist
- Server refuses boot when recipe catalog invalid (missing dir, no
*_recipes.json, bad JSON,schemaVersion≠ 1, schema violation, duplicateid, unknownitemId/requiredSkillId, Slice 3 gate failure). RecipeDefinitionCatalogresolves prototype rows by recipeid(e.g.refine_scrap_standard,make_field_stim_mk0) including embedded input/output rows.- Unit tests (AAA): loader happy path + failure modes; host resolves catalog on success; host fails startup on invalid catalog directory.
- Success log includes recipe count, catalog directory, and file count (Information).
Technical approach
-
Game/Crafting/row DTOs —RecipeIoRow(ItemId,Quantity);RecipeDefRow(Id,DisplayName,RecipeKind,RequiredSkillId,Inputs,Outputs, optionalRequiredSkillLevel,StationTag). -
RecipeDefinitionCatalogLoader.Load(recipesDirectory, recipeDefSchemaPath, recipeIoRowSchemaPath, knownItemIds, knownSkillIds, logger)— single-pass load mirroringvalidate_content.py_validate_recipe_catalogs+_prototype_slice3_recipe_gate:- Enumerate
*_recipes.json(top-level only), ordered by path. - Build JsonSchema.Net evaluator with both schemas registered by
$id(same$refURLs as Python:https://neon-sprawl.local/schemas/recipe-def.json+recipe-io-row.json). - Per file: parse JSON; require
schemaVersion == 1and top-levelrecipesarray. - Per row: validate against composite recipe-def schema; on clean rows track
id,recipeKind,requiredSkillId; buildRecipeDefRowwith parsed I/O arrays; reject duplicateidacross files. - Post-schema cross-checks per row: every I/O
itemId∈knownItemIds;requiredSkillId∈knownSkillIds. - Run
PrototypeSlice3RecipeCatalogRules.TryGetSlice3GateError(mirrorPROTOTYPE_SLICE3_RECIPE_IDS,PROTOTYPE_SLICE3_REFINE_SKILL_ID,process+makecoverage invalidate_content.py). - Throw
InvalidOperationExceptionwith aggregated, sorted messages prefixedRecipe catalog validation failed:.
- Enumerate
-
RecipeDefinitionCatalog— immutable snapshot:RecipesDirectory,ById,DistinctRecipeCount,CatalogJsonFileCount;TryGetRecipe(id, out RecipeDefRow?)for lookups (I/O rows on the row DTO). -
RecipeCatalogPathResolution—TryDiscoverRecipesDirectory,ResolveRecipesDirectory,ResolveRecipeDefSchemaPath,ResolveRecipeIoRowSchemaPath(parentcontent/schemas/layout when unset). -
PrototypeSlice3RecipeCatalogRules— frozen eight recipe ids + refine skill id +recipeKindcoverage gate; sync comments toscripts/validate_content.pyPROTOTYPE_SLICE3_RECIPE_IDS/PROTOTYPE_SLICE3_REFINE_SKILL_ID/_prototype_slice3_recipe_gate. -
RecipeCatalogServiceCollectionExtensions.AddRecipeDefinitionCatalog— bindContentPathsOptions; registerRecipeDefinitionCatalogsingleton factory that resolves paths, getsItemDefinitionCatalog.ById.KeysandSkillDefinitionCatalog.ById.Keysas hash sets, calls loader. -
Program.cs—AddRecipeDefinitionCatalog(configuration)afterAddItemDefinitionCatalogandAddSkillDefinitionCatalog; afterBuild(),GetRequiredService<RecipeDefinitionCatalog>()alongside existing catalog eager resolves. -
Tests — temp-dir fixtures copying repo
recipe-def.schema.json+recipe-io-row.schema.json; valid eight-recipe JSON matching prototype_recipes.json; stub item + skill id sets for cross-check; negative cases (duplicate id, unknown item/skill, wrong Slice 3 roster, missingprocess/make, badrequiredSkillId);InMemoryWebApplicationFactory/WebApplicationFactoryhost tests mirroring items.
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Crafting/RecipeIoRow.cs |
Load-time recipe input/output row DTO. |
server/NeonSprawl.Server/Game/Crafting/RecipeDefRow.cs |
Load-time RecipeDef row DTO with embedded I/O lists. |
server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionCatalog.cs |
In-memory catalog + TryGetRecipe. |
server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionCatalogLoader.cs |
Disk I/O, schema validation ($ref), Slice 3 gate, item + skill cross-check. |
server/NeonSprawl.Server/Game/Crafting/RecipeCatalogPathResolution.cs |
Directory/schema discovery and config resolution. |
server/NeonSprawl.Server/Game/Crafting/PrototypeSlice3RecipeCatalogRules.cs |
Frozen eight ids + refine skill + recipeKind gate (sync comment to Python). |
server/NeonSprawl.Server/Game/Crafting/RecipeCatalogServiceCollectionExtensions.cs |
AddRecipeDefinitionCatalog DI registration. |
server/NeonSprawl.Server.Tests/Game/Crafting/RecipeCatalogTestPaths.cs |
Repo content/recipes + schema discovery for tests. |
server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionCatalogLoaderTests.cs |
AAA loader + host startup tests. |
docs/plans/NEO-66-implementation-plan.md |
This plan. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs |
Add RecipesDirectory, RecipeDefSchemaPath, RecipeIoRowSchemaPath under Content section. |
server/NeonSprawl.Server/Program.cs |
Register and eagerly resolve RecipeDefinitionCatalog at startup (after item + skill catalogs). |
server/NeonSprawl.Server/appsettings.json |
Optional empty Content:RecipesDirectory / schema path keys for override documentation (if other catalogs document keys here). |
server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs |
Pin Content:RecipesDirectory to discovered repo path so existing tests keep booting. |
server/README.md |
New Recipe catalog (content/recipes, NEO-66) section (config keys, discovery, fail-fast, Slice 3 parity with CI). |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E3.M2 row — note NEO-66 server load when implementation completes. |
docs/plans/E3M2-prototype-backlog.md |
E3M2-02 checkboxes when implementation completes. |
Tests
| Test file | What it covers |
|---|---|
server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionCatalogLoaderTests.cs |
Unit: valid eight-recipe prototype catalog loads; recipes not array; schema violation (incl. bad I/O row); duplicate id across files; wrong/missing Slice 3 ids; missing process or make; requiredSkillId ≠ refine; unknown itemId in inputs/outputs; unknown requiredSkillId; schemaVersion ≠ 1; missing dir/schema. Host: InMemoryWebApplicationFactory + /health + DI RecipeDefinitionCatalog count 8; WebApplicationFactory with empty recipes dir fails startup with actionable message. |
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
Schema $ref in C# |
Register both recipe schemas by $id in JsonSchema.Net before evaluating rows (mirror Python _recipe_def_validator). |
adopted |
| Constants drift vs Python | Comment on PrototypeSlice3RecipeCatalogRules pointing at scripts/validate_content.py PROTOTYPE_SLICE3_RECIPE_IDS / _prototype_slice3_recipe_gate. |
adopted |
| Test cwd | InMemoryWebApplicationFactory must set Content:RecipesDirectory explicitly (do not rely on discovery alone in CI). |
adopted |
| Registry consumers | Craft engine / HTTP use IRecipeDefinitionRegistry in NEO-67+; this story only exposes RecipeDefinitionCatalog for boot + direct test lookup. |
adopted |
Decisions (kickoff)
RecipeDefinitionCatalogsingleton only — deferIRecipeDefinitionRegistryto NEO-67 (user confirmed kickoff).- Item + skill cross-check uses
ItemDefinitionCatalogandSkillDefinitionCatalogkeys passed into loader at DI registration time. - In-process validation + loader and host boot tests per NEO-51 / NEO-58 precedent.