# 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 - [x] Host exits on invalid faction catalog or broken quest faction refs at startup. - [x] Registry resolves faction metadata by id. - [x] `dotnet test` covers loader gates. ## Implementation reconciliation (shipped) - **Faction catalog:** `Game/Factions/` loader, catalog, `IFactionDefinitionRegistry`, `PrototypeE7M3FactionCatalogRules`, DI + `Program.cs` load order (after skills, before quests). - **Quest extensions:** `FactionGateRuleRow`, `ReputationGrantRow`, `PrototypeE7M3QuestFactionRules` (cross-ref, E7M3 bundle freeze, grid-contract shape); quest loader parses faction fields and runs E7M3 gates. - **Tests:** `FactionDefinitionCatalogLoaderTests`, `FactionDefinitionRegistryTests`, four new quest-loader negative cases; host boot resolves faction + quest catalogs (`746` tests green). - **Docs:** `server/README.md` faction catalog section; quest catalog paragraph updated for E7M3 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.