# NEO-145 — E7M4-02: Server contract template catalog load (fail-fast) **Linear:** [NEO-145](https://linear.app/neon-sprawl/issue/NEO-145) **Branch:** `NEO-145-e7m4-02-server-contract-template-catalog-load-fail-fast` **Backlog:** [E7M4-pre-production-backlog.md](E7M4-pre-production-backlog.md) — **E7M4-02** **Module:** [E7_M4_ContractMissionGenerator.md](../decomposition/modules/E7_M4_ContractMissionGenerator.md) **Pattern:** [NEO-134-implementation-plan.md](NEO-134-implementation-plan.md) (faction catalog load + CI-parity gates); [NEO-113-implementation-plan.md](NEO-113-implementation-plan.md) (quest catalog cross-ref deps); [NEO-101-implementation-plan.md](NEO-101-implementation-plan.md) (encounter registry + DI) **Precursor:** [NEO-144](https://linear.app/neon-sprawl/issue/NEO-144) **`Done`** — `contract-template` + `contract-seed` schemas, `prototype_contract_templates.json`, E7M4 CI gates (**merged to `main`**) **Blocks:** [NEO-146](https://linear.app/neon-sprawl/issue/NEO-146) (contract instance + outcome stores) **Client counterpart:** none (infrastructure-only); playable loop is **NEO-154** (E7M4-11) ## Goal Fail-fast startup load of `content/contracts/*_contract_templates.json` with **CI-parity** validation; cross-check **`encounterTemplateId`** and bundle refs against loaded catalogs. Host refuses to listen when contract content is missing, malformed, or fails prototype E7M4 gates. **`IContractTemplateRegistry`** resolves template metadata by id for E7M4-04+. ## Kickoff clarifications **No blocking decisions needed.** Linear AC, [E7M4-02 backlog scope](E7M4-pre-production-backlog.md#e7m4-02--server-contract-template-catalog-load-fail-fast), [E7M4 kickoff decisions table](E7M4-pre-production-backlog.md#kickoff-decisions-decomposition-defaults), NEO-144 shipped CI gates, and NEO-134/NEO-113 precedents settle: | Topic | Decision | Evidence | |-------|----------|----------| | CI parity scope | Full E7M4 gates on server (schema, roster, freeze, cross-ref, band caps) | Backlog “CI-parity validation”; NEO-134 full E7M3 gate precedent | | Registry in E7M4-02 | Ship **`IContractTemplateRegistry`** + DI now | Backlog in-scope list; unlike E5M3-02/03 split, E7M4-02 explicitly includes registry | | Runtime validation | In-process **C#** mirroring `scripts/validate_content.py` | NEO-51/134 precedent; no Python on server images | | Cross-ref deps | Encounter, item, skill, faction catalogs at load | Python `_prototype_e7m4_cross_ref_gate`; quest E7M2 `mission_reward` allowlist on skill XP | | Load order | After item, skill, faction, encounter, quest catalogs | All cross-ref sources resolved; contracts do not affect quest load | | Band caps | Port **`PROTOTYPE_E7M4_BAND_CAPS`** to C# in loader gates now | NEO-144 review + plan open question; E7M4-07 reuses same constants at issue time | | Bundle DTO reuse | Reuse **`QuestRewardBundleRow`**, **`FactionGateRuleRow`** from `Game/Quests/` | Same `$ref`s as quest catalog; avoids duplicate grant row types | | Instance store / generator / HTTP / Godot | **Out of scope** | Backlog E7M4-02; NEO-146+ | ## Scope and out-of-scope **In scope (from Linear + backlog):** - Loader + catalog types under `server/NeonSprawl.Server/Game/Contracts/`. - **`IContractTemplateRegistry`** + DI registration. - Cross-check **`encounterTemplateId`** vs loaded encounter catalog; bundle item/skill/faction refs + **`mission_reward`** allowlist (CI parity). - **`PrototypeE7M4ContractCatalogRules`** — roster, freeze, cross-ref, band-cap gates synced to `scripts/validate_content.py` `PROTOTYPE_E7M4_*` + `_prototype_e7m4_*`. - `ContentPathsOptions` + path resolution for `content/contracts/`. - Unit tests (AAA) for loader happy path and failure modes (broken encounter ref, roster/freeze/band-cap); host boot resolves contract catalog. - `server/README.md` contract catalog section. **Out of scope (from Linear + backlog):** - Per-player **`ContractInstance`** store, generator, HTTP (**NEO-146**–**NEO-154**). - Issue-time economy re-validation (**NEO-147** / E7M4-07) — band-cap **constants** land here; **`TryIssue`** enforcement is later. - Godot client. ## Acceptance criteria checklist - [ ] Host exits on invalid contract catalog or broken encounter refs at startup. - [ ] Registry resolves template metadata by id. - [ ] `dotnet test` covers loader gates. ## Technical approach ### 1. Contract DTOs and catalog (`Game/Contracts/`) | Type | Role | |------|------| | `ContractTemplateRow` | `Id`, `DisplayName`, `ZoneDifficultyBand`, `ObjectiveKind`, `EncounterTemplateId`, `MinFactionStanding` (`FactionGateRuleRow`), `CompletionRewardBundle` (`QuestRewardBundleRow`) | | `ContractTemplateCatalog` | Immutable `ById` map + directory metadata (mirror `FactionDefinitionCatalog`) | | `IContractTemplateRegistry` / `ContractTemplateRegistry` | `TryGetDefinition`, `GetDefinitionsInIdOrder` (mirror `IEncounterDefinitionRegistry`) | ### 2. `ContractTemplateCatalogLoader` Mirror `FactionDefinitionCatalogLoader` + quest bundle parsing: - Enumerate `*_contract_templates.json` under contracts directory; require `schemaVersion` 1 and top-level `contractTemplates` array. - JSON Schema validate each row against `contract-template.schema.json` with nested `$ref`s (`quest-reward-bundle`, `faction-gate-rule`, grant row schemas — same registration pattern as quest loader). - Reject duplicate template ids across files. - Run `PrototypeE7M4ContractCatalogRules` gates (below). - Throw `InvalidOperationException` with sorted `error:` lines on failure. **Loader inputs** (from resolved singletons): - `knownEncounterIds` — `EncounterDefinitionCatalog.ById.Keys` - `knownItemIds` — `ItemDefinitionCatalog.ById.Keys` (or `PrototypeSlice1ItemCatalogRules.ExpectedItemIds` for parity with CI) - `knownFactionIds` — `FactionDefinitionCatalog.ById.Keys` - `skillDefsById` — `SkillDefinitionCatalog.ById` (for `mission_reward` allowlist) ### 3. `PrototypeE7M4ContractCatalogRules` Sync to `scripts/validate_content.py`: | Constant / helper | Python source | |-------------------|---------------| | `ExpectedTemplateIds` | `PROTOTYPE_E7M4_TEMPLATE_IDS` | | `ExpectedTemplateFreeze` | `PROTOTYPE_E7M4_TEMPLATE_FREEZE` | | `BandCapsByDifficultyBand` | `PROTOTYPE_E7M4_BAND_CAPS` | | `TryGetRosterGateError` | `_prototype_e7m4_template_roster_gate` | | `TryGetFreezeGateError` | `_prototype_e7m4_template_freeze_gate` | | `TryGetCrossRefError` | `_prototype_e7m4_cross_ref_gate` | | `TryGetBandCapGateError` | `_prototype_e7m4_band_cap_gate` | Bundle normalization for freeze compare: mirror `_normalize_e7m3_completion_bundle` (item + skill XP + rep grants). Cross-ref gate iterates **all loaded catalog rows** (not roster-only), matching NEO-144 review fix. ### 4. Path resolution and DI - Add `ContractsDirectory`, `ContractTemplateSchemaPath` to `ContentPathsOptions`. - `ContractCatalogPathResolution` — discover `content/contracts`, resolve `contract-template.schema.json` sibling under `content/schemas/`. - `ContractCatalogServiceCollectionExtensions.AddContractTemplateCatalog` — singleton catalog + `IContractTemplateRegistry`. - **`Program.cs`:** register contract catalog **after** `AddQuestDefinitionCatalog`; eager-resolve `ContractTemplateCatalog` at boot (after quest catalog). - **`InMemoryWebApplicationFactory`:** pin repo `content/contracts` via `Content:ContractsDirectory`. ### 5. Tests **Loader (`ContractTemplateCatalogLoaderTests`):** - Happy path — repo `prototype_contract_templates.json` with real cross-ref catalogs. - Missing contracts directory / schema. - Duplicate template id. - Wrong roster count or unknown id. - Freeze table mismatch (display name, band, encounter, standing, bundle). - Unknown `encounterTemplateId` (primary AC negative). - Unknown bundle item/skill/faction ref; skill XP missing `mission_reward` allowlist. - Band-cap violation (local temp fixture or inline JSON — do not commit bad catalog). **Registry (`ContractTemplateRegistryTests`):** `TryGetDefinition` hit/miss; id-order listing. **Host boot:** `Host_ShouldResolveContractCatalogFromDi` — factory starts; `IContractTemplateRegistry` resolves `prototype_contract_clear_combat_pocket`. ### 6. Docs - **`server/README.md`** — new contract catalog paragraph (config keys, gates, load order after quests). - Optional one-line E7_M4 module CI note when implementation lands. ### 7. Implementation order 1. DTOs, rules, path resolution, loader, catalog, registry, DI. 2. `ContentPathsOptions`, `Program.cs` load order, `InMemoryWebApplicationFactory`. 3. Tests (loader negatives → registry → host boot). 4. `server/README.md`. 5. Run `dotnet test`. ## Files to add | Path | Purpose | |------|---------| | `docs/plans/NEO-145-implementation-plan.md` | This plan. | | `server/NeonSprawl.Server/Game/Contracts/ContractTemplateRow.cs` | Load-time contract template row | | `server/NeonSprawl.Server/Game/Contracts/ContractTemplateCatalog.cs` | In-memory catalog | | `server/NeonSprawl.Server/Game/Contracts/ContractTemplateCatalogLoader.cs` | Fail-fast disk load | | `server/NeonSprawl.Server/Game/Contracts/ContractCatalogPathResolution.cs` | Directory + schema path discovery | | `server/NeonSprawl.Server/Game/Contracts/ContractCatalogServiceCollectionExtensions.cs` | DI registration | | `server/NeonSprawl.Server/Game/Contracts/IContractTemplateRegistry.cs` | Lookup contract | | `server/NeonSprawl.Server/Game/Contracts/ContractTemplateRegistry.cs` | Registry adapter | | `server/NeonSprawl.Server/Game/Contracts/PrototypeE7M4ContractCatalogRules.cs` | CI-parity E7M4 gates + band caps | | `server/NeonSprawl.Server.Tests/Game/Contracts/ContractTemplateCatalogLoaderTests.cs` | Loader AAA tests | | `server/NeonSprawl.Server.Tests/Game/Contracts/ContractTemplateRegistryTests.cs` | Registry AAA tests | | `server/NeonSprawl.Server.Tests/Game/Contracts/ContractCatalogTestPaths.cs` | Repo path discovery for tests | ## Files to modify | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs` | Add `ContractsDirectory`, `ContractTemplateSchemaPath` | | `server/NeonSprawl.Server/Program.cs` | Register + eager-resolve contract catalog after quest catalog | | `server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs` | Pin `Content:ContractsDirectory` for host boot tests | | `server/README.md` | Contract catalog section (config, gates, load order) | ## Tests | Layer | What | |-------|------| | `ContractTemplateCatalogLoaderTests` | Happy path, missing paths, duplicate id, roster/freeze/cross-ref/band-cap gates; broken encounter ref (AC) | | `ContractTemplateRegistryTests` | Lookup by id, ordered listing | | Host boot | `Host_ShouldResolveContractCatalogFromDi` via `InMemoryWebApplicationFactory` | | CI | Existing `dotnet test` + `validate_content.py` (no Python changes expected) | Manual Godot QA: **none** (server infrastructure; capstone is NEO-154). ## Open questions / risks | Question / risk | Agent recommendation | Status | |-----------------|----------------------|--------| | Band-cap constants drift vs Python | Keep `PrototypeE7M4ContractCatalogRules.BandCapsByDifficultyBand` name-aligned with `PROTOTYPE_E7M4_BAND_CAPS`; add sync comment on both sides | `adopted` | | Shared bundle cross-ref logic with quests | Extract only if duplication is large; otherwise mirror quest item/skill checks in E7M4 rules with contract-specific error prefixes | `adopted` | | E7M4-07 issue-time validation | Loader gates suffice for E7M4-02 AC; port same `BandCapsByDifficultyBand` to `ContractEconomyValidation` in NEO-147 | `deferred` | ## Client counterpart None for E7M4-02. Client contract HUD: **NEO-153** / capstone **NEO-154**.