diff --git a/docs/plans/NEO-112-implementation-plan.md b/docs/plans/NEO-112-implementation-plan.md new file mode 100644 index 0000000..7230ff6 --- /dev/null +++ b/docs/plans/NEO-112-implementation-plan.md @@ -0,0 +1,141 @@ +# NEO-112 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-112 | +| **Title** | E7M1-01: Prototype QuestDef catalog + schemas + CI | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-112/e7m1-01-prototype-questdef-catalog-schemas-ci | +| **Module** | [E7.M1 — QuestStateMachine](../decomposition/modules/E7_M1_QuestStateMachine.md) · Epic 7 Slice 1 · backlog **E7M1-01** | +| **Branch** | `NEO-112-e7m1-prototype-questdef-catalog-schemas-ci` | +| **Precursor** | [E3.M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) gather **Ready** · [E3.M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) craft **Ready** · [E5.M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) encounter **Ready** (frozen item/recipe/encounter ids) | +| **Pattern** | [NEO-100](NEO-100-implementation-plan.md) — row JSON Schema + catalog envelope + `scripts/validate_content.py` gate | +| **Blocks** | [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) — server quest catalog load (fail-fast) | + +## Kickoff clarifications + +**No clarifications needed.** Scope, frozen quest ids, step/objective graph, prerequisite rules, objective kinds, and cross-ref targets are fully specified in [E7M1 kickoff decisions](E7M1-prototype-backlog.md#kickoff-decisions-decomposition-defaults), [E7.M1 freeze table](../decomposition/modules/E7_M1_QuestStateMachine.md#prototype-slice-1-freeze-e7m1-01), and [E7M1-01 backlog entry](E7M1-prototype-backlog.md#e7m1-01--prototype-questdef-catalog--schemas--ci). Catalog envelope and CI constant naming follow NEO-100 / NEO-87 precedent without ambiguity. + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| — | — | — | **No blocking decisions** (see above). | + +## Goal, scope, and out-of-scope + +**Goal:** Lock content shape and CI validation for **four** frozen `QuestDef` rows before server load (NEO-113+). + +**In scope (from Linear + [E7M1-01](E7M1-prototype-backlog.md#e7m1-01--prototype-questdef-catalog--schemas--ci)):** + +- `content/schemas/quest-def.schema.json`, `content/schemas/quest-step-def.schema.json`, `content/schemas/quest-objective-def.schema.json`. +- `content/quests/prototype_quests.json` — four rows matching the module doc freeze table. +- `scripts/validate_content.py` — schema validation, duplicate ids, four-quest allowlist, cross-ref **`itemId`** / **`recipeId`** / **`encounterId`** to frozen catalogs, acyclic **`prerequisiteQuestIds`**, objective **`kind`** enum, chain terminal step **`inventory_has_item`** **`contract_handoff_token`** ×1 gate. +- Designer note / CI rules in [E7_M1](../decomposition/modules/E7_M1_QuestStateMachine.md) (CI enforcement line already present) + **CT.M1** PR gate paragraph + `content/README.md` quest row. +- `documentation_and_implementation_alignment.md` E7.M1 row → note NEO-112 catalog when complete. + +**Out of scope (from Linear):** + +- Server loader, runtime engine, HTTP, Godot (NEO-113+). +- **`IQuestDefinitionRegistry`**, player quest state, objective hooks (E7M1-03+). +- **Client counterpart:** none — server-only content + CI; player-visible work starts at [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) / [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). + +## Acceptance criteria checklist + +- [ ] PR gate validates quest JSON against schema. +- [ ] Exactly four prototype quest ids; duplicate `id` fails CI. +- [ ] Every objective reference resolves to a frozen catalog id. +- [ ] Stable id list documented in module doc freeze box (already present; verify after ship). + +## Technical approach + +1. **`quest-objective-def.schema.json`:** Draft 2020-12 object; `additionalProperties: false`. Required: **`id`**, **`kind`**. **`kind`** enum: **`gather_item`**, **`craft_recipe`**, **`encounter_complete`**, **`inventory_has_item`**. Use **`oneOf`** branches keyed by **`kind`**: + - **`gather_item`**: required **`itemId`**, **`quantity`** (integer ≥ 1). + - **`craft_recipe`**: required **`recipeId`**, **`quantity`** (integer ≥ 1). + - **`encounter_complete`**: required **`encounterId`** (no quantity). + - **`inventory_has_item`**: required **`itemId`**, **`quantity`** (integer ≥ 1). + - Id patterns: `^[a-z][a-z0-9_]*$` (same as other catalogs). + +2. **`quest-step-def.schema.json`:** Required: **`id`**, **`displayName`**, **`objectives`** (non-empty array of `$ref` quest-objective-def). Reject duplicate objective **`id`** within a step in CI (post-schema). + +3. **`quest-def.schema.json`:** Required: **`id`**, **`displayName`**, **`prerequisiteQuestIds`** (array of strings; may be empty), **`steps`** (non-empty array of `$ref` quest-step-def). Reject duplicate step **`id`** within a quest in CI (post-schema). + +4. **Catalog file `content/quests/prototype_quests.json`:** + + Envelope: **`schemaVersion`: 1**, top-level **`quests`** array. Four rows per freeze table: + + | `id` | `displayName` | `prerequisiteQuestIds` | Steps | + |------|---------------|------------------------|-------| + | `prototype_quest_gather_intro` | Intro: Salvage Run | `[]` | 1× **`gather_item`** `scrap_metal_bulk` ×3 | + | `prototype_quest_refine_intro` | Intro: Refine Stock | `[prototype_quest_gather_intro]` | 1× **`craft_recipe`** `refine_scrap_standard` ×1 | + | `prototype_quest_combat_intro` | Intro: Clear the Pocket | `[]` | 1× **`encounter_complete`** `prototype_combat_pocket` | + | `prototype_quest_operator_chain` | Operator Chain | all three intros | 4 steps: gather ×5 → refine → make_field_stim_mk0 → **`inventory_has_item`** token ×1 | + + Step/objective **`id`** values are stable content keys (e.g. `gather_intro_step_salvage`, `chain_obj_token`); chosen for readability and future HTTP projection (E7M1-04). + +5. **`validate_content.py` constants (keep in sync with E7.M1 module doc + future NEO-113 loader):** + + | Constant | Value | + |----------|--------| + | `PROTOTYPE_E7M1_QUEST_IDS` | `{ prototype_quest_gather_intro, prototype_quest_refine_intro, prototype_quest_combat_intro, prototype_quest_operator_chain }` | + | `PROTOTYPE_E7M1_CHAIN_QUEST_ID` | `prototype_quest_operator_chain` | + | `PROTOTYPE_E7M1_CHAIN_TERMINAL_ITEM_ID` | `contract_handoff_token` | + +6. **`validate_content.py` validators:** + + - Add schema paths + dir: `QUEST_DEF_SCHEMA`, `QUEST_STEP_DEF_SCHEMA`, `QUEST_OBJECTIVE_DEF_SCHEMA`, `QUESTS_DIR`. + - Register `$ref` between quest / step / objective schemas in the Draft 2020-12 registry (same pattern as recipe + recipe-io-row). + - **`_validate_quest_catalogs`:** envelope `schemaVersion: 1`, top-level **`quests`** array; validate each row; track `seen_ids`; reject duplicate quest **`id`** across files; build `id_to_row` map. + - **`_prototype_e7m1_quest_gate`:** ids must equal `PROTOTYPE_E7M1_QUEST_IDS`. + - **`_prototype_e7m1_quest_prerequisite_gate`:** every **`prerequisiteQuestIds`** entry must exist in `seen_ids`; graph must be **acyclic** (DFS / Kahn). + - **`_prototype_e7m1_quest_cross_ref_gate`:** per objective — **`itemId`** ∈ `PROTOTYPE_SLICE1_ITEM_IDS`; **`recipeId`** ∈ `PROTOTYPE_SLICE3_RECIPE_IDS`; **`encounterId`** ∈ `PROTOTYPE_E5M3_ENCOUNTER_IDS` (only when present for kind). + - **`_prototype_e7m1_chain_terminal_gate`:** operator chain last step's sole objective is **`inventory_has_item`** with **`itemId`** `contract_handoff_token`, **`quantity`** 1. + - Run quest validation **after** item, recipe, and encounter catalogs (cross-ref dependencies). + - Extend module docstring, startup schema file checks, success summary with quest file/id counts. + +7. **Docs:** + + - `E7_M1_QuestStateMachine.md` — verify CI enforcement line under freeze table matches shipped gates. + - `CT_M1_ContentValidationPipeline.md` — quest catalog PR gate paragraph. + - `content/README.md` — add `quests/` table row + E7M1 prototype paragraph (four quest ids, cross-ref rules). + - `documentation_and_implementation_alignment.md` — E7.M1 row notes NEO-112 catalog when complete. + - `E7M1-prototype-backlog.md` — E7M1-01 checkboxes when implementation completes. + +8. **No server/C#/client changes** in this story. + +## Files to add + +| Path | Purpose | +|------|---------| +| `content/schemas/quest-objective-def.schema.json` | JSON Schema for a single quest objective row (discriminated by `kind`). | +| `content/schemas/quest-step-def.schema.json` | JSON Schema for a quest step (objectives array). | +| `content/schemas/quest-def.schema.json` | JSON Schema for a single `QuestDef` row (steps + prerequisites). | +| `content/quests/prototype_quests.json` | Prototype four-quest catalog (`schemaVersion` + `quests` array). | +| `docs/plans/NEO-112-implementation-plan.md` | This plan. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `scripts/validate_content.py` | Load/validate quest catalogs; duplicate `id`; E7M1 four-id gate; item/recipe/encounter cross-refs; acyclic prerequisites; chain terminal token gate; success summary. | +| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for quest catalog validation. | +| `content/README.md` | `quests/` path row + E7M1 prototype CI paragraph. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M1 / NEO-112 status after catalog lands. | + +## Tests + +| Item | Coverage | +|------|----------| +| **CI / PR gate** | `.github/workflows/pr-gate.yml` runs `python scripts/validate_content.py` — extended script is the primary regression signal. | +| **Manual negative cases** | From repo root after implementation: (1) duplicate quest `id` → non-zero exit; (2) remove frozen quest id → gate error; (3) unknown `itemId` / `recipeId` / `encounterId` in objective → cross-ref error; (4) cyclic `prerequisiteQuestIds` → gate error; (5) unknown prerequisite id → gate error; (6) operator chain terminal step not `inventory_has_item` token ×1 → gate error; (7) invalid `kind` or missing required field per kind → schema error. | +| **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports quest catalog + existing catalogs OK. | + +No dedicated pytest module in-repo today (same as NEO-100 / NEO-87). + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| Objective `oneOf` schema complexity vs flat optional fields | **`oneOf` per `kind`** — mirrors encounter `completionCriteria`; catches wrong-field-at-wrong-kind at schema time. | **adopted** | +| Combat intro has no gather prerequisite (parallel onboarding path) | **Ship as freeze table specifies** — chain quest requires all three intros; refine only requires gather intro. | **adopted** | +| Step/objective ids not in freeze table | **Stable descriptive ids in catalog JSON** — required for E7M1-04 HTTP projection; not part of four-quest id gate. | **adopted** | +| `content/README.md` has no quest paragraph yet | **Add paragraph on ship** — unlike NEO-100 (pre-landed), quests row is new. | **adopted** |