# 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 - [x] PR gate validates quest JSON against schema. - [x] Exactly four prototype quest ids; duplicate `id` fails CI. - [x] Every objective reference resolves to a frozen catalog id. - [x] Stable id list documented in module doc freeze box (already present; verify after ship). ## Implementation reconciliation (shipped) - **Schemas:** [`quest-objective-def.schema.json`](../../content/schemas/quest-objective-def.schema.json), [`quest-step-def.schema.json`](../../content/schemas/quest-step-def.schema.json), [`quest-def.schema.json`](../../content/schemas/quest-def.schema.json). - **Catalog:** [`prototype_quests.json`](../../content/quests/prototype_quests.json) — four frozen quest rows per E7.M1 freeze table. - **CI:** `PROTOTYPE_E7M1_QUEST_IDS`, prerequisite acyclic gate, objective cross-ref gates, chain terminal token gate in [`validate_content.py`](../../scripts/validate_content.py); duplicate quest/step/objective id rejection. - **Docs:** CT.M1 quest paragraph, `content/README.md` quests row + E7M1 paragraph, alignment register E7.M1 row, E7M1-01 backlog checkboxes. ## 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** |