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

11 KiB

NEO-125 — Implementation plan

Story reference

Field Value
Key NEO-125
Title E7M2-02: Server quest catalog load — reward bundle validation
Linear https://linear.app/neon-sprawl/issue/NEO-125/e7m2-02-server-quest-catalog-load-reward-bundle-validation
Module E7.M2 — RewardAndUnlockRouter · Epic 7 Slice 2 · backlog E7M2-02
Branch NEO-125-e7m2-server-quest-catalog-reward-bundle-validation
Precursor NEO-124 DonecompletionRewardBundle schemas, catalog rows, CI gates (landed on main)
Pattern NEO-113 — in-process C# gates mirroring scripts/validate_content.py; NEO-101 — multi-schema $ref registration (reward-table loader)
Blocks NEO-127 (router apply) via NEO-126 parallel path; catalog must load before delivery stories
Client counterpart none — infrastructure-only (E7M2-prototype-backlog)

Kickoff clarifications

No clarifications needed. Linear goal (“CI parity”), E7M2 backlog in/out-of-scope, and NEO-113 / NEO-124 precedents settle:

  • Full E7M2 gate parity on the server (presence + freeze table + item/skill cross-refs + mission_reward allowlist) — not cross-ref-only.
  • Parse typed bundle rows onto QuestDefRow now — backlog “extend catalog loader / registry types”; NEO-127 router reads catalog without re-parsing JSON.
  • Reuse RewardGrantRow (Encounters) for itemGrants — same schema shape as E5.M3 / CI; add QuestSkillXpGrantRow + QuestRewardBundleRow under Game/Quests/.
  • Separate PrototypeE7M2QuestCatalogRules — mirrors E7M1 rules class + Python PROTOTYPE_E7M2_* constants.
  • Host boot tests — same kickoff default as NEO-113 (loader + WebApplicationFactory).

Known gap on main after NEO-124: quest catalog load throws RefResolutionException for quest-reward-bundle.json because bundle schemas are not registered in CatalogSchemaRegistry — fixing this is in scope.

Goal, scope, and out-of-scope

Goal: Fail-fast startup validation of completionRewardBundle with CI parity; host refuses to listen when bundle refs or prototype freeze rules fail.

In scope (from Linear + E7M2-02):

  • Extend server/NeonSprawl.Server/Game/Quests/ loader, DTOs, and prototype rules.
  • Register bundle-related JSON Schemas before row validation.
  • Cross-check itemGrants[].itemId against the item catalog and skillXpGrants[].skillId against the skill catalog + allowedXpSourceKinds (must include mission_reward).
  • Unit tests (AAA) for happy path and broken bundle refs; host boot still resolves catalog.
  • server/README.md quest-catalog section update.

Out of scope (from Linear):

  • Delivery apply, IRewardDeliveryStore, router, HTTP projection changes (NEO-126+).
  • Godot client.

Acceptance criteria checklist

  • Host exits on invalid bundle refs at startup.
  • dotnet test covers loader gates.

Implementation reconciliation (shipped)

  • DTOs: QuestSkillXpGrantRow, QuestRewardBundleRow; optional CompletionRewardBundle on QuestDefRow.
  • Rules: PrototypeE7M2QuestCatalogRules — presence, freeze-table, cross-ref + mission_reward allowlist gates synced to validate_content.py.
  • Loader: registers bundle-related JSON Schemas; parses completionRewardBundle; accepts SkillDefinitionCatalog.ById from DI.
  • Tests: 25 AAA tests in QuestDefinitionCatalogLoaderTests (5 new E7M2 cases + updated prototype fixture with bundles).
  • Docs: server/README.md quest catalog E7M2 paragraph; E7M2-02 backlog AC checked.

Technical approach

1. Bundle DTOs (Game/Quests/)

  • QuestSkillXpGrantRowSkillId, Amount (record or sealed class, same style as QuestObjectiveDefRow).
  • QuestRewardBundleRowItemGrants (IReadOnlyList<RewardGrantRow> from Game.Encounters), SkillXpGrants (IReadOnlyList<QuestSkillXpGrantRow>).
  • Extend QuestDefRow with optional CompletionRewardBundle (QuestRewardBundleRow?).

2. PrototypeE7M2QuestCatalogRules

New static class synced to scripts/validate_content.py:

Constant / helper Python source
MissionRewardSourceKind = "mission_reward" PROTOTYPE_E7M2_MISSION_REWARD_SOURCE_KIND
ExpectedCompletionBundles PROTOTYPE_E7M2_COMPLETION_BUNDLES (normalized item + skill grant rows per quest id)
TryGetCompletionBundlePresenceError(rowsById) _prototype_e7m2_completion_bundle_presence_gate
TryGetCompletionBundleFreezeError(rowsById) _prototype_e7m2_completion_bundle_freeze_gate (sort grant rows before compare)
TryGetCompletionBundleCrossRefError(rowsById, knownItemIds, skillDefsById) _prototype_e7m2_completion_bundle_cross_ref_gate

Presence/freeze gates apply only to ids in PrototypeE7M1QuestCatalogRules.ExpectedQuestIds (four frozen quests).

3. Schema registration (QuestDefinitionCatalogLoader)

