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

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 (ContentPathsOptions extension).
  • CI-parity validation: schemaVersion, row schemas (recipe-def + $ref to recipe-io-row), duplicate id, cross-check item + skill ids, Slice 3 eight-id + recipeKind + refine gate.
  • DI registration of RecipeDefinitionCatalog singleton (factory depends on ItemDefinitionCatalog + SkillDefinitionCatalog); eager resolve in Program.cs after 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, duplicate id, unknown itemId / requiredSkillId, Slice 3 gate failure).
  • RecipeDefinitionCatalog resolves prototype rows by recipe id (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

  1. Game/Crafting/ row DTOsRecipeIoRow (ItemId, Quantity); RecipeDefRow (Id, DisplayName, RecipeKind, RequiredSkillId, Inputs, Outputs, optional RequiredSkillLevel, StationTag).

  2. RecipeDefinitionCatalogLoader.Load(recipesDirectory, recipeDefSchemaPath, recipeIoRowSchemaPath, knownItemIds, knownSkillIds, logger) — single-pass load mirroring validate_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 $ref URLs as Python: https://neon-sprawl.local/schemas/recipe-def.json + recipe-io-row.json).
    • Per file: parse JSON; require schemaVersion == 1 and top-level recipes array.
    • Per row: validate against composite recipe-def schema; on clean rows track id, recipeKind, requiredSkillId; build RecipeDefRow with parsed I/O arrays; reject duplicate id across files.
    • Post-schema cross-checks per row: every I/O itemIdknownItemIds; requiredSkillIdknownSkillIds.
    • Run PrototypeSlice3RecipeCatalogRules.TryGetSlice3GateError (mirror PROTOTYPE_SLICE3_RECIPE_IDS, PROTOTYPE_SLICE3_REFINE_SKILL_ID, process + make coverage in validate_content.py).
    • Throw InvalidOperationException with aggregated, sorted messages prefixed Recipe catalog validation failed:.
  3. RecipeDefinitionCatalog — immutable snapshot: RecipesDirectory, ById, DistinctRecipeCount, CatalogJsonFileCount; TryGetRecipe(id, out RecipeDefRow?) for lookups (I/O rows on the row DTO).

  4. RecipeCatalogPathResolutionTryDiscoverRecipesDirectory, ResolveRecipesDirectory, ResolveRecipeDefSchemaPath, ResolveRecipeIoRowSchemaPath (parent content/schemas/ layout when unset).

  5. PrototypeSlice3RecipeCatalogRules — frozen eight recipe ids + refine skill id + recipeKind coverage gate; sync comments to scripts/validate_content.py PROTOTYPE_SLICE3_RECIPE_IDS / PROTOTYPE_SLICE3_REFINE_SKILL_ID / _prototype_slice3_recipe_gate.

  6. RecipeCatalogServiceCollectionExtensions.AddRecipeDefinitionCatalog — bind ContentPathsOptions; register RecipeDefinitionCatalog singleton factory that resolves paths, gets ItemDefinitionCatalog.ById.Keys and SkillDefinitionCatalog.ById.Keys as hash sets, calls loader.

  7. Program.csAddRecipeDefinitionCatalog(configuration) after AddItemDefinitionCatalog and AddSkillDefinitionCatalog; after Build(), GetRequiredService<RecipeDefinitionCatalog>() alongside existing catalog eager resolves.

  8. 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, missing process/make, bad requiredSkillId); InMemoryWebApplicationFactory / WebApplicationFactory host 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; requiredSkillIdrefine; 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)

  • RecipeDefinitionCatalog singleton only — defer IRecipeDefinitionRegistry to NEO-67 (user confirmed kickoff).
  • Item + skill cross-check uses ItemDefinitionCatalog and SkillDefinitionCatalog keys passed into loader at DI registration time.
  • In-process validation + loader and host boot tests per NEO-51 / NEO-58 precedent.