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

157 lines
10 KiB
Markdown

# 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<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 `$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.