diff --git a/docs/plans/NEO-51-implementation-plan.md b/docs/plans/NEO-51-implementation-plan.md new file mode 100644 index 0000000..7c308e7 --- /dev/null +++ b/docs/plans/NEO-51-implementation-plan.md @@ -0,0 +1,101 @@ +# 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](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) · Epic 3 Slice 1 (E3M3-02) | +| **Branch** | `NEO-51-server-item-catalog-load-fail-fast` | +| **Precursor** | [NEO-50](https://linear.app/neon-sprawl/issue/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](https://linear.app/neon-sprawl/issue/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. | **Adopted** — user resumed kickoff without override. | +| **Startup failure tests** | Loader unit tests only, or also `WebApplicationFactory` host boot failure? | **Both** — mirror [NEO-34](NEO-34-implementation-plan.md) (`SkillDefinitionCatalogLoaderTests` happy path, failure modes, `Host_ShouldResolveCatalogFromDi`, `Host_ShouldFailStartup_WhenCatalogDirectoryInvalid`). | **Adopted** | +| **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`](../../server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs) 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](NEO-50-implementation-plan.md)), 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](NEO-34-implementation-plan.md) 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`](../../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+](E3M3-prototype-backlog.md)). +- **`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/` loader** — `ItemDefinitionCatalogLoader.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. **`ItemCatalogPathResolution`** — `TryDiscoverItemsDirectory`, `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.cs`** — `AddItemDefinitionCatalog(configuration)`; after `Build()`, `GetRequiredService()` before routes/`Run()`. +6. **Tests** — temp-dir fixtures copying repo `item-def.schema.json`; valid six-item JSON matching [prototype_items.json](../../content/items/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.