NEO-145: add E7M4-02 contract catalog load implementation plan.
Kickoff plan for fail-fast server loader, IContractTemplateRegistry, and CI-parity E7M4 gates following NEO-144 content landing.pull/185/head
parent
b5a0f71bca
commit
215493f63b
|
|
@ -0,0 +1,185 @@
|
|||
# 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**.
|
||||
Loading…
Reference in New Issue