From 06c58998ccd020c4e813b38f4495f039abdd5084 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 00:03:00 -0400 Subject: [PATCH] NEO-101: Add E5M3-02 encounter/reward catalog load kickoff plan. Documents fail-fast server loader scope, CI-parity gates, and loader plus host boot test approach after kickoff clarifications. --- docs/plans/NEO-101-implementation-plan.md | 137 ++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/plans/NEO-101-implementation-plan.md diff --git a/docs/plans/NEO-101-implementation-plan.md b/docs/plans/NEO-101-implementation-plan.md new file mode 100644 index 0000000..e90c02e --- /dev/null +++ b/docs/plans/NEO-101-implementation-plan.md @@ -0,0 +1,137 @@ +# NEO-101 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-101 | +| **Title** | E5M3-02: Server encounter + reward catalog load (fail-fast) | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-101/e5m3-02-server-encounter-reward-catalog-load-fail-fast | +| **Module** | [E5.M3 — EncounterAndRewardTables](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) · Epic 5 Slice 3 · backlog **E5M3-02** | +| **Branch** | `NEO-101-encounter-reward-catalog-load-fail-fast` | +| **Precursor** | [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) — encounter + reward-table schemas, catalogs, CI gates (**landed on `main`**) | +| **Pattern** | [NEO-51](NEO-51-implementation-plan.md) / [NEO-66](NEO-66-implementation-plan.md) / [NEO-88](NEO-88-implementation-plan.md) — row JSON Schema + catalog envelope + in-process C# gates mirroring `scripts/validate_content.py` | +| **Blocks** | [NEO-102](https://linear.app/neon-sprawl/issue/NEO-102) — injectable encounter/reward registries + DI | +| **Client counterpart** | None — server-only catalog load; player-visible work starts at [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) / [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **Test scope** | Loader unit tests only, or also `WebApplicationFactory` host boot failure / DI resolve tests? | **Both** — mirror [NEO-51](NEO-51-implementation-plan.md) and [NEO-88](NEO-88-implementation-plan.md) (`Host_ShouldResolveCatalogFromDi`, `Host_ShouldFailStartup_WhenCatalogDirectoryInvalid`); Linear AC “host fails on invalid JSON” is stronger with an integration boot test. | **User:** loader + host boot tests. | +| **NEO-101 vs NEO-102 split** | Ship `IEncounterDefinitionRegistry` / `IRewardTableDefinitionRegistry` on this story? | **Strict split** — E5M3-02: loader + catalog singletons + eager fail-fast boot; [NEO-102](https://linear.app/neon-sprawl/issue/NEO-102) adds injectable registries (E5M3-03). Same pattern as NEO-51/52. | **Adopted** — backlog out-of-scope; no separate question needed. | +| **Runtime validation** | In-process C# vs subprocess `validate_content.py`? | **In-process C#** mirroring `scripts/validate_content.py` E5M3 gates (NEO-51/66 precedent; no Python on server images). | **Adopted** | +| **Cross-ref at load** | Validate `fixedGrants[].itemId` against item catalog? NPC ids against `PrototypeNpcRegistry`? | **Yes** — reward-table loader takes `IReadOnlySet knownItemIds` from `ItemDefinitionCatalog` (recipe pattern); encounter loader cross-checks `requiredNpcInstanceIds` set against `PrototypeNpcRegistry.GetInstanceIdsInOrder()` and `rewardTableId` against loaded reward-table ids. Load **reward tables before encounters** (same order as CI `main()`). | **Adopted** | + +## Goal, scope, and out-of-scope + +**Goal:** Disk → host: startup load of `content/encounters/*.json` and `content/reward-tables/*.json` with CI-parity validation; process refuses to listen when catalogs are missing, malformed, or fail prototype E5M3 gates. + +**In scope (from Linear + [E5M3-02](E5M3-prototype-backlog.md#e5m3-02--server-encounter--reward-catalog-load-fail-fast)):** + +- Loader + catalog types under `server/NeonSprawl.Server/Game/Encounters/`. +- Fail-fast on malformed files, duplicate ids, allowlist mismatch with CI, broken cross-refs (item ids, NPC instance ids, `rewardTableId`). +- Unit tests (AAA) for loader happy path and failure modes; host boot tests (AAA). +- `server/README.md` section for encounter + reward-table catalogs. + +**Out of scope (from Linear + backlog):** + +- Injectable registry interfaces (**NEO-102** / E5M3-03). +- HTTP projection, progress store, runtime completion (E5M3-04+). +- Godot client. + +## Acceptance criteria checklist + +- [ ] Host fails startup on invalid encounter or reward-table JSON (mirror CI rules). +- [ ] Tests cover at least one happy path and one malformed catalog rejection (loader + host boot per kickoff). + +## Technical approach + +1. **`Game/Encounters/` reward-table loader** — `RewardTableDefinitionCatalogLoader.Load(rewardTablesDirectory, rewardTableSchemaPath, rewardGrantRowSchemaPath, knownItemIds, logger)`: + - Enumerate `*_reward_tables.json` (top-level only), ordered by path. + - Per file: parse JSON; require `schemaVersion == 1` and top-level `rewardTables` array. + - Register `reward-grant-row.schema.json` in `SchemaRegistry.Global` before evaluating `reward-table.schema.json` (recipe I/O row pattern). + - Per row: JsonSchema.Net validation; track `id`; reject duplicate `id` across files. + - Cross-ref: each `fixedGrants[].itemId` ∈ `knownItemIds`; reject duplicate `itemId` within a table (mirror CI `_validate_reward_table_catalogs`). + - Run **`PrototypeE5M3RewardTableCatalogRules`**: ids == `{ prototype_combat_pocket_clear }`; frozen grant quantities `{ scrap_metal_bulk: 10, contract_handoff_token: 1 }` (sync comment → `PROTOTYPE_E5M3_REWARD_TABLE_IDS` / `PROTOTYPE_E5M3_REWARD_GRANTS` in `validate_content.py`). + - Throw **`InvalidOperationException`** with aggregated sorted messages prefixed `Reward table catalog validation failed:`. + +2. **`RewardTableDefinitionCatalog`** — immutable snapshot: directory path, `ById`, `DistinctRewardTableCount`, `CatalogJsonFileCount`; `TryGetRewardTable(id, out RewardTableDefRow?)`. + +3. **`RewardTableDefRow` / `RewardGrantRow`** — load-time DTOs (`Id`, `DisplayName`, `FixedGrants` with `ItemId` + `Quantity`). + +4. **`Game/Encounters/` encounter loader** — `EncounterDefinitionCatalogLoader.Load(encountersDirectory, encounterDefSchemaPath, knownRewardTableIds, logger)`: + - Enumerate `*_encounters.json` (top-level only), ordered by path. + - Per file: `schemaVersion == 1`, top-level `encounters` array; row schema via `encounter-def.schema.json`. + - Duplicate `id` rejection across files. + - Run **`PrototypeE5M3EncounterCatalogRules`**: ids == `{ prototype_combat_pocket }`; `requiredNpcInstanceIds` set equals `PrototypeNpcRegistry` instance id set; each `rewardTableId` ∈ `knownRewardTableIds`. + - Throw with prefix `Encounter catalog validation failed:`. + +5. **`EncounterDefinitionCatalog`** — `ById`, counts, `TryGetEncounter(id, out EncounterDefRow?)`. + +6. **`EncounterDefRow`** — `Id`, `DisplayName`, `CompletionCriteriaKind`, `RequiredNpcInstanceIds`, `RewardTableId`. + +7. **Path resolution** — `EncounterCatalogPathResolution` / `RewardTableCatalogPathResolution`: `TryDiscover*Directory`, `Resolve*Directory`, `Resolve*SchemaPath` (ancestor walk from `AppContext.BaseDirectory` for `content/encounters` and `content/reward-tables`; schema defaults under sibling `content/schemas/`). + +8. **`ContentPathsOptions`** — add `EncountersDirectory`, `EncounterDefSchemaPath`, `RewardTablesDirectory`, `RewardTableDefSchemaPath`, `RewardGrantRowSchemaPath`. + +9. **`EncounterCatalogServiceCollectionExtensions.AddEncounterAndRewardCatalogs`** (single extension, two singletons): + - Factory for `RewardTableDefinitionCatalog` depends on `ItemDefinitionCatalog` (known item ids). + - Factory for `EncounterDefinitionCatalog` depends on `RewardTableDefinitionCatalog` (known reward table ids). + - **No** `IEncounterDefinitionRegistry` / `IRewardTableDefinitionRegistry` yet (NEO-102). + +10. **`Program.cs`** — register extension; after `Build()`, eager-resolve both catalogs before `Run()` (same as items/recipes/NPC behaviors). + +11. **`InMemoryWebApplicationFactory`** — pin `Content:EncountersDirectory` and `Content:RewardTablesDirectory` to repo paths so existing tests keep booting. + +12. **Tests** — temp-dir fixtures copying repo schemas; valid JSON matching [prototype_reward_tables.json](../../content/reward-tables/prototype_reward_tables.json) and [prototype_encounters.json](../../content/encounters/prototype_encounters.json); negative cases for schema violation, duplicate id, wrong allowlist id, unknown item id, wrong NPC set, broken `rewardTableId`, duplicate `fixedGrants[].itemId`, wrong grant quantities; host resolve via DI + `/health`; host fail with empty encounters dir. + +13. **Docs** — `server/README.md` sections (config keys, discovery, fail-fast, load order); `documentation_and_implementation_alignment.md` E5.M3 row note server load landed when complete. + +## Files to add + +| Path | Purpose | +|------|---------| +| `server/NeonSprawl.Server/Game/Encounters/RewardGrantRow.cs` | Load-time DTO for a single fixed grant row. | +| `server/NeonSprawl.Server/Game/Encounters/RewardTableDefRow.cs` | Load-time reward table row DTO. | +| `server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalog.cs` | In-memory reward-table catalog + `TryGetRewardTable`. | +| `server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalogLoader.cs` | Disk I/O, schema validation, item cross-ref, E5M3 gates, logging. | +| `server/NeonSprawl.Server/Game/Encounters/RewardTableCatalogPathResolution.cs` | Directory/schema discovery for reward tables. | +| `server/NeonSprawl.Server/Game/Encounters/PrototypeE5M3RewardTableCatalogRules.cs` | Frozen reward-table ids + grant quantity gate (sync → Python). | +| `server/NeonSprawl.Server/Game/Encounters/EncounterDefRow.cs` | Load-time encounter row DTO. | +| `server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionCatalog.cs` | In-memory encounter catalog + `TryGetEncounter`. | +| `server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionCatalogLoader.cs` | Disk I/O, schema validation, NPC + rewardTableId cross-ref, E5M3 gates. | +| `server/NeonSprawl.Server/Game/Encounters/EncounterCatalogPathResolution.cs` | Directory/schema discovery for encounters. | +| `server/NeonSprawl.Server/Game/Encounters/PrototypeE5M3EncounterCatalogRules.cs` | Frozen encounter ids + NPC instance set + rewardTableId cross-ref rules. | +| `server/NeonSprawl.Server/Game/Encounters/EncounterCatalogServiceCollectionExtensions.cs` | `AddEncounterAndRewardCatalogs` DI registration. | +| `server/NeonSprawl.Server.Tests/Game/Encounters/EncounterCatalogTestPaths.cs` | Repo `content/encounters` + `content/reward-tables` + schema discovery for tests. | +| `server/NeonSprawl.Server.Tests/Game/Encounters/RewardTableDefinitionCatalogLoaderTests.cs` | AAA loader tests for reward-table catalog. | +| `server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionCatalogLoaderTests.cs` | AAA loader + host boot tests for both catalogs. | +| `docs/plans/NEO-101-implementation-plan.md` | This plan. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs` | Add encounter + reward-table directory and schema path options under `Content` section. | +| `server/NeonSprawl.Server/Program.cs` | Register encounter/reward catalogs; eager-resolve after build. | +| `server/NeonSprawl.Server/appsettings.json` | Optional empty `Content:EncountersDirectory` / `RewardTablesDirectory` / schema override keys for documentation. | +| `server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs` | Pin encounter + reward-table content paths so test host boots with repo catalogs. | +| `server/README.md` | New encounter + reward-table catalog sections (config, discovery, fail-fast, load order). | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M3 row — note NEO-101 server load when complete. | + +## Tests + +| Test file | What it covers | +|-----------|----------------| +| `server/NeonSprawl.Server.Tests/Game/Encounters/RewardTableDefinitionCatalogLoaderTests.cs` | **Unit:** valid prototype reward-table catalog loads; `rewardTables` not array; schema violation; duplicate `id`; wrong/missing E5M3 table id; unknown `itemId` in `fixedGrants`; duplicate `itemId` in `fixedGrants`; wrong frozen grant quantities; `schemaVersion` ≠ 1; missing schema file. | +| `server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionCatalogLoaderTests.cs` | **Unit:** valid prototype encounter catalog loads (with pre-built reward-table id set); `encounters` not array; schema violation; duplicate `id`; wrong/missing E5M3 encounter id; wrong `requiredNpcInstanceIds` set; unknown `rewardTableId`; `schemaVersion` ≠ 1. **Host:** `InMemoryWebApplicationFactory` + `/health` + DI resolve both catalogs (encounter `prototype_combat_pocket`, reward table `prototype_combat_pocket_clear`); `WebApplicationFactory` with empty encounters dir fails startup with actionable message. | + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| **Constants drift vs Python** | Comment on `PrototypeE5M3*` rules pointing at `scripts/validate_content.py` `PROTOTYPE_E5M3_*` constants and gate functions. | **adopted** | +| **NPC instance ids are code constants, not content** | Encounter cross-ref uses `PrototypeNpcRegistry.GetInstanceIdsInOrder()` set equality (same as CI `PROTOTYPE_E5M2_NPC_INSTANCE_IDS`). | **adopted** | +| **Test cwd / CI** | `InMemoryWebApplicationFactory` must set `Content:EncountersDirectory` and `Content:RewardTablesDirectory` explicitly. | **adopted** | +| **Load order in DI** | Register reward-table catalog factory before encounter catalog; encounter factory reads reward-table ids from resolved singleton. | **adopted** |