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

208 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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-135NEO-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.