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? |
Omit — RecipeDefinitionCatalog 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):
RecipeDefinitionRegistrythin adapter overRecipeDefinitionCatalog.- DI registration alongside existing catalog singleton.
- Unit tests (AAA): known prototype id lookup +
recipeKind+ I/O metadata; unknownrecipeIdreturns 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
IRecipeDefinitionRegistryat host startup (test viaInMemoryWebApplicationFactory). - Unit tests (AAA): lookup for known prototype
recipeId(e.g.refine_scrap_standard,make_field_stim_mk0) with expectedrecipeKind,requiredSkillId, and input/output rows. - Unit tests (AAA): unknown
recipeIdreturns false / absent without throwing. GetDefinitionsInIdOrderreturns all eight loaded rows ordered byid(ordinal).
Technical approach
-
IRecipeDefinitionRegistry— mirrorIItemDefinitionRegistry:TryGetDefinition(string? recipeId, [NotNullWhen(true)] out RecipeDefRow? definition)— null and unknown ids return false without throwing.GetDefinitionsInIdOrder()— all rows ordered byRecipeDefRow.Id(ordinal).- XML remarks: craft engine / inventory callers (NEO-69+) should use this interface; HTTP read model (NEO-68) should not reach into
RecipeDefinitionCatalog.
-
RecipeDefinitionRegistry— primary-constructor adapter overRecipeDefinitionCatalog(same pattern asItemDefinitionRegistry). -
DI — extend
AddRecipeDefinitionCatalogto registerIRecipeDefinitionRegistry→RecipeDefinitionRegistryafter catalog registration. No change toProgram.cseager-resolve (catalog only). -
Comments — update
RecipeDefinitionCatalogsummary to point atIRecipeDefinitionRegistry; update server README recipe-catalog section (currently says “until NEO-67”). -
Tests — new
RecipeDefinitionRegistryTests.csmirroringItemDefinitionRegistryTests: in-memory catalog helper, loader-backed prototype fixture viaRecipeCatalogTestPaths+ 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 IRecipeDefinitionRegistry → RecipeDefinitionRegistry 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).TryGetDefinitionnaming — consistent with item/skill registries; parameter isrecipeId(user confirmed).- No
Program.csregistry eager-resolve — catalog fail-fast only (NEO-52 pattern). - I/O and
recipeKindonRecipeDefRow— single lookup surface; no separate TryGet methods.