NEO-125: add implementation plan for quest bundle catalog load
parent
0ae6bea327
commit
47ddde671f
|
|
@ -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<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.
|
||||||
Loading…
Reference in New Issue