NEO-112: add kickoff implementation plan for QuestDef catalog + CI
parent
47105ccd90
commit
be1dae3872
|
|
@ -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** |
|
||||||
Loading…
Reference in New Issue