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

12 KiB
Raw Blame History

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.mdE7M3-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 (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

  • Host exits on invalid faction catalog or broken quest faction refs at startup.
  • Registry resolves faction metadata by id.
  • 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 PrototypeE7M1QuestCatalogRulesPrototypeE7M3QuestCatalogRules 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.