12 KiB
NEO-145 — E7M4-02: Server contract template catalog load (fail-fast)
Linear: NEO-145
Branch: NEO-145-e7m4-02-server-contract-template-catalog-load-fail-fast
Backlog: E7M4-pre-production-backlog.md — E7M4-02
Module: E7_M4_ContractMissionGenerator.md
Pattern: NEO-134-implementation-plan.md (faction catalog load + CI-parity gates); NEO-113-implementation-plan.md (quest catalog cross-ref deps); NEO-101-implementation-plan.md (encounter registry + DI)
Precursor: NEO-144 Done — contract-template + contract-seed schemas, prototype_contract_templates.json, E7M4 CI gates (merged to main)
Blocks: 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 kickoff decisions table, 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 $refs 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
encounterTemplateIdvs loaded encounter catalog; bundle item/skill/faction refs +mission_rewardallowlist (CI parity). PrototypeE7M4ContractCatalogRules— roster, freeze, cross-ref, band-cap gates synced toscripts/validate_content.pyPROTOTYPE_E7M4_*+_prototype_e7m4_*.ContentPathsOptions+ path resolution forcontent/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.mdcontract catalog section.
Out of scope (from Linear + backlog):
- Per-player
ContractInstancestore, generator, HTTP (NEO-146–NEO-154). - Issue-time economy re-validation (NEO-147 / E7M4-07) — band-cap constants land here;
TryIssueenforcement 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 testcovers 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.jsonunder contracts directory; requireschemaVersion1 and top-levelcontractTemplatesarray. - JSON Schema validate each row against
contract-template.schema.jsonwith nested$refs (quest-reward-bundle,faction-gate-rule, grant row schemas — same registration pattern as quest loader). - Reject duplicate template ids across files.
- Run
PrototypeE7M4ContractCatalogRulesgates (below). - Throw
InvalidOperationExceptionwith sortederror:lines on failure.
Loader inputs (from resolved singletons):
knownEncounterIds—EncounterDefinitionCatalog.ById.KeysknownItemIds—ItemDefinitionCatalog.ById.Keys(orPrototypeSlice1ItemCatalogRules.ExpectedItemIdsfor parity with CI)knownFactionIds—FactionDefinitionCatalog.ById.KeysskillDefsById—SkillDefinitionCatalog.ById(formission_rewardallowlist)
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,ContractTemplateSchemaPathtoContentPathsOptions. ContractCatalogPathResolution— discovercontent/contracts, resolvecontract-template.schema.jsonsibling undercontent/schemas/.ContractCatalogServiceCollectionExtensions.AddContractTemplateCatalog— singleton catalog +IContractTemplateRegistry.Program.cs: register contract catalog afterAddQuestDefinitionCatalog; eager-resolveContractTemplateCatalogat boot (after quest catalog).InMemoryWebApplicationFactory: pin repocontent/contractsviaContent:ContractsDirectory.
5. Tests
Loader (ContractTemplateCatalogLoaderTests):
- Happy path — repo
prototype_contract_templates.jsonwith 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_rewardallowlist. - 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
- DTOs, rules, path resolution, loader, catalog, registry, DI.
ContentPathsOptions,Program.csload order,InMemoryWebApplicationFactory.- Tests (loader negatives → registry → host boot).
server/README.md.- 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.