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 Done — completionRewardBundle 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_rewardallowlist) — not cross-ref-only. - Parse typed bundle rows onto
QuestDefRownow — backlog “extend catalog loader / registry types”; NEO-127 router reads catalog without re-parsing JSON. - Reuse
RewardGrantRow(Encounters) foritemGrants— same schema shape as E5.M3 / CI; addQuestSkillXpGrantRow+QuestRewardBundleRowunderGame/Quests/. - Separate
PrototypeE7M2QuestCatalogRules— mirrors E7M1 rules class + PythonPROTOTYPE_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[].itemIdagainst the item catalog andskillXpGrants[].skillIdagainst the skill catalog +allowedXpSourceKinds(must includemission_reward). - Unit tests (AAA) for happy path and broken bundle refs; host boot still resolves catalog.
server/README.mdquest-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 testcovers loader gates.
Implementation reconciliation (shipped)
- DTOs:
QuestSkillXpGrantRow,QuestRewardBundleRow; optionalCompletionRewardBundleonQuestDefRow. - Rules:
PrototypeE7M2QuestCatalogRules— presence, freeze-table, cross-ref +mission_rewardallowlist gates synced tovalidate_content.py. - Loader: registers bundle-related JSON Schemas; parses
completionRewardBundle; acceptsSkillDefinitionCatalog.ByIdfrom DI. - Tests: 25 AAA tests in
QuestDefinitionCatalogLoaderTests(5 E7M2 loader cases + updated prototype fixture with bundles);PrototypeE7M2QuestCatalogRulesTests(4 direct cross-ref / allowlist cases). - Docs:
server/README.mdquest catalog E7M2 paragraph; E7M2-02 backlog AC checked.
Technical approach
1. Bundle DTOs (Game/Quests/)
QuestSkillXpGrantRow—SkillId,Amount(record or sealed class, same style asQuestObjectiveDefRow).QuestRewardBundleRow—ItemGrants(IReadOnlyList<RewardGrantRow>fromGame.Encounters),SkillXpGrants(IReadOnlyList<QuestSkillXpGrantRow>).- Extend
QuestDefRowwith optionalCompletionRewardBundle(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):
reward-grant-row.schema.jsonquest-skill-xp-grant.schema.jsonquest-reward-bundle.schema.json- 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: whencompletionRewardBundlepresent, parse intoQuestRewardBundleRow(omit emptyitemGrantsarrays).- After existing E7M1 gates, run E7M2 rules in order: presence → freeze → cross-ref.
- Extend
Load(...)signature withIReadOnlyDictionary<string, SkillDefRow> skillDefsById(orSkillDefinitionCatalog+ extract.ByIdin DI factory).
5. DI (QuestCatalogServiceCollectionExtensions)
- Inject
SkillDefinitionCatalogalongside item / recipe / encounter catalogs. - Pass
skillCatalog.ByIdinto loader (skills already register before quests inProgram.cs).
6. Tests (QuestDefinitionCatalogLoaderTests)
- Update
ValidPrototypeCatalogJsonwithcompletionRewardBundleon all four quests (match repo / freeze table). CreateTempContentLayout: copy bundle + grant-row schemas from repo (add helpers onQuestCatalogTestPaths).- Extend
LoadCataloghelper with frozen skill defs map (reuse repo skill catalog discovery or inline salvage/refine rows withmission_rewardinAllowedXpSourceKinds). - New negative tests (AAA):
- Missing
completionRewardBundleon a frozen quest id. - Wrong freeze-table XP amount.
- Unknown
itemIdin bundle. - Unknown
skillIdin bundle. - Skill missing
mission_rewardinallowedXpSourceKinds.
- Missing
- Existing
Load_ShouldSucceed_WhenCatalogMatchesRepoPrototypeFileandHost_ShouldResolveQuestCatalogFromDimust 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/PrototypeE7M2QuestCatalogRulesTests.cs |
Direct unit tests for TryGetCompletionBundleCrossRefError (item/skill catalog + allowlist paths). |
server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionCatalogLoaderTests.cs |
Happy path (repo + temp fixture with bundles); E7M2 presence, freeze, allowlist deny; host DI resolve. |
| No other new test files | Loader + rules classes 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 PrototypeE7M2QuestCatalogRules → PROTOTYPE_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.