12 KiB
NEO-134 — E7M3-02: Server faction catalog load (fail-fast)
Linear: NEO-134
Branch: NEO-134-e7m3-server-faction-catalog-load-fail-fast
Backlog: E7M3-pre-production-backlog.md — E7M3-02
Module: E7_M3_FactionReputationLedger.md
Pattern: NEO-125-implementation-plan.md (catalog loader + CI-parity gates); NEO-113 (skill/item catalog DI shape)
Precursor: 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, 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 (mirrorsscripts/validate_content.pyPROTOTYPE_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 forcontent/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.mdfaction 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 testcovers loader gates.
Implementation reconciliation (shipped)
- Faction catalog:
Game/Factions/loader, catalog,IFactionDefinitionRegistry,PrototypeE7M3FactionCatalogRules, DI +Program.csload 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 (746tests green). - Docs:
server/README.mdfaction 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.jsonunder factions directory; requireschemaVersion1 and top-levelfactionsarray. - 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
InvalidOperationExceptionwith sortederror: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,FactionDefSchemaPathtoContentPathsOptions. FactionCatalogPathResolution— discovercontent/factions, resolvefaction-def.schema.jsonsibling undercontent/schemas/.FactionCatalogServiceCollectionExtensions.AddFactionDefinitionCatalog— singleton catalog + registry.Program.cs: register faction catalog after skill catalog, before quest catalog; eager-resolveFactionDefinitionCatalogat boot.QuestCatalogServiceCollectionExtensions: injectFactionDefinitionCatalog; passfactionCatalog.ById.Keysinto 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
factionIdinfactionGateRules. - Unknown
factionIdinreputationGrants. - E7M3 bundle freeze mismatch (operator-chain rep row).
- Grid-contract shape violation (wrong
minStandingor objective). - Existing repo prototype load +
Host_ShouldResolveQuestCatalogFromDistill 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
- Faction DTOs, rules, path resolution, loader, catalog, registry, DI.
- Quest DTO extensions + parse +
PrototypeE7M3QuestFactionRules+ wire into quest loader. Program.csload order +ContentPathsOptions.- Tests (faction loader → quest negatives → host boot).
server/README.md.- Run
dotnet teston 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.