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

9.4 KiB

NEO-51 — Implementation plan

Story reference

Field Value
Key NEO-51
Title E3.M3: Server loads item catalog at startup (fail-fast)
Linear https://linear.app/neon-sprawl/issue/NEO-51/e3m3-server-loads-item-catalog-at-startup-fail-fast
Module E3.M3 — ItemizationAndInventorySchema · Epic 3 Slice 1 (E3M3-02)
Branch NEO-51-server-item-catalog-load-fail-fast
Precursor NEO-50 — frozen six-item catalog + CI (Done on main)

Kickoff clarifications

Topic Question Agent recommendation Answer
NEO-51 vs NEO-52 Ship IItemDefinitionRegistry + ItemDefinitionRegistry on this story, or only load + ItemDefinitionCatalog? Strict split — NEO-51: loader + ItemDefinitionCatalog + eager fail-fast boot; NEO-52 adds injectable IItemDefinitionRegistry (E3M3-03). Rationale: E3M3 backlog separates E3M3-02/03; skills combined registry in NEO-34 but items already have a dedicated registry ticket. User: strict split (kickoff re-ask).
Startup failure tests Loader unit tests only, or also WebApplicationFactory host boot failure? Both — mirror NEO-34 (SkillDefinitionCatalogLoaderTests happy path, failure modes, Host_ShouldResolveCatalogFromDi, Host_ShouldFailStartup_WhenCatalogDirectoryInvalid). User: both loader + host boot tests (kickoff re-ask).
Runtime validation In-process C# vs subprocess validate_content.py? In-process C# mirroring scripts/validate_content.py (Draft 2020-12 + Slice 1 item gate). Same rationale as NEO-34 (no Python on server images; shared constants commented against Python). Adopted — NEO-34 precedent; no separate question needed.
Options location New ItemCatalogOptions vs extend ContentPathsOptions? Extend ContentPathsOptions with ItemsDirectory / ItemDefSchemaPath (skills + mastery already share Content section). Adopted
Lookup API on NEO-51 How does “registry resolves ItemDef by id” land without NEO-52? ItemDefinitionCatalog.ById (read-only map) + optional TryGetItem(string id, out ItemDefRow row) on the catalog type; no IItemDefinitionRegistry interface yet. Adopted

Goal, scope, and out-of-scope

