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

169 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# NEO-124 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-124 |
| **Title** | E7M2-01: QuestRewardBundle schema + quest catalog extension + CI |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-124/e7m2-01-questrewardbundle-schema-quest-catalog-extension-ci |
| **Module** | [E7.M2 — RewardAndUnlockRouter](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) · Epic 7 Slice 2 · backlog **E7M2-01** |
| **Branch** | `NEO-124-e7m2-questrewardbundle-schema-quest-catalog-extension-ci` |
| **Precursor** | [E7.M1](../decomposition/modules/E7_M1_QuestStateMachine.md) **Ready** ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)[NEO-123](https://linear.app/neon-sprawl/issue/NEO-123)) · [NEO-43](https://linear.app/neon-sprawl/issue/NEO-43) **`MissionRewardSkillXpGrant`** prep · frozen item + skill catalogs |
| **Pattern** | [NEO-112](NEO-112-implementation-plan.md) — extend row JSON Schema + catalog + `scripts/validate_content.py` gate |
| **Blocks** | [NEO-125](https://linear.app/neon-sprawl/issue/NEO-125) (server catalog load), [NEO-126](https://linear.app/neon-sprawl/issue/NEO-126) (delivery store) |
| **Client counterpart** | none — infrastructure-only ([E7M2-prototype-backlog](E7M2-prototype-backlog.md#e7m2-01--questrewardbundle-schema--quest-catalog-extension--ci)) |
## Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Bundle array keys** | `item_grants` / `skill_xp_grants` (backlog prose) vs camelCase? | **`itemGrants` + `skillXpGrants`** — matches repo JSON (`fixedGrants`, `prerequisiteQuestIds`, `completionRewardBundle`). | **Adopted** — camelCase |
| **Skill XP `sourceKind` in content** | Explicit `sourceKind: mission_reward` per row vs omit? | **Omit** — v1 Slice 2 is `mission_reward` only; CI checks target skill **`allowedXpSourceKinds`** includes `mission_reward`; apply path hardcodes via **`MissionRewardSkillXpGrant`** (NEO-43). | **Adopted** — omit from content |
**Additional defaults (no kickoff question — settled by backlog / precedent):**
- **Item grant rows:** `$ref` existing [`reward-grant-row.schema.json`](../../content/schemas/reward-grant-row.schema.json) (`itemId` + `quantity`) — same shape as E5.M3 `fixedGrants`.
- **Bundle required on all four quests:** CI gate enforces presence + exact freeze-table contents (not optional omission on prototype ids).
- **Empty `itemGrants`:** omit property when no item rows (XP-only quests); schema allows optional `itemGrants` with `minItems: 0` when present.
- **At least one grant:** bundle must contain ≥1 item or skill XP row (`skillXpGrants` non-empty for all four frozen quests per freeze table).
## Goal, scope, and out-of-scope
**Goal:** Lock **`completionRewardBundle`** content shape and CI validation on the four frozen E7.M1 quests before server load (NEO-125+).
**In scope (from Linear + [E7M2-01](E7M2-prototype-backlog.md#e7m2-01--questrewardbundle-schema--quest-catalog-extension--ci)):**
- `content/schemas/quest-reward-bundle.schema.json`, `content/schemas/quest-skill-xp-grant.schema.json`.
- Extend [`quest-def.schema.json`](../../content/schemas/quest-def.schema.json) with optional **`completionRewardBundle`** (`$ref` bundle schema).
- Update [`prototype_quests.json`](../../content/quests/prototype_quests.json) — bundles per freeze table (below).
- [`scripts/validate_content.py`](../../scripts/validate_content.py): bundle schema validation via extended quest validator registry; item id cross-refs; skill id cross-refs; **`mission_reward`** allowlist alignment (skill row must list `mission_reward` in **`allowedXpSourceKinds`**); E7M2 prototype bundle freeze gate.
- Designer note in [E7_M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) (camelCase array keys + CI line) + [`content/README.md`](../../content/README.md) E7 Slice 2 paragraph + [CT.M1](../decomposition/modules/CT_M1_ContentValidationPipeline.md) quest-bundle PR gate paragraph.
**Out of scope (from Linear):**
- Server loader, delivery store, router, HTTP, Godot (NEO-125+).
- **`sourceKind`** field in content rows; unlock/flag grant kinds.
## Acceptance criteria checklist
- [x] PR gate validates quest JSON including optional completion bundles.
- [x] All four prototype quests include **`completionRewardBundle`** matching freeze table.
- [x] Invalid item/skill refs fail CI.
## Implementation reconciliation (shipped)
- **Schemas:** [`quest-skill-xp-grant.schema.json`](../../content/schemas/quest-skill-xp-grant.schema.json), [`quest-reward-bundle.schema.json`](../../content/schemas/quest-reward-bundle.schema.json); [`quest-def.schema.json`](../../content/schemas/quest-def.schema.json) extended with optional **`completionRewardBundle`**.
- **Catalog:** [`prototype_quests.json`](../../content/quests/prototype_quests.json) — four **`completionRewardBundle`** rows per E7.M2 freeze table (`itemGrants` / `skillXpGrants` camelCase).
- **CI:** `PROTOTYPE_E7M2_COMPLETION_BUNDLES`, presence/freeze/cross-ref gates in [`validate_content.py`](../../scripts/validate_content.py); **`mission_reward`** allowlist check via skill **`allowedXpSourceKinds`**.
- **Docs:** E7.M2 module CI line + camelCase keys; `content/README.md` E7 Slice 2 paragraph; CT.M1 quest-bundle bullet; E7M2-01 backlog checkboxes; alignment register E7.M2 row.
## Technical approach
### 1. `quest-skill-xp-grant.schema.json`
Draft 2020-12 object; `additionalProperties: false`. Required: **`skillId`** (pattern `^[a-z][a-z0-9_]*$`), **`amount`** (integer ≥ 1). No **`sourceKind`** — apply path uses **`MissionRewardSkillXpGrant`** (NEO-43).
### 2. `quest-reward-bundle.schema.json`
Draft 2020-12 object; `additionalProperties: false`. Optional: **`itemGrants`** (array of `$ref` reward-grant-row), **`skillXpGrants`** (array of `$ref` quest-skill-xp-grant). At least one array must be present with ≥1 row (enforce in schema via `anyOf` / `minItems` or post-schema CI helper). Omit **`itemGrants`** when empty (XP-only quests).
### 3. Extend `quest-def.schema.json`
Add optional property **`completionRewardBundle`** → `$ref` quest-reward-bundle. Remaining required fields unchanged.
### 4. Extend `_quest_def_validator()` registry
Register **`quest-reward-bundle.schema.json`** and **`quest-skill-xp-grant.schema.json`** (and existing **`reward-grant-row.schema.json`**) in the Draft 2020-12 `$ref` registry alongside step/objective schemas.
### 5. Catalog — `content/quests/prototype_quests.json`
Add **`completionRewardBundle`** to each of the four rows:
| Quest id | `itemGrants` | `skillXpGrants` |
|----------|--------------|-----------------|
| `prototype_quest_gather_intro` | *(omit)* | `salvage` **25** |
| `prototype_quest_refine_intro` | *(omit)* | `refine` **25** |
| `prototype_quest_combat_intro` | *(omit)* | `salvage` **25** |
| `prototype_quest_operator_chain` | `survey_drone_kit` ×1 | `salvage` **50** |
Example (gather intro):
```json
"completionRewardBundle": {
"skillXpGrants": [{ "skillId": "salvage", "amount": 25 }]
}
```
Example (operator chain):
```json
"completionRewardBundle": {
"itemGrants": [{ "itemId": "survey_drone_kit", "quantity": 1 }],
"skillXpGrants": [{ "skillId": "salvage", "amount": 50 }]
}
```
### 6. `validate_content.py` constants + gates
| Constant | Purpose |
|----------|---------|
| `PROTOTYPE_E7M2_COMPLETION_BUNDLES` | Map quest id → normalized `{ "itemGrants": [...], "skillXpGrants": [...] }` matching freeze table (keep in sync with E7.M2 module doc + future NEO-125 loader) |
| `PROTOTYPE_E7M2_MISSION_REWARD_SOURCE_KIND` | `"mission_reward"` — used in cross-ref gate only |
**New validators (run after quest schema pass, alongside existing E7M1 gates):**
- **`_prototype_e7m2_completion_bundle_presence_gate`:** every id in `PROTOTYPE_E7M1_QUEST_IDS` must have **`completionRewardBundle`** object.
- **`_prototype_e7m2_completion_bundle_freeze_gate`:** bundle contents must exactly match `PROTOTYPE_E7M2_COMPLETION_BUNDLES` (sorted grant rows for stable compare).
- **`_prototype_e7m2_completion_bundle_cross_ref_gate`:** each **`itemGrants[].itemId`** ∈ frozen item ids; each **`skillXpGrants[].skillId`** ∈ frozen skill ids; each target skills **`allowedXpSourceKinds`** must include **`mission_reward`** (load skill rows from validated skill catalog or reuse existing skill id map if available in `main()` scope).
Extend module docstring (quest catalogs bullet) and startup schema file existence checks for new schema paths.
### 7. Docs
- **`E7_M2_RewardAndUnlockRouter.md`** — update freeze prose to **`itemGrants` / `skillXpGrants`** (camelCase); add CI enforcement line under prototype freeze.
- **`content/README.md`** — **Prototype E7 Slice 2 — quest completion bundles (NEO-124)** paragraph: four quests require bundles; cross-ref rules; link E7M2 backlog + this plan.
- **`CT_M1_ContentValidationPipeline.md`** — extend quest catalog bullet with completion bundle validation.
- **`E7M2-prototype-backlog.md`** — E7M2-01 checkboxes when implementation completes.
- **`documentation_and_implementation_alignment.md`** — E7.M2 row note NEO-124 catalog in progress / landed.
### 8. No server/C#/client changes in this story.
## Files to add
| Path | Purpose |
|------|---------|
| `content/schemas/quest-skill-xp-grant.schema.json` | Single skill XP row on a quest completion bundle (`skillId`, `amount`). |
| `content/schemas/quest-reward-bundle.schema.json` | Composite bundle (`itemGrants`, `skillXpGrants`) attached to `QuestDef`. |
| `docs/plans/NEO-124-implementation-plan.md` | This plan. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `content/schemas/quest-def.schema.json` | Add optional **`completionRewardBundle`** property referencing bundle schema. |
| `content/quests/prototype_quests.json` | Add **`completionRewardBundle`** to all four frozen quest rows per freeze table. |
| `scripts/validate_content.py` | New schema paths; extended quest `$ref` registry; E7M2 bundle presence/freeze/cross-ref gates; docstring + schema existence checks. |
| `docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md` | camelCase grant keys + CI enforcement note under prototype freeze. |
| `content/README.md` | E7 Slice 2 quest completion bundle paragraph (NEO-124). |
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for quest completion bundles. |
| `docs/plans/E7M2-prototype-backlog.md` | Mark E7M2-01 AC complete when shipped. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M2 alignment row — NEO-124 catalog landed. |
## Tests
| Target | Coverage |
|--------|----------|
| **No new automated test files** | Content-only slice; regression surface is **`scripts/validate_content.py`** invoked by PR gate (NEO-112 / NEO-100 precedent). |
| **Manual verification** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — must pass with four bundles. Spot-check failures: typo **`itemId`**, unknown **`skillId`**, skill without **`mission_reward`** in **`allowedXpSourceKinds`**, missing **`completionRewardBundle`** on a quest row, wrong XP amount vs freeze table. |
## Open questions / risks
| Question or risk | Agent recommendation | Status |
|------------------|----------------------|--------|
| Constants drift vs future NEO-125 C# loader | Sync comment on `PROTOTYPE_E7M2_COMPLETION_BUNDLES` pointing at E7.M2 module freeze + future server rules class. | **adopted** |
| Backlog prose still says `item_grants` / `skill_xp_grants` | Update E7M2 backlog + E7.M2 module to camelCase when implementing (kickoff decision). | **adopted** |
| Bundle compare stability (grant row order) | Normalize/sort grant rows in freeze gate before compare. | **adopted** |
None beyond the above.