Before evaluating quest-def.schema.json, register (dependency order, same as CI $ref registry):

  1. reward-grant-row.schema.json
  2. quest-skill-xp-grant.schema.json
  3. quest-reward-bundle.schema.json
  4. Existing objective → step → def chain

Add path resolvers on QuestCatalogPathResolution (sibling content/schemas/ defaults, optional ContentPathsOptions overrides if needed for symmetry — can derive from quests directory like step schema).

Extend EnsureQuestSchemasLoaded signature to accept bundle schema paths; validate schema files exist at load start.

4. Loader parse + gates

  • ParseRow: when completionRewardBundle present, parse into QuestRewardBundleRow (omit empty itemGrants arrays).
  • After existing E7M1 gates, run E7M2 rules in order: presence → freeze → cross-ref.
  • Extend Load(...) signature with IReadOnlyDictionary<string, SkillDefRow> skillDefsById (or SkillDefinitionCatalog + extract .ById in DI factory).

5. DI (QuestCatalogServiceCollectionExtensions)

  • Inject SkillDefinitionCatalog alongside item / recipe / encounter catalogs.
  • Pass skillCatalog.ById into loader (skills already register before quests in Program.cs).

6. Tests (QuestDefinitionCatalogLoaderTests)

  • Update ValidPrototypeCatalogJson with completionRewardBundle on all four quests (match repo / freeze table).
  • CreateTempContentLayout: copy bundle + grant-row schemas from repo (add helpers on QuestCatalogTestPaths).
  • Extend LoadCatalog helper with frozen skill defs map (reuse repo skill catalog discovery or inline salvage/refine rows with mission_reward in AllowedXpSourceKinds).
  • New negative tests (AAA):
    • Missing completionRewardBundle on a frozen quest id.
    • Wrong freeze-table XP amount.
    • Unknown itemId in bundle.
    • Unknown skillId in bundle.
    • Skill missing mission_reward in allowedXpSourceKinds.
  • Existing Load_ShouldSucceed_WhenCatalogMatchesRepoPrototypeFile and Host_ShouldResolveQuestCatalogFromDi must pass again.

7. Docs

  • server/README.md — extend quest catalog paragraph: bundle schema $refs, E7M2 presence/freeze/cross-ref gates, skill catalog dependency.
  • E7M2-prototype-backlog.md — check E7M2-02 AC boxes when shipped.
  • documentation_and_implementation_alignment.md — E7.M2 row note server load (NEO-125).

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Quests/QuestSkillXpGrantRow.cs Skill XP row on a completion bundle.
server/NeonSprawl.Server/Game/Quests/QuestRewardBundleRow.cs Composite bundle DTO (ItemGrants, SkillXpGrants).
server/NeonSprawl.Server/Game/Quests/PrototypeE7M2QuestCatalogRules.cs E7M2 prototype gates + constants synced to validate_content.py.
docs/plans/NEO-125-implementation-plan.md This plan.

Files to modify

Path Rationale
server/NeonSprawl.Server/Game/Quests/QuestDefRow.cs Add optional CompletionRewardBundle property.
server/NeonSprawl.Server/Game/Quests/QuestDefinitionCatalogLoader.cs Register bundle schemas; parse bundle; run E7M2 gates; accept skill defs.
server/NeonSprawl.Server/Game/Quests/QuestCatalogPathResolution.cs Resolve paths for bundle / skill-xp-grant / reward-grant-row schemas.
server/NeonSprawl.Server/Game/Quests/QuestCatalogServiceCollectionExtensions.cs Pass SkillDefinitionCatalog into loader.
server/NeonSprawl.Server.Tests/Game/Quests/QuestCatalogTestPaths.cs Repo discovery helpers for new schema files.
server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionCatalogLoaderTests.cs Bundle fixtures, skill map, new failure tests; fix regressions from NEO-124 content.
server/README.md Document E7M2 bundle validation at quest catalog load.
docs/plans/E7M2-prototype-backlog.md Mark E7M2-02 AC when complete.
docs/decomposition/modules/documentation_and_implementation_alignment.md E7.M2 alignment — server bundle load landed.

Tests

Target Coverage
server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionCatalogLoaderTests.cs Happy path (repo + temp fixture with bundles); E7M2 presence, freeze, unknown item/skill, missing mission_reward allowlist; existing E7M1 tests unchanged; host DI resolve.
No new test files Extend existing loader test class per NEO-113 precedent.
Manual verification dotnet test server/NeonSprawl.Server.Tests --filter QuestDefinitionCatalogLoaderTests; boot dev server and confirm quest catalog Information log after NEO-124 content.

Open questions / risks

Question or risk Agent recommendation Status
Constants drift vs validate_content.py Sync comments on PrototypeE7M2QuestCatalogRulesPROTOTYPE_E7M2_*; same pattern as E7M1 rules. adopted
QuestDefRow bundle null on future non-prototype quests Optional property; E7M2 gates only enforce bundles on four frozen ids. adopted
Cross-namespace RewardGrantRow reuse Reuse Encounters struct — identical JSON shape; avoids duplicate DTO. adopted
HTTP quest-definitions omits bundle Out of scope; NEO-115 DTOs unchanged until a client story needs bundle preview. deferred

None beyond the above.