neon-sprawl/docs/plans/NEO-145-implementation-plan.md

12 KiB
Raw Blame History

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.mdE7M4-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 Donecontract-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 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-146NEO-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 $refs (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):

  • knownEncounterIdsEncounterDefinitionCatalog.ById.Keys
  • knownItemIdsItemDefinitionCatalog.ById.Keys (or PrototypeSlice1ItemCatalogRules.ExpectedItemIds for parity with CI)
  • knownFactionIdsFactionDefinitionCatalog.ById.Keys
  • skillDefsByIdSkillDefinitionCatalog.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.