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

8.8 KiB

NEO-67 — Implementation plan

Story reference

Field Value
Key NEO-67
Title E3.M2: Recipe definition registry + DI
Linear https://linear.app/neon-sprawl/issue/NEO-67/e3m2-recipe-definition-registry-di
Module E3.M2 — RefinementAndRecipeExecution · Epic 3 Slice 3 · backlog E3M2-03
Branch NEO-67-recipe-definition-registry-di
Precursor NEO-66 — fail-fast RecipeDefinitionCatalog load (Done on main)
Pattern NEO-52 / NEO-59 — thin registry adapter over startup catalog + DI; strict split after NEO-66 loader
Blocks NEO-68 — GET world recipe-definitions; NEO-69 — CraftResult engine

Kickoff clarifications

Topic Question Agent recommendation Answer
Enumeration API Include GetDefinitionsInIdOrder() now? Yes — mirror IItemDefinitionRegistry / ISkillDefinitionRegistry; NEO-68 GET /game/world/recipe-definitions will need ordered defs without reaching into RecipeDefinitionCatalog. User: include enumeration.
Lookup naming TryGetDefinition vs TryGetRecipe? TryGetDefinition(string? recipeId, …) — same naming as item/skill registries; catalog keeps TryGetRecipe for direct catalog access. User: TryGetDefinition.
NEO-66 vs NEO-67 split Combine registry with catalog load? Strict split (decided on NEO-66 kickoff) — NEO-66 ships loader + catalog; this story adds injectable registry only. Adopted — NEO-66 plan.
Program.cs eager resolve Eager-resolve IRecipeDefinitionRegistry at boot? OmitRecipeDefinitionCatalog is already eager-resolved in Program.cs (NEO-66); registry is a thin adapter (NEO-52 default). Adopted
I/O + recipeKind exposure Separate TryGet methods for inputs/outputs? No — embedded on RecipeDefRow returned by TryGetDefinition; backlog tests cover metadata + recipeKind via one lookup (unlike node+yield on NEO-59). Adopted
Runtime validation Re-validate recipes in registry? No — catalog load is fail-fast (NEO-66); registry delegates to loaded rows only. Adopted

Goal, scope, and out-of-scope

Goal: Provide IRecipeDefinitionRegistry backed by the startup-loaded RecipeDefinitionCatalog: resolve by stable recipeId, enumerate definitions in id order, expose prototype metadata (displayName, recipeKind, requiredSkillId, embedded inputs / outputs). Register in DI so NEO-68+ (HTTP), NEO-69 (craft engine), and future callers depend on the interface instead of the catalog type.

In scope (from Linear + E3M2-03):

  • RecipeDefinitionRegistry thin adapter over RecipeDefinitionCatalog.
  • DI registration alongside existing catalog singleton.
  • Unit tests (AAA): known prototype id lookup + recipeKind + I/O metadata; unknown recipeId returns false without throwing; enumeration order; host resolves registry from DI.

Out of scope (from Linear + backlog):

  • HTTP (NEO-68).
  • Persistence, per-player craft state, inventory mutation (NEO-69+).
  • Changing loader, Slice 3 gate, or catalog load semantics (NEO-66).

Acceptance criteria checklist

  • DI resolves IRecipeDefinitionRegistry at host startup (test via InMemoryWebApplicationFactory).
  • Unit tests (AAA): lookup for known prototype recipeId (e.g. refine_scrap_standard, make_field_stim_mk0) with expected recipeKind, requiredSkillId, and input/output rows.
  • Unit tests (AAA): unknown recipeId returns false / absent without throwing.
  • GetDefinitionsInIdOrder returns all eight loaded rows ordered by id (ordinal).

Technical approach

  1. IRecipeDefinitionRegistry — mirror IItemDefinitionRegistry:

    • TryGetDefinition(string? recipeId, [NotNullWhen(true)] out RecipeDefRow? definition) — null and unknown ids return false without throwing.
    • GetDefinitionsInIdOrder() — all rows ordered by RecipeDefRow.Id (ordinal).
    • XML remarks: craft engine / inventory callers (NEO-69+) should use this interface; HTTP read model (NEO-68) should not reach into RecipeDefinitionCatalog.
  2. RecipeDefinitionRegistry — primary-constructor adapter over RecipeDefinitionCatalog (same pattern as ItemDefinitionRegistry).

  3. DI — extend AddRecipeDefinitionCatalog to register IRecipeDefinitionRegistryRecipeDefinitionRegistry after catalog registration. No change to Program.cs eager-resolve (catalog only).

  4. Comments — update RecipeDefinitionCatalog summary to point at IRecipeDefinitionRegistry; update server README recipe-catalog section (currently says “until NEO-67”).

  5. Tests — new RecipeDefinitionRegistryTests.cs mirroring ItemDefinitionRegistryTests: in-memory catalog helper, loader-backed prototype fixture via RecipeCatalogTestPaths + item/skill id stubs, host DI test.

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Crafting/IRecipeDefinitionRegistry.cs Public contract: lookup, enumerate, remarks for NEO-68/69 callers.
server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionRegistry.cs Singleton adapter over RecipeDefinitionCatalog.
server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionRegistryTests.cs AAA unit + host DI tests (mirror ItemDefinitionRegistryTests).
docs/plans/NEO-67-implementation-plan.md This plan.

Files to modify

Path Rationale
server/NeonSprawl.Server/Game/Crafting/RecipeCatalogServiceCollectionExtensions.cs Register IRecipeDefinitionRegistryRecipeDefinitionRegistry after catalog singleton.
server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionCatalog.cs Tighten summary comment to reference IRecipeDefinitionRegistry (replace “NEO-67 adds…” placeholder).
server/README.md Recipe catalog section: document IRecipeDefinitionRegistry as preferred lookup surface.
docs/decomposition/modules/documentation_and_implementation_alignment.md E3.M2 row — note NEO-67 registry when landed.

Tests

Test file What it covers
server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionRegistryTests.cs Unit: TryGetDefinition for refine_scrap_standard returns true with RecipeKind process, RequiredSkillId refine, expected input/output ItemId/Quantity; make_field_stim_mk0 returns make with multi-input rows; null and unknown id return false without throw; GetDefinitionsInIdOrder count (8) and ordinal order for prototype fixture; loader-backed prototype fixture matches catalog. Host: InMemoryWebApplicationFactory resolves IRecipeDefinitionRegistry and finds refine_scrap_standard.

Manual QA: Skipped — no player-visible HTTP; acceptance is fully covered by AAA unit/host tests (same as NEO-52 / NEO-59).

Open questions / risks

Question / risk Agent recommendation Status
Catalog vs registry for game code New callers inject IRecipeDefinitionRegistry; leave RecipeDefinitionCatalog eager-resolve in Program.cs for fail-fast only. adopted
Duplicate API on catalog Keep RecipeDefinitionCatalog.TryGetRecipe for loader/tests; registry is the game-facing surface (NEO-52 precedent). adopted
NEO-68 HTTP projection API layer maps RecipeDefRow fields to DTOs via registry enumeration + lookup; no combined read model in NEO-67. adopted

Decisions (kickoff)

  • GetDefinitionsInIdOrder() included — enumeration for NEO-68 HTTP without catalog coupling (user confirmed).
  • TryGetDefinition naming — consistent with item/skill registries; parameter is recipeId (user confirmed).
  • No Program.cs registry eager-resolve — catalog fail-fast only (NEO-52 pattern).
  • I/O and recipeKind on RecipeDefRow — single lookup surface; no separate TryGet methods.