diff --git a/docs/plans/NEO-124-implementation-plan.md b/docs/plans/NEO-124-implementation-plan.md new file mode 100644 index 0000000..bd58c99 --- /dev/null +++ b/docs/plans/NEO-124-implementation-plan.md @@ -0,0 +1,161 @@ +# 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 + +- [ ] PR gate validates quest JSON including optional completion bundles. +- [ ] All four prototype quests include **`completionRewardBundle`** matching freeze table. +- [ ] Invalid item/skill refs fail CI. + +## 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 skill’s **`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.