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

149 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-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** |