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.pull/173/head
parent
f248022c2d
commit
edb33514c5
|
|
@ -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.
|
||||||
Loading…
Reference in New Issue