Goal: On host startup, load all content/items/*_items.json catalogs (validated in CI per NEO-50), build an in-memory ItemDefinitionCatalog, log item count and resolved paths, and refuse to listen when any file is missing, malformed, has duplicate id, or fails the prototype Slice 1 gate. Mirror E2.M1 / NEO-34 skill-catalog behavior.

In scope (from Linear + E3M3-02):

  • Loader + catalog under server/NeonSprawl.Server/Game/Items/.
  • Configurable items directory and schema path (auto-discovery + overrides).
  • Fail-fast semantics aligned with scripts/validate_content.py item validation (schemaVersion, row schema, duplicate id, frozen six ids, prototypeRole coverage, equip_stub → equipment + stackMax 1).
  • DI registration of ItemDefinitionCatalog singleton; eager resolve in Program.cs before Run().
  • Unit + host startup tests (AAA).

Out of scope (from Linear):

  • Per-player inventory, HTTP (NEO-53+).
  • IItemDefinitionRegistry / ItemDefinitionRegistry (NEO-52).

Acceptance criteria checklist

  • Server refuses boot when item catalog invalid (missing dir, no *_items.json, bad JSON, schemaVersion ≠ 1, schema violation, duplicate id, Slice 1 gate failure).
  • ItemDefinitionCatalog resolves prototype rows by id (e.g. scrap_metal_bulk, prototype_armor_shell).
  • Unit tests (AAA): loader happy path + failure modes; host resolves catalog on success; host fails startup on invalid catalog directory.
  • Success log includes item count, catalog directory, and file count (Information).

Technical approach

  1. Game/Items/ loaderItemDefinitionCatalogLoader.Load(itemsDirectory, schemaPath, logger):
    • Enumerate *_items.json (top-level only), ordered by path.
    • Per file: parse JSON; require schemaVersion == 1 and top-level items array.
    • Per row: validate with item-def.schema.json via existing JsonSchema.Net; on clean rows, track id, prototypeRole, inventorySlotKind, stackMax for gates; build ItemDefRow dictionary.
    • Reject duplicate id across files with paths in the error message.
    • Run PrototypeSlice1ItemCatalogRules.TryGetSlice1GateError (mirror PROTOTYPE_SLICE1_ITEM_IDS, PROTOTYPE_SLICE1_ITEM_ROLES, equip_stub rules in validate_content.py).
    • Throw InvalidOperationException with aggregated, sorted messages prefixed Item catalog validation failed:.
  2. ItemDefinitionCatalog — immutable snapshot: ItemsDirectory, ById, DistinctItemCount, CatalogJsonFileCount; TryGetItem(id, out ItemDefRow?) for lookups.
  3. ItemCatalogPathResolutionTryDiscoverItemsDirectory, ResolveItemsDirectory, ResolveItemDefSchemaPath (parallel to skill path resolution: parent schemas/item-def.schema.json when schema path unset).
  4. ItemCatalogServiceCollectionExtensions.AddItemDefinitionCatalog — bind ContentPathsOptions; register ItemDefinitionCatalog singleton factory (load on first resolve / same pattern as skills).
  5. Program.csAddItemDefinitionCatalog(configuration); after Build(), GetRequiredService<ItemDefinitionCatalog>() before routes/Run().
  6. Tests — temp-dir fixtures copying repo item-def.schema.json; valid six-item JSON matching prototype_items.json; negative cases for each failure class; InMemoryWebApplicationFactory sets Content:ItemsDirectory to discovered repo path; host failure uses empty temp dir like skills test.

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Items/ItemDefRow.cs Load-time row DTO (Id, DisplayName, PrototypeRole, StackMax, InventorySlotKind, optional forward-compat fields).
server/NeonSprawl.Server/Game/Items/ItemDefinitionCatalog.cs In-memory catalog + ById + TryGetItem.
server/NeonSprawl.Server/Game/Items/ItemDefinitionCatalogLoader.cs Disk I/O, schema validation, Slice 1 gate, logging.
server/NeonSprawl.Server/Game/Items/ItemCatalogPathResolution.cs Directory/schema discovery and config resolution.
server/NeonSprawl.Server/Game/Items/PrototypeSlice1ItemCatalogRules.cs Frozen six ids / six roles + equip_stub slot rules (sync comment to Python).
server/NeonSprawl.Server/Game/Items/ItemCatalogServiceCollectionExtensions.cs AddItemDefinitionCatalog DI registration.
server/NeonSprawl.Server.Tests/Game/Items/ItemCatalogTestPaths.cs Repo content/items + schema discovery for tests.
server/NeonSprawl.Server.Tests/Game/Items/ItemDefinitionCatalogLoaderTests.cs AAA loader + host startup tests (mirror SkillDefinitionCatalogLoaderTests).

Files to modify

Path Rationale
server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs Add ItemsDirectory, ItemDefSchemaPath under existing Content section.
server/NeonSprawl.Server/Program.cs Register item catalog; eager-resolve after build.
server/NeonSprawl.Server/appsettings.json Optional empty Content:ItemsDirectory / ItemDefSchemaPath keys for override documentation.
server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs Pin Content:ItemsDirectory to repo catalog so existing tests keep booting.
server/README.md New Item catalog (content/items, NEO-51) section (config keys, discovery, fail-fast).
docs/decomposition/modules/documentation_and_implementation_alignment.md E3.M3 row — note NEO-51 server load in progress / landed when done.

Tests

Test file What it covers
server/NeonSprawl.Server.Tests/Game/Items/ItemDefinitionCatalogLoaderTests.cs Unit: valid six-item prototype catalog loads; items not array; schema violation; duplicate id across files; wrong/missing Slice 1 ids; wrong prototypeRole set; equip_stub not equipment or stackMax ≠ 1; schemaVersion ≠ 1. Host: InMemoryWebApplicationFactory + /health + DI ItemDefinitionCatalog count 6; WebApplicationFactory with empty items dir fails startup with actionable message.

Open questions / risks

Question / risk Agent recommendation Status
Schema path when only ItemsDirectory is set Resolve {itemsDir}/../schemas/item-def.schema.json (same rule as skills/mastery). Document in README + ItemCatalogPathResolution. adopted
Test cwd InMemoryWebApplicationFactory must set Content:ItemsDirectory explicitly (do not rely on discovery alone in CI). adopted
Constants drift vs Python Comment on PrototypeSlice1ItemCatalogRules pointing at scripts/validate_content.py PROTOTYPE_SLICE1_ITEM_IDS / _prototype_slice1_item_gate. adopted

None blocking beyond the above.