From 47ddde671f607dd2f2aa742c4be06f4648fcf94d Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 7 Jun 2026 15:23:21 -0400 Subject: [PATCH] NEO-125: add implementation plan for quest bundle catalog load --- docs/plans/NEO-125-implementation-plan.md | 156 ++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/plans/NEO-125-implementation-plan.md diff --git a/docs/plans/NEO-125-implementation-plan.md b/docs/plans/NEO-125-implementation-plan.md new file mode 100644 index 0000000..814154c --- /dev/null +++ b/docs/plans/NEO-125-implementation-plan.md @@ -0,0 +1,156 @@ +# 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](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) · Epic 7 Slice 2 · backlog **E7M2-02** | +| **Branch** | `NEO-125-e7m2-server-quest-catalog-reward-bundle-validation` | +| **Precursor** | [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) **`Done`** — `completionRewardBundle` schemas, catalog rows, CI gates (**landed on `main`**) | +| **Pattern** | [NEO-113](NEO-113-implementation-plan.md) — in-process C# gates mirroring `scripts/validate_content.py`; [NEO-101](NEO-101-implementation-plan.md) — multi-schema `$ref` registration (reward-table loader) | +| **Blocks** | [NEO-127](https://linear.app/neon-sprawl/issue/NEO-127) (router apply) via [NEO-126](https://linear.app/neon-sprawl/issue/NEO-126) parallel path; catalog must load before delivery stories | +| **Client counterpart** | none — infrastructure-only ([E7M2-prototype-backlog](E7M2-prototype-backlog.md#e7m2-02--server-quest-catalog-load-reward-bundle-validation)) | + +## Kickoff clarifications + +**No clarifications needed.** Linear goal (“CI parity”), E7M2 backlog in/out-of-scope, and [NEO-113](NEO-113-implementation-plan.md) / [NEO-124](NEO-124-implementation-plan.md) 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](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](E7M2-prototype-backlog.md#e7m2-02--server-quest-catalog-load-reward-bundle-validation)):** + +- 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. + +## Technical approach + +### 1. Bundle DTOs (`Game/Quests/`) + +- **`QuestSkillXpGrantRow`** — `SkillId`, `Amount` (record or sealed class, same style as `QuestObjectiveDefRow`). +- **`QuestRewardBundleRow`** — `ItemGrants` (`IReadOnlyList` from `Game.Encounters`), `SkillXpGrants` (`IReadOnlyList`). +- 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 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 `$ref`s, 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 `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.