From edb33514c5de74cabcbff6cbee886df66d7b06d7 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Mon, 15 Jun 2026 20:03:44 -0400 Subject: [PATCH] NEO-134: add E7M3-02 faction catalog load implementation plan Kickoff plan for fail-fast server faction catalog load with CI-parity gates, quest faction cross-refs, and registry DI wiring. --- docs/plans/NEO-134-implementation-plan.md | 200 ++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 docs/plans/NEO-134-implementation-plan.md diff --git a/docs/plans/NEO-134-implementation-plan.md b/docs/plans/NEO-134-implementation-plan.md new file mode 100644 index 0000000..bb45dd1 --- /dev/null +++ b/docs/plans/NEO-134-implementation-plan.md @@ -0,0 +1,200 @@ +# NEO-134 — E7M3-02: Server faction catalog load (fail-fast) + +**Linear:** [NEO-134](https://linear.app/neon-sprawl/issue/NEO-134) +**Branch:** `NEO-134-e7m3-server-faction-catalog-load-fail-fast` +**Backlog:** [E7M3-pre-production-backlog.md](E7M3-pre-production-backlog.md) — **E7M3-02** +**Module:** [E7_M3_FactionReputationLedger.md](../decomposition/modules/E7_M3_FactionReputationLedger.md) +**Pattern:** [NEO-125-implementation-plan.md](NEO-125-implementation-plan.md) (catalog loader + CI-parity gates); [NEO-113](NEO-113-implementation-plan.md) (skill/item catalog DI shape) +**Precursor:** [NEO-133](https://linear.app/neon-sprawl/issue/NEO-133) **`Done`** — faction schemas, `prototype_factions.json`, quest `factionGateRules` / `reputationGrants`, five-quest CI gates (**landed on `main`**) + +## Goal + +Fail-fast startup load of `content/factions/*_factions.json` with **CI-parity** validation; cross-check quest **`factionGateRules`** and **`completionRewardBundle.reputationGrants`** against the loaded faction catalog. Host refuses to listen when faction content or quest faction refs are invalid. + +## Kickoff clarifications + +**No clarifications needed.** Linear AC, [E7M3-02 backlog scope](E7M3-pre-production-backlog.md#e7m3-02--server-faction-catalog-load-fail-fast), kickoff decisions table, and NEO-125/NEO-133 precedents settle: + +| Topic | Decision | Evidence | +|-------|----------|----------| +| Module folder | `server/NeonSprawl.Server/Game/Factions/` | Backlog lists this path first; contracts are `FactionDef` / `IFactionDefinitionRegistry` | +| CI parity | Full E7M3 faction + quest faction gates on server | Backlog “CI-parity validation”; NEO-125 full E7M2 gate precedent | +| Quest DTO parse | Parse `factionGateRules` + `reputationGrants` onto typed rows now | NEO-125 parsed `completionRewardBundle` at load; NEO-137 gate eval needs rows | +| Load order | Faction catalog **before** quest catalog | Quest cross-ref gate needs `knownFactionIds` from faction registry | +| E7M2 bundle freeze | Keep existing item/skill freeze; **add** E7M3 freeze including rep | CI runs both `_prototype_e7m2_*` and `_prototype_e7m3_completion_bundle_freeze_gate` | +| `PrototypeE7M1QuestCatalogRules` rename | **Defer** full type rename | NEO-133 partial fix (error strings); rename is churn across many call sites — not in E7M3-02 AC | +| Standing store / apply / HTTP / Godot | **Out of scope** | Backlog E7M3-02; NEO-135+ | + +## Scope and out-of-scope + +**In scope (from Linear + backlog):** + +- Faction catalog loader, catalog type, `IFactionDefinitionRegistry`, DI registration. +- `PrototypeE7M3FactionCatalogRules` — roster, freeze table, standing-band ordering (mirrors `scripts/validate_content.py` `PROTOTYPE_E7M3_FACTION_*` + `_prototype_e7m3_faction_*` gates). +- Quest loader extensions: parse `FactionGateRuleRow`, `ReputationGrantRow`; E7M3 quest gates — faction cross-ref, E7M3 completion bundle freeze (item + skill + rep), grid-contract shape. +- `ContentPathsOptions` + path resolution for `content/factions/`. +- Unit tests (AAA) for faction loader happy path + broken refs; quest loader faction cross-ref negatives; host boot resolves faction + quest catalogs. +- `server/README.md` faction catalog section. + +**Out of scope:** + +- Per-player standing store, reputation apply, gate evaluation on accept, HTTP projection (NEO-135–NEO-139). +- Godot client (**client counterpart:** none — infrastructure-only). + +## Acceptance criteria checklist + +- [ ] Host exits on invalid faction catalog or broken quest faction refs at startup. +- [ ] Registry resolves faction metadata by id. +- [ ] `dotnet test` covers loader gates. + +## Technical approach + +### 1. Faction DTOs and catalog (`Game/Factions/`) + +| Type | Role | +|------|------| +| `FactionDefRow` | `Id`, `DisplayName`, `MinStanding`, `MaxStanding`, `NeutralStanding` | +| `FactionGateRuleRow` | `FactionId`, `MinStanding` (quest-side; lives in `Game/Quests/` or shared — prefer `Game/Quests/` next to quest rows) | +| `ReputationGrantRow` | `FactionId`, `Amount` (quest bundle row; `Game/Quests/`) | +| `FactionDefinitionCatalog` | Immutable `ById` map + directory metadata (mirror `SkillDefinitionCatalog`) | +| `IFactionDefinitionRegistry` / `FactionDefinitionRegistry` | `TryGetDefinition`, `GetDefinitionsInIdOrder` (mirror item registry) | + +### 2. `FactionDefinitionCatalogLoader` + +Mirror `SkillDefinitionCatalogLoader`: + +- Enumerate `*_factions.json` under factions directory; require `schemaVersion` 1 and top-level `factions` array. +- JSON Schema validate each row against `faction-def.schema.json`. +- Reject duplicate faction ids across files. +- Run `PrototypeE7M3FactionCatalogRules`: roster (exactly two ids), freeze table (display names + standing band), standing-band ordering (`minStanding <= neutralStanding <= maxStanding`). +- Throw `InvalidOperationException` with sorted `error:` lines on failure. + +### 3. `PrototypeE7M3FactionCatalogRules` + +Sync to `scripts/validate_content.py`: + +| Constant / helper | Python source | +|-------------------|---------------| +| `ExpectedFactionIds` | `PROTOTYPE_E7M3_FACTION_IDS` | +| `ExpectedFactionFreeze` | `PROTOTYPE_E7M3_FACTION_FREEZE` | +| `TryGetRosterGateError` | `_prototype_e7m3_faction_roster_gate` | +| `TryGetFreezeGateError` | `_prototype_e7m3_faction_freeze_gate` | +| `TryGetStandingBandGateError` | `_prototype_e7m3_faction_standing_band_gate` | + +### 4. `PrototypeE7M3QuestFactionRules` (`Game/Quests/`) + +Sync quest-side E7M3 gates: + +| Helper | Python source | +|--------|---------------| +| `TryGetFactionCrossRefError(rows, knownFactionIds)` | `_prototype_e7m3_faction_cross_ref_gate` | +| `TryGetCompletionBundleFreezeError(rows)` | `_prototype_e7m3_completion_bundle_freeze_gate` (uses `PROTOTYPE_E7M3_COMPLETION_BUNDLES` incl. rep) | +| `TryGetGridContractShapeError(rows)` | `_prototype_e7m3_grid_contract_shape_gate` | + +Extend **`QuestDefRow`** with optional `FactionGateRules`. Extend **`QuestRewardBundleRow`** with `ReputationGrants`. Update **`ParseRow`** / **`ParseCompletionRewardBundle`** in `QuestDefinitionCatalogLoader`. + +Run E7M3 quest gates **after** E7M2 bundle gates, passing `knownFactionIds` from faction catalog into cross-ref. + +### 5. Path resolution and DI + +- Add `FactionsDirectory`, `FactionDefSchemaPath` to `ContentPathsOptions`. +- `FactionCatalogPathResolution` — discover `content/factions`, resolve `faction-def.schema.json` sibling under `content/schemas/`. +- `FactionCatalogServiceCollectionExtensions.AddFactionDefinitionCatalog` — singleton catalog + registry. +- **`Program.cs`:** register faction catalog **after** skill catalog, **before** quest catalog; eager-resolve `FactionDefinitionCatalog` at boot. +- **`QuestCatalogServiceCollectionExtensions`:** inject `FactionDefinitionCatalog`; pass `factionCatalog.ById.Keys` into quest loader. + +### 6. Tests + +**Faction loader (`FactionDefinitionCatalogLoaderTests`):** + +- Happy path — repo `prototype_factions.json`. +- Missing factions directory / schema. +- Duplicate faction id. +- Wrong roster count or unknown id. +- Freeze table mismatch (display name or standing). +- Invalid standing band ordering. + +**Quest loader extensions (`QuestDefinitionCatalogLoaderTests`):** + +- Unknown `factionId` in `factionGateRules`. +- Unknown `factionId` in `reputationGrants`. +- E7M3 bundle freeze mismatch (operator-chain rep row). +- Grid-contract shape violation (wrong `minStanding` or objective). +- Existing repo prototype load + `Host_ShouldResolveQuestCatalogFromDi` still pass. + +**Registry (`FactionDefinitionRegistryTests`):** `TryGetDefinition` hit/miss; id-order listing. + +### 7. Docs + +- **`server/README.md`** — new faction catalog paragraph (config keys, gates, load order before quests). +- Optional one-line E7_M3 module status when implementation lands. + +## Files to add + +| Path | Purpose | +|------|---------| +| `server/NeonSprawl.Server/Game/Factions/FactionDefRow.cs` | Load-time faction row | +| `server/NeonSprawl.Server/Game/Factions/FactionDefinitionCatalog.cs` | In-memory catalog | +| `server/NeonSprawl.Server/Game/Factions/FactionDefinitionCatalogLoader.cs` | Fail-fast disk load | +| `server/NeonSprawl.Server/Game/Factions/FactionCatalogPathResolution.cs` | Directory + schema path discovery | +| `server/NeonSprawl.Server/Game/Factions/FactionCatalogServiceCollectionExtensions.cs` | DI registration | +| `server/NeonSprawl.Server/Game/Factions/IFactionDefinitionRegistry.cs` | Lookup contract | +| `server/NeonSprawl.Server/Game/Factions/FactionDefinitionRegistry.cs` | Registry adapter | +| `server/NeonSprawl.Server/Game/Factions/PrototypeE7M3FactionCatalogRules.cs` | CI-parity faction gates | +| `server/NeonSprawl.Server/Game/Quests/FactionGateRuleRow.cs` | Quest gate row | +| `server/NeonSprawl.Server/Game/Quests/ReputationGrantRow.cs` | Bundle rep grant row | +| `server/NeonSprawl.Server/Game/Quests/PrototypeE7M3QuestFactionRules.cs` | Quest-side E7M3 faction gates | +| `server/NeonSprawl.Server.Tests/Game/Factions/FactionDefinitionCatalogLoaderTests.cs` | Loader AAA tests | +| `server/NeonSprawl.Server.Tests/Game/Factions/FactionDefinitionRegistryTests.cs` | Registry AAA tests | +| `server/NeonSprawl.Server.Tests/Game/Factions/FactionCatalogTestPaths.cs` | Repo path discovery for tests | + +## Files to modify + +| Path | Change | +|------|--------| +| `server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs` | Add `FactionsDirectory`, `FactionDefSchemaPath` | +| `server/NeonSprawl.Server/Game/Quests/QuestDefRow.cs` | Optional `FactionGateRules` property | +| `server/NeonSprawl.Server/Game/Quests/QuestRewardBundleRow.cs` | Add `ReputationGrants` list | +| `server/NeonSprawl.Server/Game/Quests/QuestDefinitionCatalogLoader.cs` | Parse faction fields; accept `knownFactionIds`; run E7M3 quest faction gates | +| `server/NeonSprawl.Server/Game/Quests/QuestCatalogServiceCollectionExtensions.cs` | Inject faction catalog into quest loader | +| `server/NeonSprawl.Server/Program.cs` | Register + eager-resolve faction catalog before quests | +| `server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionCatalogLoaderTests.cs` | E7M3 negative cases; extend `LoadCatalog` helper with faction ids | +| `server/NeonSprawl.Server.Tests/Game/Quests/QuestCatalogTestPaths.cs` | Faction directory/schema discovery helpers | +| `server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs` | Ensure host boot resolves faction catalog if needed | +| `server/README.md` | Faction catalog section | + +## Tests + +| Layer | What | +|-------|------| +| `FactionDefinitionCatalogLoaderTests` | Happy path, missing paths, duplicate id, roster/freeze/standing-band gates | +| `FactionDefinitionRegistryTests` | Lookup by id, ordered listing | +| `QuestDefinitionCatalogLoaderTests` | Faction cross-ref, E7M3 bundle freeze, grid-contract shape; repo prototype + host boot | +| CI | Existing `dotnet test` + `validate_content.py` (no Python changes expected) | + +Manual QA: **none** (server infrastructure; Godot capstone is NEO-143). + +## Implementation order + +1. Faction DTOs, rules, path resolution, loader, catalog, registry, DI. +2. Quest DTO extensions + parse + `PrototypeE7M3QuestFactionRules` + wire into quest loader. +3. `Program.cs` load order + `ContentPathsOptions`. +4. Tests (faction loader → quest negatives → host boot). +5. `server/README.md`. +6. Run `dotnet test` on server project. + +## Open questions / risks + +| Item | Agent recommendation | Status | +|------|---------------------|--------| +| Rename `PrototypeE7M1QuestCatalogRules` → `PrototypeE7M3QuestCatalogRules` | Defer — not required for AC; large rename across tests and call sites | `deferred` | +| E7M2 vs E7M3 bundle freeze overlap | Keep both gates (CI does); E7M2 item/skill only, E7M3 adds rep rows | `adopted` | +| Quest loader already registers `faction-gate-rule` / `reputation-grant-row` schemas (NEO-133) | Reuse; no schema registration changes needed | `adopted` | + +## Client counterpart + +None for E7M3-02. Client faction HUD: **NEO-142** / capstone **NEO-143**. + +## Blocks + +**NEO-135** (faction standing store) is blocked until faction catalog load lands.