From be1dae3872512b450a73fa36a3d43d540ac1a1c6 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 21:12:54 -0400 Subject: [PATCH 1/8] NEO-112: add kickoff implementation plan for QuestDef catalog + CI --- docs/plans/NEO-112-implementation-plan.md | 141 ++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 docs/plans/NEO-112-implementation-plan.md 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** | From 140d8573984b9a04b5457ac3842eb51e9a81a2ad Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 21:13:03 -0400 Subject: [PATCH 2/8] NEO-112: add E7M1 prototype backlog and decomposition cross-links --- .../epics/epic_07_quest_faction.md | 10 +- .../modules/E7_M1_QuestStateMachine.md | 37 +- ...umentation_and_implementation_alignment.md | 1 + .../modules/module_dependency_register.md | 2 + docs/plans/E7M1-prototype-backlog.md | 392 ++++++++++++++++++ 5 files changed, 439 insertions(+), 3 deletions(-) create mode 100644 docs/plans/E7M1-prototype-backlog.md diff --git a/docs/decomposition/epics/epic_07_quest_faction.md b/docs/decomposition/epics/epic_07_quest_faction.md index 9791736..02bfe0e 100644 --- a/docs/decomposition/epics/epic_07_quest_faction.md +++ b/docs/decomposition/epics/epic_07_quest_faction.md @@ -46,14 +46,18 @@ Provide a quest state machine and reward routing that ties gathering, crafting, ## Implementation Slices (Backlog-Ready) + + ### Slice 1 - Quest core and persistence - Scope: E7.M1 supporting branching steps and failure/reset rules for prototype chain. -- Dependencies: E3.M2, E5.M1 +- Dependencies: E3.M2, E5.M1 (gather/craft/encounter hooks on main) - Acceptance criteria: - 3-5 onboarding quests + one chain requiring gather/craft/combat complete without duped rewards. - Telemetry hooks: `quest_start`, `quest_step_complete`, `quest_complete`, funnel times. +**Linear backlog:** [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) — [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Verify in **Godot**, not Bruno-only. + ### Slice 2 - Reward and unlock routing - Scope: E7.M2 delivering XP, items, blueprint unlocks, and flags for world gates. @@ -85,7 +89,9 @@ Provide a quest state machine and reward routing that ties gathering, crafting, - Risk: Rewards break parity or economy. - Mitigation: Lint bundles against E6.M4 and E3.M5 policies. + + ## Definition of Done -- Prototype quest counts and integrated chain meet master plan minimums. +- Prototype quest counts and integrated chain meet master plan minimums **in the Godot client** (Slice 1 capstone [NEO-123](../../manual-qa/NEO-123.md)), not Bruno-only. - Pre-production adds faction + contracts with validation. diff --git a/docs/decomposition/modules/E7_M1_QuestStateMachine.md b/docs/decomposition/modules/E7_M1_QuestStateMachine.md index 5b9f225..35cc282 100644 --- a/docs/decomposition/modules/E7_M1_QuestStateMachine.md +++ b/docs/decomposition/modules/E7_M1_QuestStateMachine.md @@ -7,7 +7,8 @@ | **Module ID** | E7.M1 | | **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) | | **Stage target** | Prototype | -| **Status** | Planned (see [dependency register](module_dependency_register.md)) | +| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) (decomposed; implementation not started) | +| **Linear** | Label **`E7.M1`** · [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) | ## Purpose @@ -45,6 +46,40 @@ Data-driven quest lifecycle: start, step progression, branching, failure/reset, See Epic 7 **Slice 1 — Quest core and persistence**: 3–5 onboarding quests plus one chain across gather/craft/combat; telemetry `quest_start`, `quest_step_complete`, `quest_complete`, funnel times. +## Linear backlog (decomposed) + +Full story tables, kickoff defaults, and **`blockedBy` graph:** [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md). + +| Slug | Linear | +|------|--------| +| E7M1-01 | [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) | +| E7M1-02 | [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) | +| E7M1-03 | [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) | +| E7M1-04 | [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) | +| E7M1-05 | [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) | +| E7M1-06 | [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) | +| E7M1-07 | [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) | +| E7M1-08 | [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) | +| E7M1-09 | [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) | +| E7M1-10 | [NEO-121](https://linear.app/neon-sprawl/issue/NEO-121) | +| E7M1-11 | [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) | +| E7M1-12 | [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) | + +## Prototype Slice 1 freeze (E7M1-01) + +The **first shipped quest spine** is **frozen** for prototype tuning until a deliberate migration issue expands the roster. + +| `QuestDef.id` | `displayName` | `prerequisiteQuestIds` | Steps (summary) | +|---------------|---------------|------------------------|-----------------| +| **`prototype_quest_gather_intro`** | Intro: Salvage Run | *(none)* | 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 | *(none)* | 1× **`encounter_complete`**: **`prototype_combat_pocket`** | +| **`prototype_quest_operator_chain`** | Operator Chain | **`prototype_quest_gather_intro`**, **`prototype_quest_refine_intro`**, **`prototype_quest_combat_intro`** | 4 steps: gather **`scrap_metal_bulk` ×5** → craft **`refine_scrap_standard`** → craft **`make_field_stim_mk0`** → **`inventory_has_item`** **`contract_handoff_token` ×1** | + +**CI enforcement (NEO-112):** `scripts/validate_content.py` requires **exactly** these four quest ids; objective cross-refs to frozen item/recipe/encounter catalogs; acyclic **`prerequisiteQuestIds`**; chain quest terminal step uses **`contract_handoff_token`** from [E5.M3 encounter loot](E5_M3_EncounterAndRewardTables.md#prototype-slice-3-freeze-e5m3-01). + +**Reward policy (Slice 1):** Quest completion updates **`QuestStepState` only** — no duplicate item/XP grants. Gather/craft/encounter paths keep existing payouts; [E7.M2](E7_M2_RewardAndUnlockRouter.md) adds **`QuestRewardBundle`** apply in Slice 2. + ## Risks and telemetry - Scripting weight: keep `QuestDef` data-first; templates in decomposition/tooling path per epic. diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index d2eb2e4..5b77d2d 100644 --- a/docs/decomposition/modules/documentation_and_implementation_alignment.md +++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md @@ -59,6 +59,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not | E5.M1 | Ready | **NEO-76 landed:** frozen prototype four-ability catalog in [`content/abilities/prototype_abilities.json`](../../../content/abilities/prototype_abilities.json); [`ability-def.schema.json`](../../../content/schemas/ability-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) E5M1 four-id gate ([NEO-76](../../plans/NEO-76-implementation-plan.md)). **NEO-77 landed:** fail-fast server load of `content/abilities/*_abilities.json` at startup — `server/NeonSprawl.Server/Game/Combat/` ([NEO-77](../../plans/NEO-77-implementation-plan.md)); [server README — Ability catalog](../../../server/README.md#ability-catalog-contentabilities-neo-77). **NEO-79 landed:** injectable **`IAbilityDefinitionRegistry`** + DI; hotbar/cast use **`TryNormalizeKnown`** ([NEO-79](../../plans/NEO-79-implementation-plan.md)); Bruno `bruno/neon-sprawl-server/hotbar-loadout/` + `ability-cast/` unknown-ability deny smokes. **NEO-78 landed:** **`GET /game/world/ability-definitions`** — `AbilityDefinitionsWorldApi` + DTOs in `Game/Combat/` ([NEO-78](../../plans/NEO-78-implementation-plan.md)); [server README — Ability definitions (NEO-78)](../../../server/README.md#ability-definitions-neo-78); Bruno `bruno/neon-sprawl-server/ability-definitions/`. **NEO-80 landed:** **`ICombatEntityHealthStore`** / **`InMemoryCombatEntityHealthStore`** — lazy init (Slice 1 flat 100 HP; **superseded by [NEO-91](../../plans/NEO-91-implementation-plan.md)** per-archetype catalog max HP + three NPC instance ids); DI via **`AddCombatEntityHealthStore()`** ([NEO-80](../../plans/NEO-80-implementation-plan.md)); [server README — Combat entity health (NEO-80, NEO-91)](../../../server/README.md#combat-entity-health-neo-80-neo-91). **NEO-81 landed:** **`CombatOperations.TryResolve`** + **`CombatResult`** + **`CombatReasonCodes`** — defeated pre-check deny, catalog damage ([NEO-81](../../plans/NEO-81-implementation-plan.md)); [server README — Combat engine (NEO-81)](../../../server/README.md#combat-engine-neo-81). **NEO-82 landed:** **`AbilityCastApi`** → **`CombatOperations.TryResolve`**; nested wire **`combatResolution`** on accept; per-ability catalog cooldown; **`target_defeated`** cast deny ([NEO-82](../../plans/NEO-82-implementation-plan.md)); [server README — Ability cast (NEO-82)](../../../server/README.md#ability-cast-neo-31-neo-82); Bruno `bruno/neon-sprawl-server/ability-cast/` defeat spine. **NEO-83 landed:** **`GET /game/world/combat-targets`** — `CombatTargetsWorldApi` + DTOs; authoritative HP snapshot from **`ICombatEntityHealthStore`** ([NEO-83](../../plans/NEO-83-implementation-plan.md)); [server README — Combat targets snapshot (NEO-83)](../../../server/README.md#combat-targets-snapshot-neo-83); Bruno `bruno/neon-sprawl-server/combat-targets/`. **NEO-44 landed:** **`CombatDefeatGigXpGrant`** on cast **`targetDefeated`** — **25** gig XP to **`breach`** via **`IPlayerGigProgressionStore`** (V007); **`GET …/gig-progression`** ([NEO-44](../../plans/NEO-44-implementation-plan.md), [`NEO-44` manual QA](../../manual-qa/NEO-44.md)); [server README — Gig progression (NEO-44)](../../../server/README.md#gig-progression-snapshot-neo-44); Bruno `bruno/neon-sprawl-server/gig-progression/`. **NEO-84 landed:** comment-only **`ability_used`** / **`enemy_defeat`** telemetry hook sites in **`CombatOperations.TryResolve`**; reserved **`encounter_start`** / **`player_death`** ([NEO-84](../../plans/NEO-84-implementation-plan.md)); [server README — Combat telemetry hooks (NEO-84)](../../../server/README.md#combat-telemetry-hooks-neo-84). **NEO-85 landed:** client combat HUD — **`ability_cast_client.gd`** parses **`combatResolution`**; **`combat_targets_client.gd`**, **`CastFeedbackLabel`** resolution copy, **`CombatTargetHpLabel`**; event-driven GET refresh ([NEO-85](../../plans/NEO-85-implementation-plan.md), [`NEO-85` manual QA](../../manual-qa/NEO-85.md)); `client/README.md` combat HUD section. **NEO-86 landed:** playable combat capstone — **`gig_progression_client.gd`**, **`GigXpLabel`**, defeat-triggered gig GET refresh; capstone manual QA [`NEO-86`](../../manual-qa/NEO-86.md); `client/README.md` end-to-end combat loop section ([NEO-86](../../plans/NEO-86-implementation-plan.md)). **Epic 5 Slice 1 client capstone complete.** **Backlog decomposed:** Epic 5 Slice 1 — **E5M1-01**–**E5M1-12** landed. **Precursor landed:** E1.M4 cast funnel (NEO-28/31/32). | [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1](E5_M1_CombatRulesEngine.md), [NEO-77](../../plans/NEO-77-implementation-plan.md), [NEO-79](../../plans/NEO-79-implementation-plan.md), [NEO-78](../../plans/NEO-78-implementation-plan.md), [NEO-80](../../plans/NEO-80-implementation-plan.md), [NEO-81](../../plans/NEO-81-implementation-plan.md), [NEO-82](../../plans/NEO-82-implementation-plan.md), [NEO-83](../../plans/NEO-83-implementation-plan.md), [NEO-44](../../plans/NEO-44-implementation-plan.md), [NEO-84](../../plans/NEO-84-implementation-plan.md), [NEO-85](../../plans/NEO-85-implementation-plan.md), [NEO-86](../../plans/NEO-86-implementation-plan.md), [E1M4-prototype-backlog.md](../../plans/E1M4-prototype-backlog.md) | | E5.M2 | Ready | **NEO-87 landed:** frozen prototype three-behavior catalog in [`content/npc-behaviors/prototype_npc_behaviors.json`](../../../content/npc-behaviors/prototype_npc_behaviors.json); [`npc-behavior-def.schema.json`](../../../content/schemas/npc-behavior-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) E5M2 three-id gate ([NEO-87](../../plans/NEO-87-implementation-plan.md)). **NEO-88 landed:** fail-fast server load of `content/npc-behaviors/*_npc_behaviors.json` at startup — `server/NeonSprawl.Server/Game/Npc/` ([NEO-88](../../plans/NEO-88-implementation-plan.md)); [server README — NPC behavior catalog](../../../server/README.md#npc-behavior-catalog-contentnpc-behaviors-neo-88). **NEO-89 landed:** injectable **`INpcBehaviorDefinitionRegistry`** + DI; **`PrototypeNpcBehaviorRegistry`** id constants ([NEO-89](../../plans/NEO-89-implementation-plan.md)). **NEO-90 landed:** **`GET /game/world/npc-behavior-definitions`** — `NpcBehaviorDefinitionsWorldApi` + DTOs in `Game/Npc/` ([NEO-90](../../plans/NEO-90-implementation-plan.md)); [server README — NPC behavior definitions (NEO-90)](../../../server/README.md#npc-behavior-definitions-neo-90); Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`. **NEO-91 landed:** **`PrototypeNpcRegistry`** + combat-target migration — three NPC instance ids replace alpha/beta; per-archetype max HP from behavior catalog ([NEO-91](../../plans/NEO-91-implementation-plan.md)); [server README — Combat entity health (NEO-91)](../../../server/README.md#combat-entity-health-neo-80-neo-91). **NEO-92 landed:** **`IThreatStateStore`** + **`AggroOperations`** — first-hit acquire on damaging cast, leash clear on move/cast; cast/move/fixture hooks ([NEO-92](../../plans/NEO-92-implementation-plan.md)); [server README — Threat / aggro state (NEO-92)](../../../server/README.md#threat--aggro-state-neo-92). **NEO-93 landed:** **`INpcRuntimeStateStore`** + **`NpcRuntimeOperations.AdvanceAll`** — catalog-driven behavior state machine, lazy tick advance with delta cap; dev fixture reset ([NEO-93](../../plans/NEO-93-implementation-plan.md)); [server README — NPC runtime behavior state (NEO-93)](../../../server/README.md#npc-runtime-behavior-state-neo-93). **NEO-94 landed:** **`GET /game/world/npc-runtime-snapshot`** — `NpcRuntimeSnapshotWorldApi` + DTOs; lazy **`AdvanceAll`** on poll; nested **`activeTelegraph`** with server-computed **`windupRemainingSeconds`** ([NEO-94](../../plans/NEO-94-implementation-plan.md)); [server README — NPC runtime snapshot (NEO-94)](../../../server/README.md#npc-runtime-snapshot-neo-94); Bruno `bruno/neon-sprawl-server/npc-runtime-snapshot/`. **NEO-95 landed:** **`IPlayerCombatHealthStore`** + **`NpcAttackOperations.TryResolveTelegraphComplete`** — telegraph resolve via behavior **`attackAbilityId`** + ability **`maxRange`** / **`baseDamage`** ([NEO-95](../../plans/NEO-95-implementation-plan.md), [NEO-98](../../plans/NEO-98-implementation-plan.md)); [server README — Session player combat HP (NEO-95)](../../../server/README.md#session-player-combat-hp-neo-95); Bruno `bruno/neon-sprawl-server/combat-health/`. **NEO-96 landed:** comment-only **`telegraph_fired`** + **`npc_state_transition`** hook sites in **`NpcRuntimeOperations.AdvanceAll`** ([NEO-96](../../plans/NEO-96-implementation-plan.md)); [server README — NPC runtime telemetry hooks (NEO-96)](../../../server/README.md#npc-runtime-telemetry-hooks-neo-96). **NEO-97 landed:** client telegraph HUD — **`npc_runtime_client.gd`**, **`player_combat_health_client.gd`**, **`npc_runtime_hud_state.gd`**, **`TelegraphLabel`** / **`NpcStateLabel`** / **`PlayerCombatHpLabel`**; combat-active ~1 Hz poll ([NEO-97](../../plans/NEO-97-implementation-plan.md), [`NEO-97` manual QA](../../manual-qa/NEO-97.md)); [client README — NPC runtime + telegraph HUD (NEO-97)](../../../client/README.md#npc-runtime--telegraph-hud-neo-97). **NEO-98 landed:** playable NPC telegraph combat capstone — [`NEO-98` manual QA](../../manual-qa/NEO-98.md); `client/README.md` end-to-end NPC telegraph combat loop section ([NEO-98](../../plans/NEO-98-implementation-plan.md)). **NEO-98 server integration:** defeat clears aggro/runtime (**`TryStopOnTargetDefeat`**); telegraph damage range-gated via [`prototype_npc_abilities.json`](../../../content/abilities/prototype_npc_abilities.json) + behavior **`attackAbilityId`**. **Epic 5 Slice 2 client capstone complete.** **Backlog decomposed:** Epic 5 Slice 2 — [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md) **E5M2-01** [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98) **landed**. Three archetypes (`prototype_melee_pressure`, `prototype_ranged_control`, `prototype_elite_mini_boss`); replaces alpha/beta dummies with **`prototype_npc_melee` / `ranged` / `elite`**. Owns **`ThreatState`**, **`TelegraphEvent`**, **`GET …/npc-runtime-snapshot`**, session **`IPlayerCombatHealthStore`**. Upstream **E5.M1 Ready**. | [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2](E5_M2_NpcAiAndBehaviorProfiles.md), [NEO-87](../../plans/NEO-87-implementation-plan.md), [NEO-88](../../plans/NEO-88-implementation-plan.md), [NEO-89](../../plans/NEO-89-implementation-plan.md), [NEO-90](../../plans/NEO-90-implementation-plan.md), [NEO-91](../../plans/NEO-91-implementation-plan.md), [NEO-92](../../plans/NEO-92-implementation-plan.md), [NEO-93](../../plans/NEO-93-implementation-plan.md), [NEO-94](../../plans/NEO-94-implementation-plan.md), [NEO-95](../../plans/NEO-95-implementation-plan.md), [NEO-96](../../plans/NEO-96-implementation-plan.md), [NEO-97](../../plans/NEO-97-implementation-plan.md), [NEO-98](../../plans/NEO-98-implementation-plan.md), label **`E5.M2`** on NEO-87–NEO-98 | | E5.M3 | Ready | **E5M3-01 catalog landed ([NEO-100](https://linear.app/neon-sprawl/issue/NEO-100)):** encounter + reward-table schemas, `prototype_encounters.json`, `prototype_reward_tables.json`, CI gates. **E5M3-02 server load ([NEO-101](https://linear.app/neon-sprawl/issue/NEO-101)):** fail-fast startup loaders for encounter + reward-table catalogs (CI parity). **E5M3-03 registries ([NEO-102](https://linear.app/neon-sprawl/issue/NEO-102)):** `IEncounterDefinitionRegistry` + `IRewardTableDefinitionRegistry` + DI. **E5M3-04 HTTP read ([NEO-103](https://linear.app/neon-sprawl/issue/NEO-103)):** **`GET /game/world/encounter-definitions`** — `EncounterDefinitionsWorldApi` + nested **`rewardTable`** summary ([NEO-103](../../plans/NEO-103-implementation-plan.md)); [server README — Encounter definitions (NEO-103)](../../../server/README.md#encounter-definitions-neo-103); Bruno `bruno/neon-sprawl-server/encounter-definitions/`. **E5M3-05 stores ([NEO-104](https://linear.app/neon-sprawl/issue/NEO-104)):** **`IEncounterProgressStore`** + **`IEncounterCompletionStore`** + **`EncounterProgressOperations`** ([NEO-104](../../plans/NEO-104-implementation-plan.md)); [server README — Encounter progress (NEO-104)](../../../server/README.md#encounter-progress--completion-stores-neo-104). **E5M3-06 completion grants ([NEO-105](https://linear.app/neon-sprawl/issue/NEO-105)):** **`EncounterCompletionOperations`** + **`EncounterCompleteEvent`** result payload ([NEO-105](../../plans/NEO-105-implementation-plan.md)); [server README — Encounter completion (NEO-105)](../../../server/README.md#encounter-completion--inventory-grants-neo-105). **E5M3-07 combat wiring ([NEO-106](https://linear.app/neon-sprawl/issue/NEO-106)):** **`EncounterCombatWiring`** on **`AbilityCastApi`** ([NEO-106](../../plans/NEO-106-implementation-plan.md)); [server README — Encounter combat wiring (NEO-106)](../../../server/README.md#encounter-combat-wiring-neo-106). **E5M3-09 event record ([NEO-107](https://linear.app/neon-sprawl/issue/NEO-107)):** **`IEncounterCompleteEventStore`** + E7.M2 hook stub ([NEO-107](../../plans/NEO-107-implementation-plan.md)); [server README — Encounter complete event (NEO-107)](../../../server/README.md#encounter-complete-event-record-neo-107). **E5M3-08 per-player GET ([NEO-108](https://linear.app/neon-sprawl/issue/NEO-108)):** **`GET /game/players/{id}/encounter-progress`** — `EncounterProgressApi` + DTOs ([NEO-108](../../plans/NEO-108-implementation-plan.md)); [server README — Per-player encounter progress (NEO-108)](../../../server/README.md#per-player-encounter-progress-neo-108); Bruno `bruno/neon-sprawl-server/encounter-progress/`. **E5M3-10 telemetry ([NEO-109](https://linear.app/neon-sprawl/issue/NEO-109)):** comment-only **`encounter_start`**, **`encounter_complete`**, **`reward_attribution`**, **`encounter_complete_denied`** in **`EncounterProgressOperations`** / **`EncounterCompletionOperations`** ([NEO-109](../../plans/NEO-109-implementation-plan.md)); [server README — Encounter telemetry hooks (NEO-109)](../../../server/README.md#encounter-telemetry-hooks-neo-109). **E5M3-11 client HUD ([NEO-110](https://linear.app/neon-sprawl/issue/NEO-110)):** **`encounter_progress_client.gd`**, **`EncounterProgressLabel`** / **`EncounterCompleteLabel`**; boot + defeat-triggered GET; inventory refresh on **`completed`** ([NEO-110](../../plans/NEO-110-implementation-plan.md), [`NEO-110` manual QA](../../manual-qa/NEO-110.md)); [client README — Encounter progress + loot HUD (NEO-110)](../../../client/README.md#encounter-progress--loot-hud-neo-110). **E5M3-12 client capstone ([NEO-111](https://linear.app/neon-sprawl/issue/NEO-111)):** playable encounter clear loop — [`NEO-111` manual QA](../../manual-qa/NEO-111.md); [client README — End-to-end encounter clear loop (NEO-111)](../../../client/README.md#end-to-end-encounter-clear-loop-neo-111); plan [NEO-111](../../plans/NEO-111-implementation-plan.md). **Epic 5 Slice 3 client capstone complete.** Epic 5 Slice 3 backlog [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md) **E5M3-01** [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) → **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) **landed**. Prototype spine: **`prototype_combat_pocket`** → **`prototype_combat_pocket_clear`**; idempotent completion; per-defeat gig XP unchanged ([NEO-44](../../plans/NEO-44-implementation-plan.md)). Upstream **E5.M2 Ready**, **E3.M3** inventory landed. | [NEO-111 plan](../../plans/NEO-111-implementation-plan.md), [NEO-110 plan](../../plans/NEO-110-implementation-plan.md), [NEO-109 plan](../../plans/NEO-109-implementation-plan.md), [NEO-108 plan](../../plans/NEO-108-implementation-plan.md), [NEO-107 plan](../../plans/NEO-107-implementation-plan.md), [NEO-106 plan](../../plans/NEO-106-implementation-plan.md), [NEO-105 plan](../../plans/NEO-105-implementation-plan.md), [NEO-104 plan](../../plans/NEO-104-implementation-plan.md), [NEO-103 plan](../../plans/NEO-103-implementation-plan.md), [NEO-102 plan](../../plans/NEO-102-implementation-plan.md), [E5_M3](E5_M3_EncounterAndRewardTables.md), label **`E5.M3`** on NEO-100–NEO-111 | +| E7.M1 | Planned | **Backlog decomposed (not started):** Epic 7 Slice 1 — [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Four frozen quests (3 onboarding + 1 gather→craft→combat chain); solo only; state-only completion in Slice 1 (rewards deferred to E7.M2). Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **`contract_handoff_token`** + **`EncounterCompleteEvent`** **Ready**. | [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1](E7_M1_QuestStateMachine.md), label **`E7.M1`** on NEO-112–NEO-123 | --- diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index 914425d..ad1dfd6 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -108,6 +108,8 @@ Gameplay **content and curves** default to **boot load** with optional **dev-onl | Module ID | Module Name | Depends On | Contract Needed | Phase Required | Status | |---|---|---|---|---|---| | E7.M1 | QuestStateMachine | E3.M2, E5.M1 | QuestDef, QuestStepState, QuestStateTransition | Prototype | Planned | + +**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Register row stays **Planned** until **NEO-112** lands. | E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Planned | | E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned | | E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned | diff --git a/docs/plans/E7M1-prototype-backlog.md b/docs/plans/E7M1-prototype-backlog.md new file mode 100644 index 0000000..2ce41c3 --- /dev/null +++ b/docs/plans/E7M1-prototype-backlog.md @@ -0,0 +1,392 @@ +# E7.M1 — Prototype story backlog (QuestStateMachine) + +Working backlog for **Epic 7 — Slice 1** ([quest core and persistence](../decomposition/epics/epic_07_quest_faction.md#slice-1---quest-core-and-persistence)). Decomposition and contracts: [E7.M1 — QuestStateMachine](../decomposition/modules/E7_M1_QuestStateMachine.md). + +**Full-stack policy:** Epics deliver **playable game features**. Every **player-visible** story has a **`client`** Linear issue created in **this same decomposition pass** — not an undocumented follow-up. Reference: [E5M3 (paired server+client)](../plans/E5M3-prototype-backlog.md). + +**Labels (Linear):** every issue **`E7.M1`**; add **`server`** or **`client`** + **`Story`** as listed. + +**Precursor (do not re-scope):** [E5.M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) encounter complete grants **`contract_handoff_token`** + records **`EncounterCompleteEvent`** ([NEO-107](https://linear.app/neon-sprawl/issue/NEO-107)) — prototype quest credit stand-in until [E7.M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) **`QuestRewardBundle`** router lands. **NEO-43** [`MissionRewardSkillXpGrant`](../../server/NeonSprawl.Server/Game/Skills/MissionRewardSkillXpGrant.cs) is the future skill-XP apply helper for E7.M2; E7.M1 tracks quest state only (no duplicate item grants on quest complete in Slice 1). + +**Upstream (must be landed):** [E3.M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) gather on interact ([NEO-63](https://linear.app/neon-sprawl/issue/NEO-63)); [E3.M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) craft POST ([NEO-70](https://linear.app/neon-sprawl/issue/NEO-70)); [E5.M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) encounter wiring + progress GET ([NEO-106](https://linear.app/neon-sprawl/issue/NEO-106), [NEO-108](https://linear.app/neon-sprawl/issue/NEO-108)). [E3S5](../plans/E3S5-client-prototype-backlog.md) client economy HUD ([NEO-72](https://linear.app/neon-sprawl/issue/NEO-72)–[NEO-75](https://linear.app/neon-sprawl/issue/NEO-75)). + +**Prototype quest spine (frozen in E7M1-01):** **three** solo onboarding quests + **one** multi-step chain quest (vision minimum 3–5 onboarding + one integrated chain). **Solo only** — no party credit ([quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md)). + +**Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the story branch when work starts. + +| Slug | Layer | Linear | +|------|-------|--------| +| E7M1-01 | server | [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) | +| E7M1-02 | server | [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) | +| E7M1-03 | server | [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) | +| E7M1-04 | server | [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) | +| E7M1-05 | server | [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) | +| E7M1-06 | server | [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) | +| E7M1-07 | server | [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) | +| E7M1-08 | server | [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) | +| E7M1-09 | server | [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) | +| E7M1-10 | server | [NEO-121](https://linear.app/neon-sprawl/issue/NEO-121) | +| E7M1-11 | client | [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) | +| E7M1-12 | client | [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) | + +**Dependency graph in Linear:** E7M1-02 blocked by E7M1-01. E7M1-03 blocked by E7M1-02. E7M1-04 blocked by E7M1-03. E7M1-05 blocked by E7M1-03. E7M1-06 blocked by E7M1-05. E7M1-07 blocked by E7M1-06. E7M1-08 blocked by E7M1-07. E7M1-09 blocked by E7M1-06 (may parallel E7M1-08). E7M1-10 blocked by E7M1-07. E7M1-11 blocked by E7M1-08 and E7M1-09. E7M1-12 blocked by E7M1-11. + +**Board order:** estimates **1–12** matching slug order (E7M1-01 = 1 … E7M1-12 = 12). On the Epic 7 project board, sort by **Estimate** (ascending). + +--- + +## Story order (recommended) + +| Order | Slug | Layer | Depends on | +|-------|------|-------|------------| +| 1 | **E7M1-01** | server | E3.M1/E3.M2/E5.M3 content ids frozen | +| 2 | **E7M1-02** | server | E7M1-01 | +| 3 | **E7M1-03** | server | E7M1-02 | +| 4 | **E7M1-04** | server | E7M1-03 | +| 5 | **E7M1-05** | server | E7M1-03 | +| 6 | **E7M1-06** | server | E7M1-05 | +| 7 | **E7M1-07** | server | E7M1-06 | +| 8 | **E7M1-08** | server | E7M1-07 | +| 9 | **E7M1-09** | server | E7M1-06 | +| 10 | **E7M1-10** | server | E7M1-07 | +| 11 | **E7M1-11** | client | E7M1-08, E7M1-09 | +| 12 | **E7M1-12** | client | E7M1-11 | + +**Downstream (separate modules):** [E7.M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) idempotent **`QuestRewardBundle`** apply + consume **`EncounterCompleteEvent`**; [E7.M3](../decomposition/modules/E7_M3_FactionReputationLedger.md) faction ledger (pre-production); [E8.M1](../decomposition/modules/E8_M1_PartyAndMatchAssembly.md) party-credit quests. + +--- + +## Kickoff decisions (decomposition defaults) + +| Topic | Decision | Rationale | +|-------|----------|-----------| +| Quest count | **3** onboarding + **1** multi-step chain (4 total) | Vision prototype minimum; readable manual QA | +| Party mode | **Solo only** | [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md); E8.M1 not landed | +| Accept flow | **`POST …/quests/{questId}/accept`** | Explicit start; boot does not auto-grant all quests | +| Objective advance | **Server hooks** on gather/craft/encounter success | Client displays GET snapshot only ([client_server_authority.md](../decomposition/modules/client_server_authority.md)) | +| Objective kinds (v1) | **`gather_item`**, **`craft_recipe`**, **`encounter_complete`**, **`inventory_has_item`** | Covers gather→craft→combat chain without custom scripting VM | +| Chain quest | **4 sequential steps** in one `QuestDef` | Single HUD row with step index; epic “integrated chain” | +| Prerequisites | **`prerequisiteQuestIds`** enforced on accept | Onboarding order: gather → refine → combat → chain | +| Rewards (Slice 1) | **State-only completion**; no duplicate item/XP grants | Loot stays on gather/craft/encounter paths; E7.M2 owns bundles | +| Token handoff | Chain final step **`inventory_has_item`** **`contract_handoff_token`** | Uses E5.M3 encounter loot; no consume mutation in Slice 1 | +| Idempotency | **Once per player per quest id** complete | Slice 1 AC: no duped quest completion | +| Store | **In-memory** **`IPlayerQuestStateStore`** (+ optional Postgres migration) | Matches encounter prototype stores | +| HTTP read | **`GET /game/players/{id}/quest-progress`** | Client HUD without local objective math | +| HUD fidelity | Prototype Labels / debug panels (not final art) | NEO-110 / NEO-122 precedent | + +--- + +### E7M1-01 — Prototype QuestDef catalog + schemas + CI + +**Goal:** Lock content shape and CI validation for **four** frozen quests before server load. + +**In scope** + +- `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` — stable quest ids and step/objective graph (see module doc freeze table). +- `scripts/validate_content.py`: schema validation, duplicate ids, exact four-quest allowlist, cross-ref **`itemId`** / **`recipeId`** / **`encounterId`** to frozen catalogs, **`prerequisiteQuestIds`** acyclic + known ids, objective kind enum. +- Designer note in [E7_M1](../decomposition/modules/E7_M1_QuestStateMachine.md) + `content/README.md`. + +**Out of scope** + +- Server loader, runtime engine, HTTP, Godot. + +**Acceptance criteria** + +- [ ] 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. + +**Client counterpart:** none (infrastructure-only). + +--- + +### E7M1-02 — Server quest catalog load (fail-fast) + +**Goal:** Disk → host: startup load of `content/quests/*.json` with CI-parity validation. + +**In scope** + +- Loader + catalog types under `server/NeonSprawl.Server/Game/Quests/` (path finalized in plan). +- Fail-fast on malformed files, duplicate ids, allowlist mismatch with CI, broken cross-refs (items, recipes, encounters). +- Unit tests (AAA) for loader happy path and failure modes. +- `server/README.md` section. + +**Out of scope** + +- Injectable registry interface (E7M1-03), HTTP projection, runtime state machine. + +**Acceptance criteria** + +- [ ] Host fails startup on invalid quest JSON (mirror CI rules). +- [ ] Loader tests cover allowlist + cross-ref failures. + +**Client counterpart:** none (infrastructure-only). + +--- + +### E7M1-03 — Injectable quest definition registry + DI + +**Goal:** **`IQuestDefinitionRegistry`** with **`TryGet`**, startup registration from E7M1-02 catalog. + +**In scope** + +- Interface + in-memory implementation; **`AddQuestDefinitionRegistry`** DI extension. +- Unit tests: lookup known ids, unknown id returns false. +- Wire in host startup alongside other content registries. + +**Out of scope** + +- HTTP, player quest state. + +**Acceptance criteria** + +- [ ] Registry resolves all four frozen quest ids at runtime. +- [ ] Unknown quest id fails closed for downstream operations. + +**Client counterpart:** none (infrastructure-only). + +--- + +### E7M1-04 — GET /game/world/quest-definitions + +**Goal:** Versioned read-only projection of frozen quest defs for client display names and step summaries. + +**In scope** + +- `QuestDefinitionsWorldApi` + DTOs in `Game/Quests/`. +- Nested steps/objectives summary (ids, kinds, display copy — no server secrets). +- Bruno folder `bruno/neon-sprawl-server/quest-definitions/`. +- Integration tests (AAA). + +**Out of scope** + +- Per-player progress (E7M1-08). + +**Acceptance criteria** + +- [ ] GET returns all four prototype quests with stable JSON v1 envelope. +- [ ] Bruno request documents example response shape. + +**Client counterpart:** consumed by [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) for display names (optional cache). + +--- + +### E7M1-05 — Player quest state store + QuestStepState persistence + +**Goal:** Durable per-player quest rows: status (`not_started` / `active` / `completed`), current step index, per-step objective counters. + +**In scope** + +- **`IPlayerQuestStateStore`** + in-memory implementation (registered in quest DI extension). +- Optional Postgres migration **`V00X`** when DB configured (mirror encounter/inventory pattern). +- **`QuestStepState`** / **`PlayerQuestProgressRow`** types aligned with module contracts. +- Unit tests: accept transitions, idempotent complete mark. + +**Out of scope** + +- Objective evaluation (E7M1-06), HTTP. + +**Acceptance criteria** + +- [ ] Store supports read/update per `(playerId, questId)`. +- [ ] Completed quest cannot regress to active without explicit reset API (none in prototype). + +**Client counterpart:** none (infrastructure-only). + +--- + +### E7M1-06 — QuestStateOperations (accept, step advance, complete) + +**Goal:** Server-authoritative quest lifecycle without objective wiring yet. + +**In scope** + +- **`QuestStateOperations.TryAccept`**, **`TryAdvanceStep`**, **`TryMarkComplete`** with structured **`reasonCode`** denials (`unknown_quest`, `prerequisite_incomplete`, `already_completed`, `already_active`). +- Prerequisite enforcement from **`prerequisiteQuestIds`**. +- Idempotent complete: second complete returns success without state change. +- Unit tests (AAA) for accept order, prerequisite gate, idempotent complete. + +**Out of scope** + +- Gather/craft/encounter hooks (E7M1-07); HTTP (E7M1-08/09). + +**Acceptance criteria** + +- [ ] Accept fails when prerequisite quest not **completed**. +- [ ] Complete is idempotent per player+quest. +- [ ] Reason codes documented in `server/README.md`. + +**Client counterpart:** [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) exposes accept POST to Godot. + +--- + +### E7M1-07 — Quest objective wiring (gather, craft, encounter) + +**Goal:** Advance active quest steps when server-side gather, craft, and encounter events succeed. + +**In scope** + +- **`QuestObjectiveWiring`** (or equivalent) invoked from: + - **`GatherOperations.TryGather`** success path — **`gather_item`** objectives. + - **`CraftOperations.TryCraft`** success path — **`craft_recipe`** objectives. + - **`EncounterCompletionOperations.TryCompleteAndGrant`** success path — **`encounter_complete`** objectives. + - Inventory snapshot read for **`inventory_has_item`** (post-grant or on polling hook from accept). +- Counter accumulation for quantity objectives (e.g. gather 3 scrap). +- Auto **`TryAdvanceStep`** / **`TryMarkComplete`** when step objectives satisfied. +- Integration tests: accept quest → simulate gather/craft/encounter → progress row advances. + +**Out of scope** + +- Client-side objective tracking; item consume on turn-in; E7.M2 reward bundles. + +**Acceptance criteria** + +- [ ] Active **`prototype_quest_gather_intro`** advances when **`scrap_metal_bulk`** gather succeeds. +- [ ] Active **`prototype_quest_refine_intro`** advances on **`refine_scrap_standard`** craft success. +- [ ] Active **`prototype_quest_combat_intro`** advances when **`prototype_combat_pocket`** completes. +- [ ] Chain quest steps advance in order across mixed objective kinds. + +**Client counterpart:** progress visible via [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) GET HUD after mutations. + +--- + +### E7M1-08 — GET /game/players/{id}/quest-progress + +**Goal:** Per-player quest progress snapshot for client HUD. + +**In scope** + +- **`QuestProgressApi`** + DTOs: all known quests with status, current step index, objective progress counters, completion timestamp when applicable. +- Known-player gate (mirror encounter-progress / skill-progression patterns). +- Bruno `bruno/neon-sprawl-server/quest-progress/`. +- Integration tests (AAA): accept → partial progress → complete rows. + +**Out of scope** + +- Quest definition catalog (E7M1-04). + +**Acceptance criteria** + +- [ ] GET returns stable v1 envelope for **`dev-local-1`** player. +- [ ] Completed quests show **`completed`** with step count at terminal step. + +**Client counterpart:** [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) — **`quest_progress_client.gd`** + HUD labels. + +--- + +### E7M1-09 — POST /game/players/{id}/quests/{questId}/accept + +**Goal:** Player-initiated quest accept from Godot (prototype “quest giver” is HUD button or key binding). + +**In scope** + +- **`QuestAcceptApi`**: POST accept; JSON body optional v1 `{}`; response includes updated progress row or deny payload. +- Wire **`QuestStateOperations.TryAccept`**. +- Bruno + integration tests: happy accept, prerequisite deny, duplicate accept. + +**Out of scope** + +- Abandon/reset POST (defer unless capstone QA requires). + +**Acceptance criteria** + +- [ ] Successful accept transitions row **`not_started` → `active`**. +- [ ] Deny returns structured **`reasonCode`** without mutation. + +**Client counterpart:** [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) — accept binding + feedback label. + +--- + +### E7M1-10 — Quest telemetry hook sites (comment-only) + +**Goal:** Document and place comment-only hook sites for future E9.M1 catalog events **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**. + +**In scope** + +- Hook sites in **`QuestStateOperations`** / **`QuestObjectiveWiring`** at accept, step advance, and complete. +- Reserved deny hook **`quest_accept_denied`** (optional comment). +- `server/README.md` subsection; module doc note. + +**Out of scope** + +- E9.M1 ingest pipeline; runtime telemetry emit. + +**Acceptance criteria** + +- [ ] Hook sites exist at accept, step complete, quest complete paths. +- [ ] Epic 7 Slice 1 telemetry vocabulary documented. + +**Client counterpart:** none (infrastructure-only). + +--- + +### E7M1-11 — Client quest progress + accept HUD (Godot) + +**Goal:** Godot **`GET …/quest-progress`** client + readable quest panel; accept via **`POST …/accept`**. + +**In scope** + +- `quest_progress_client.gd`, optional `quest_definitions_client.gd` for display names. +- HUD: **`QuestProgressLabel`** (active/completed summary), **`QuestAcceptFeedbackLabel`** (deny/success copy). +- Boot hydrate + refresh after gather/craft/cast/encounter events (mirror **`encounter_progress_client.gd`** pattern). +- Prototype accept bindings: e.g. keys **Q** / **Shift+Q** for next eligible quest accept (document in manual QA). +- GdUnit: HTTP double, parse happy path + deny path. +- `client/README.md` section. +- `docs/manual-qa/NEO-122.md`. + +**Out of scope** + +- Quest journal art, map pins, NPC dialog; final quest picker UI. + +**Acceptance criteria** + +- [ ] Player sees quest status change after accept without Bruno. +- [ ] Objective progress updates after gather/craft/encounter in Godot session. +- [ ] Failed GET/accept surfaces visible HUD error (match interaction client pattern). + +**Client counterpart:** this story **is** the client counterpart for E7M1-08/09. + +--- + +### E7M1-12 — Playable onboarding quest chain capstone (Godot) + +**Goal:** Prove Epic 7 Slice 1 acceptance **in Godot**: complete all four prototype quests (gather → refine → combat → operator chain) without Bruno; satisfies vision **quest loop** gate. + +**In scope** + +- **`docs/manual-qa/NEO-123.md`**: numbered single-session capstone — fresh server restart, accept onboarding quests in order, perform gather/craft/combat actions, verify chain quest **`prototype_quest_operator_chain`** completes when **`contract_handoff_token`** held. +- **`client/README.md`**: **End-to-end onboarding quest loop** section — flow table; cross-links NEO-112–NEO-122. +- Module alignment on completion: [E7_M1](../decomposition/modules/E7_M1_QuestStateMachine.md), [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md), [module_dependency_register.md](../decomposition/modules/module_dependency_register.md). + +**Out of scope** + +- E7.M2 reward bundles; faction reputation; quest VFX; Bruno-only verification as prototype-complete proof. + +**Acceptance criteria** + +- [ ] Human completes **`docs/manual-qa/NEO-123.md`** with server + client. +- [ ] Epic 7 Slice 1 AC: 3 onboarding + 1 chain complete without duplicate completion on replay. +- [ ] Re-read [epic_07 Definition of Done](../decomposition/epics/epic_07_quest_faction.md#definition-of-done) for prototype minimums. + +**Client counterpart:** this story **is** the Slice 1 client capstone. + +--- + +## Decomposition complete checklist + +- [x] Every player-visible AC has a **`client`** Linear issue (NEO-122, NEO-123) +- [x] No forbidden deferral language in backlog +- [x] Capstone client story exists (E7M1-12 / NEO-123) +- [x] Epic DoD updated for Godot verification +- [x] [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E7.M1 row updated +- [x] [module_dependency_register.md](../decomposition/modules/module_dependency_register.md) E7.M1 note updated + +## Related docs + +- [epic_07_quest_faction.md](../decomposition/epics/epic_07_quest_faction.md) +- [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md) +- [E5M3-prototype-backlog.md](E5M3-prototype-backlog.md) (encounter token precursor) +- [E3S5-client-prototype-backlog.md](E3S5-client-prototype-backlog.md) (gather/craft client HUD) +- [neon_sprawl_vision.plan.md — Prototype Scope](../../neon_sprawl_vision.plan.md) From 91a4abfe863708eb6cb9e6ebc88c65d1748dfa7a Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 21:16:10 -0400 Subject: [PATCH 3/8] NEO-112: add QuestDef schemas and prototype quest catalog --- content/quests/prototype_quests.json | 120 ++++++++++++++++++ content/schemas/quest-def.schema.json | 37 ++++++ .../schemas/quest-objective-def.schema.json | 59 +++++++++ content/schemas/quest-step-def.schema.json | 29 +++++ 4 files changed, 245 insertions(+) create mode 100644 content/quests/prototype_quests.json create mode 100644 content/schemas/quest-def.schema.json create mode 100644 content/schemas/quest-objective-def.schema.json create mode 100644 content/schemas/quest-step-def.schema.json diff --git a/content/quests/prototype_quests.json b/content/quests/prototype_quests.json new file mode 100644 index 0000000..bdc3857 --- /dev/null +++ b/content/quests/prototype_quests.json @@ -0,0 +1,120 @@ +{ + "schemaVersion": 1, + "quests": [ + { + "id": "prototype_quest_gather_intro", + "displayName": "Intro: Salvage Run", + "prerequisiteQuestIds": [], + "steps": [ + { + "id": "gather_intro_step_salvage", + "displayName": "Gather scrap metal", + "objectives": [ + { + "id": "gather_intro_obj_scrap", + "kind": "gather_item", + "itemId": "scrap_metal_bulk", + "quantity": 3 + } + ] + } + ] + }, + { + "id": "prototype_quest_refine_intro", + "displayName": "Intro: Refine Stock", + "prerequisiteQuestIds": ["prototype_quest_gather_intro"], + "steps": [ + { + "id": "refine_intro_step_craft", + "displayName": "Refine scrap", + "objectives": [ + { + "id": "refine_intro_obj_recipe", + "kind": "craft_recipe", + "recipeId": "refine_scrap_standard", + "quantity": 1 + } + ] + } + ] + }, + { + "id": "prototype_quest_combat_intro", + "displayName": "Intro: Clear the Pocket", + "prerequisiteQuestIds": [], + "steps": [ + { + "id": "combat_intro_step_encounter", + "displayName": "Clear the combat pocket", + "objectives": [ + { + "id": "combat_intro_obj_encounter", + "kind": "encounter_complete", + "encounterId": "prototype_combat_pocket" + } + ] + } + ] + }, + { + "id": "prototype_quest_operator_chain", + "displayName": "Operator Chain", + "prerequisiteQuestIds": [ + "prototype_quest_gather_intro", + "prototype_quest_refine_intro", + "prototype_quest_combat_intro" + ], + "steps": [ + { + "id": "chain_step_gather", + "displayName": "Bulk salvage", + "objectives": [ + { + "id": "chain_obj_gather", + "kind": "gather_item", + "itemId": "scrap_metal_bulk", + "quantity": 5 + } + ] + }, + { + "id": "chain_step_refine", + "displayName": "Refine stock", + "objectives": [ + { + "id": "chain_obj_refine", + "kind": "craft_recipe", + "recipeId": "refine_scrap_standard", + "quantity": 1 + } + ] + }, + { + "id": "chain_step_stim", + "displayName": "Craft field stim", + "objectives": [ + { + "id": "chain_obj_stim", + "kind": "craft_recipe", + "recipeId": "make_field_stim_mk0", + "quantity": 1 + } + ] + }, + { + "id": "chain_step_token", + "displayName": "Hand off contract token", + "objectives": [ + { + "id": "chain_obj_token", + "kind": "inventory_has_item", + "itemId": "contract_handoff_token", + "quantity": 1 + } + ] + } + ] + } + ] +} diff --git a/content/schemas/quest-def.schema.json b/content/schemas/quest-def.schema.json new file mode 100644 index 0000000..557ca9d --- /dev/null +++ b/content/schemas/quest-def.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://neon-sprawl.local/schemas/quest-def.json", + "title": "QuestDef", + "description": "Single quest row for catalogs (e.g. content/quests/*_quests.json). IDs are stable forever—rename display in displayName only. See docs/decomposition/modules/E7_M1_QuestStateMachine.md.", + "type": "object", + "additionalProperties": false, + "required": ["id", "displayName", "prerequisiteQuestIds", "steps"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Immutable quest key for accept/progress APIs and telemetry." + }, + "displayName": { + "type": "string", + "minLength": 1, + "description": "Player-facing label; safe to change without migrating character data." + }, + "prerequisiteQuestIds": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "description": "Quest ids that must be completed before accept; may be empty." + }, + "steps": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "https://neon-sprawl.local/schemas/quest-step-def.json" + }, + "description": "Ordered steps; prototype Slice 1 uses sequential advance only." + } + } +} diff --git a/content/schemas/quest-objective-def.schema.json b/content/schemas/quest-objective-def.schema.json new file mode 100644 index 0000000..190c12e --- /dev/null +++ b/content/schemas/quest-objective-def.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://neon-sprawl.local/schemas/quest-objective-def.json", + "title": "QuestObjectiveDef", + "description": "Single objective row on a QuestStepDef (content/quests/*_quests.json). Kinds cover gather, craft, encounter, and inventory checks for E7.M1 prototype Slice 1.", + "type": "object", + "additionalProperties": false, + "required": ["id", "kind"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Immutable objective key for progress tracking and HTTP projection." + }, + "kind": { + "type": "string", + "enum": ["gather_item", "craft_recipe", "encounter_complete", "inventory_has_item"], + "description": "Objective completion predicate evaluated server-side on success hooks." + }, + "itemId": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Frozen item catalog id (gather_item, inventory_has_item)." + }, + "quantity": { + "type": "integer", + "minimum": 1, + "description": "Required count for gather_item, craft_recipe, or inventory_has_item." + }, + "recipeId": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Frozen recipe catalog id (craft_recipe)." + }, + "encounterId": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Frozen encounter catalog id (encounter_complete)." + } + }, + "oneOf": [ + { + "properties": { "kind": { "const": "gather_item" } }, + "required": ["itemId", "quantity"] + }, + { + "properties": { "kind": { "const": "craft_recipe" } }, + "required": ["recipeId", "quantity"] + }, + { + "properties": { "kind": { "const": "encounter_complete" } }, + "required": ["encounterId"] + }, + { + "properties": { "kind": { "const": "inventory_has_item" } }, + "required": ["itemId", "quantity"] + } + ] +} diff --git a/content/schemas/quest-step-def.schema.json b/content/schemas/quest-step-def.schema.json new file mode 100644 index 0000000..e7ea489 --- /dev/null +++ b/content/schemas/quest-step-def.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://neon-sprawl.local/schemas/quest-step-def.json", + "title": "QuestStepDef", + "description": "Single step row on a QuestDef (content/quests/*_quests.json). Steps advance sequentially in prototype Slice 1.", + "type": "object", + "additionalProperties": false, + "required": ["id", "displayName", "objectives"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Immutable step key for progress tracking and HTTP projection." + }, + "displayName": { + "type": "string", + "minLength": 1, + "description": "Player-facing label; safe to change without migrating character data." + }, + "objectives": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "https://neon-sprawl.local/schemas/quest-objective-def.json" + }, + "description": "Objectives that must complete before the step advances." + } + } +} From 81ef9b94cba862784ac070baa6c36f00827346ef Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 21:16:12 -0400 Subject: [PATCH 4/8] NEO-112: extend validate_content.py with E7M1 quest CI gates --- scripts/validate_content.py | 297 ++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) diff --git a/scripts/validate_content.py b/scripts/validate_content.py index 13d68fc..76f5de2 100644 --- a/scripts/validate_content.py +++ b/scripts/validate_content.py @@ -14,6 +14,7 @@ Validates: - npc behavior catalogs: content/npc-behaviors/*_npc_behaviors.json rows vs content/schemas/npc-behavior-def.schema.json (NEO-87) - reward table catalogs: content/reward-tables/*_reward_tables.json rows vs content/schemas/reward-table.schema.json (NEO-100) - encounter catalogs: content/encounters/*_encounters.json rows vs content/schemas/encounter-def.schema.json (NEO-100) +- quest catalogs: content/quests/*_quests.json rows vs content/schemas/quest-def.schema.json (NEO-112) """ from __future__ import annotations @@ -39,6 +40,9 @@ NPC_BEHAVIOR_SCHEMA = REPO_ROOT / "content/schemas/npc-behavior-def.schema.json" REWARD_GRANT_ROW_SCHEMA = REPO_ROOT / "content/schemas/reward-grant-row.schema.json" REWARD_TABLE_SCHEMA = REPO_ROOT / "content/schemas/reward-table.schema.json" ENCOUNTER_DEF_SCHEMA = REPO_ROOT / "content/schemas/encounter-def.schema.json" +QUEST_OBJECTIVE_DEF_SCHEMA = REPO_ROOT / "content/schemas/quest-objective-def.schema.json" +QUEST_STEP_DEF_SCHEMA = REPO_ROOT / "content/schemas/quest-step-def.schema.json" +QUEST_DEF_SCHEMA = REPO_ROOT / "content/schemas/quest-def.schema.json" SKILLS_DIR = REPO_ROOT / "content/skills" MASTERY_DIR = REPO_ROOT / "content/mastery" ITEMS_DIR = REPO_ROOT / "content/items" @@ -48,6 +52,7 @@ ABILITIES_DIR = REPO_ROOT / "content/abilities" NPC_BEHAVIORS_DIR = REPO_ROOT / "content/npc-behaviors" REWARD_TABLES_DIR = REPO_ROOT / "content/reward-tables" ENCOUNTERS_DIR = REPO_ROOT / "content/encounters" +QUESTS_DIR = REPO_ROOT / "content/quests" # Slice 1 prototype lock (NEO-33): exact ids + category coverage after schema passes. PROTOTYPE_SLICE1_SKILL_IDS = frozenset({"salvage", "refine", "intrusion"}) @@ -152,6 +157,19 @@ PROTOTYPE_E5M3_REWARD_GRANTS: dict[str, int] = { "contract_handoff_token": 1, } +# Epic 7 Slice 1 prototype lock (NEO-112): exact quest ids after schema passes. +# Keep in sync with E7.M1 freeze table and future NEO-113 server loader. +PROTOTYPE_E7M1_QUEST_IDS = frozenset( + { + "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" + def _prototype_slice1_gate(seen_ids: dict[str, str], id_to_category: dict[str, str]) -> str | None: """Return a human-readable error if Slice 1 contract fails, else None.""" @@ -823,6 +841,236 @@ def _prototype_e5m3_encounter_cross_ref_gate(id_to_row: dict[str, dict]) -> str return None +def _quest_def_validator() -> Draft202012Validator: + """Build a validator that resolves quest-def $ref to step and objective schemas.""" + objective_schema = json.loads(QUEST_OBJECTIVE_DEF_SCHEMA.read_text(encoding="utf-8")) + step_schema = json.loads(QUEST_STEP_DEF_SCHEMA.read_text(encoding="utf-8")) + def_schema = json.loads(QUEST_DEF_SCHEMA.read_text(encoding="utf-8")) + registry = Registry().with_resources( + [ + (def_schema["$id"], Resource.from_contents(def_schema)), + (step_schema["$id"], Resource.from_contents(step_schema)), + (objective_schema["$id"], Resource.from_contents(objective_schema)), + ] + ) + return Draft202012Validator(def_schema, registry=registry) + + +def _validate_quest_catalogs( + *, + quest_files: list[Path], + quest_validator: Draft202012Validator, +) -> tuple[int, dict[str, str], dict[str, dict]]: + """Validate quest JSON files. Returns (error_count, seen_ids, id_to_row).""" + errors = 0 + seen_ids: dict[str, str] = {} + id_to_row: dict[str, dict] = {} + + for path in quest_files: + rel = str(path.relative_to(REPO_ROOT)) + data = json.loads(path.read_text(encoding="utf-8")) + schema_version = data.get("schemaVersion") + if schema_version != 1: + print(f"error: {rel}: expected schemaVersion 1, got {schema_version!r}", file=sys.stderr) + errors += 1 + continue + + quests = data.get("quests") + if not isinstance(quests, list): + print(f"error: {rel}: expected top-level 'quests' array", file=sys.stderr) + errors += 1 + continue + + for i, row in enumerate(quests): + if not isinstance(row, dict): + print(f"error: {rel}: quests[{i}] must be an object", file=sys.stderr) + errors += 1 + continue + row_schema_errors = 0 + for err in sorted(quest_validator.iter_errors(row), key=lambda e: e.path): + loc = ".".join(str(p) for p in err.path) or "(root)" + print(f"error: {rel} quests[{i}] {loc}: {err.message}", file=sys.stderr) + row_schema_errors += 1 + errors += 1 + qid = row.get("id") + if isinstance(qid, str) and row_schema_errors == 0: + prev = seen_ids.get(qid) + if prev: + print(f"error: duplicate quest id {qid!r} in {prev} and {rel}", file=sys.stderr) + errors += 1 + else: + seen_ids[qid] = rel + id_to_row[qid] = row + + steps = row.get("steps") + if isinstance(steps, list): + step_ids_seen: set[str] = set() + for si, step in enumerate(steps): + if not isinstance(step, dict): + continue + sid = step.get("id") + if isinstance(sid, str): + if sid in step_ids_seen: + print( + f"error: {rel} quests[{i}] steps[{si}]: duplicate step id {sid!r}", + file=sys.stderr, + ) + errors += 1 + else: + step_ids_seen.add(sid) + objectives = step.get("objectives") + if isinstance(objectives, list): + obj_ids_seen: set[str] = set() + for oi, obj in enumerate(objectives): + if not isinstance(obj, dict): + continue + oid = obj.get("id") + if isinstance(oid, str): + if oid in obj_ids_seen: + print( + f"error: {rel} quests[{i}] steps[{si}] objectives[{oi}]: " + f"duplicate objective id {oid!r}", + file=sys.stderr, + ) + errors += 1 + else: + obj_ids_seen.add(oid) + + return errors, seen_ids, id_to_row + + +def _prototype_e7m1_quest_gate(seen_ids: dict[str, str]) -> str | None: + """Return a human-readable error if E7M1 quest contract fails, else None.""" + ids = frozenset(seen_ids.keys()) + if ids != PROTOTYPE_E7M1_QUEST_IDS: + return ( + "error: prototype E7M1 expects exactly quest ids " + f"{sorted(PROTOTYPE_E7M1_QUEST_IDS)!r}, got {sorted(ids)!r}" + ) + return None + + +def _prototype_e7m1_quest_prerequisite_gate(id_to_row: dict[str, dict]) -> str | None: + """Return a human-readable error if prerequisite ids are unknown or cyclic, else None.""" + known = frozenset(id_to_row.keys()) + for qid, row in id_to_row.items(): + prereqs = row.get("prerequisiteQuestIds") + if not isinstance(prereqs, list): + continue + for pid in prereqs: + if isinstance(pid, str) and pid not in known: + return ( + f"error: quest {qid!r}: prerequisiteQuestIds references unknown quest id {pid!r}" + ) + + visiting: set[str] = set() + visited: set[str] = set() + + def dfs(node: str) -> str | None: + if node in visiting: + return f"error: cyclic prerequisiteQuestIds detected involving quest {node!r}" + if node in visited: + return None + visiting.add(node) + row = id_to_row.get(node, {}) + prereqs = row.get("prerequisiteQuestIds", []) + if isinstance(prereqs, list): + for pid in prereqs: + if isinstance(pid, str): + err = dfs(pid) + if err: + return err + visiting.remove(node) + visited.add(node) + return None + + for qid in known: + err = dfs(qid) + if err: + return err + return None + + +def _prototype_e7m1_quest_cross_ref_gate(id_to_row: dict[str, dict]) -> str | None: + """Return a human-readable error if objective catalog cross-refs fail, else None.""" + for qid, row in id_to_row.items(): + steps = row.get("steps") + if not isinstance(steps, list): + continue + for si, step in enumerate(steps): + if not isinstance(step, dict): + continue + objectives = step.get("objectives") + if not isinstance(objectives, list): + continue + for oi, obj in enumerate(objectives): + if not isinstance(obj, dict): + continue + kind = obj.get("kind") + item_id = obj.get("itemId") + if isinstance(item_id, str) and kind in ("gather_item", "inventory_has_item"): + if item_id not in PROTOTYPE_SLICE1_ITEM_IDS: + return ( + f"error: quest {qid!r} steps[{si}] objectives[{oi}]: itemId {item_id!r} " + "is not in the frozen prototype item catalog" + ) + recipe_id = obj.get("recipeId") + if isinstance(recipe_id, str) and kind == "craft_recipe": + if recipe_id not in PROTOTYPE_SLICE3_RECIPE_IDS: + return ( + f"error: quest {qid!r} steps[{si}] objectives[{oi}]: recipeId {recipe_id!r} " + "is not in the frozen prototype recipe catalog" + ) + encounter_id = obj.get("encounterId") + if isinstance(encounter_id, str) and kind == "encounter_complete": + if encounter_id not in PROTOTYPE_E5M3_ENCOUNTER_IDS: + return ( + f"error: quest {qid!r} steps[{si}] objectives[{oi}]: encounterId " + f"{encounter_id!r} is not in the frozen prototype encounter catalog" + ) + return None + + +def _prototype_e7m1_chain_terminal_gate(id_to_row: dict[str, dict]) -> str | None: + """Return a human-readable error if operator chain terminal step fails, else None.""" + chain = id_to_row.get(PROTOTYPE_E7M1_CHAIN_QUEST_ID) + if not isinstance(chain, dict): + return f"error: missing chain quest {PROTOTYPE_E7M1_CHAIN_QUEST_ID!r}" + steps = chain.get("steps") + if not isinstance(steps, list) or len(steps) == 0: + return f"error: chain quest {PROTOTYPE_E7M1_CHAIN_QUEST_ID!r} must have at least one step" + last_step = steps[-1] + if not isinstance(last_step, dict): + return f"error: chain quest {PROTOTYPE_E7M1_CHAIN_QUEST_ID!r} terminal step must be an object" + objectives = last_step.get("objectives") + if not isinstance(objectives, list) or len(objectives) != 1: + return ( + f"error: chain quest {PROTOTYPE_E7M1_CHAIN_QUEST_ID!r} terminal step must have " + "exactly one objective" + ) + obj = objectives[0] + if not isinstance(obj, dict): + return ( + f"error: chain quest {PROTOTYPE_E7M1_CHAIN_QUEST_ID!r} terminal objective must be an object" + ) + if obj.get("kind") != "inventory_has_item": + return ( + f"error: chain quest {PROTOTYPE_E7M1_CHAIN_QUEST_ID!r} terminal objective kind must be " + "'inventory_has_item'" + ) + if obj.get("itemId") != PROTOTYPE_E7M1_CHAIN_TERMINAL_ITEM_ID: + return ( + f"error: chain quest {PROTOTYPE_E7M1_CHAIN_QUEST_ID!r} terminal itemId must be " + f"{PROTOTYPE_E7M1_CHAIN_TERMINAL_ITEM_ID!r}, got {obj.get('itemId')!r}" + ) + if obj.get("quantity") != 1: + return ( + f"error: chain quest {PROTOTYPE_E7M1_CHAIN_QUEST_ID!r} terminal quantity must be 1, " + f"got {obj.get('quantity')!r}" + ) + return None + + def _prototype_slice4_gate(track_skill_ids: list[str]) -> str | None: """Return a human-readable error if Slice 4 contract fails, else None.""" if len(track_skill_ids) != 1: @@ -1158,6 +1406,15 @@ def main() -> int: if not ENCOUNTER_DEF_SCHEMA.is_file(): print(f"error: missing schema {ENCOUNTER_DEF_SCHEMA}", file=sys.stderr) return 1 + if not QUEST_OBJECTIVE_DEF_SCHEMA.is_file(): + print(f"error: missing schema {QUEST_OBJECTIVE_DEF_SCHEMA}", file=sys.stderr) + return 1 + if not QUEST_STEP_DEF_SCHEMA.is_file(): + print(f"error: missing schema {QUEST_STEP_DEF_SCHEMA}", file=sys.stderr) + return 1 + if not QUEST_DEF_SCHEMA.is_file(): + print(f"error: missing schema {QUEST_DEF_SCHEMA}", file=sys.stderr) + return 1 skill_schema = json.loads(SKILL_SCHEMA.read_text(encoding="utf-8")) skill_validator = Draft202012Validator(skill_schema) @@ -1179,6 +1436,7 @@ def main() -> int: reward_table_validator = _reward_table_def_validator() encounter_schema = json.loads(ENCOUNTER_DEF_SCHEMA.read_text(encoding="utf-8")) encounter_validator = Draft202012Validator(encounter_schema) + quest_validator = _quest_def_validator() if not SKILLS_DIR.is_dir(): print(f"error: missing directory {SKILLS_DIR}", file=sys.stderr) @@ -1271,6 +1529,15 @@ def main() -> int: print(f"error: no *_encounters.json files under {ENCOUNTERS_DIR}", file=sys.stderr) return 1 + if not QUESTS_DIR.is_dir(): + print(f"error: missing directory {QUESTS_DIR}", file=sys.stderr) + return 1 + + quest_files = sorted(QUESTS_DIR.glob("*_quests.json")) + if not quest_files: + print(f"error: no *_quests.json files under {QUESTS_DIR}", file=sys.stderr) + return 1 + seen_ids: dict[str, str] = {} id_to_category: dict[str, str] = {} errors = 0 @@ -1536,6 +1803,34 @@ def main() -> int: print(e5m3_encounter_cross_ref_err, file=sys.stderr) return 1 + quest_errors, quest_seen_ids, quest_id_to_row = _validate_quest_catalogs( + quest_files=quest_files, + quest_validator=quest_validator, + ) + if quest_errors: + print(f"content validation failed with {quest_errors} error(s)", file=sys.stderr) + return 1 + + e7m1_quest_err = _prototype_e7m1_quest_gate(quest_seen_ids) + if e7m1_quest_err: + print(e7m1_quest_err, file=sys.stderr) + return 1 + + e7m1_prereq_err = _prototype_e7m1_quest_prerequisite_gate(quest_id_to_row) + if e7m1_prereq_err: + print(e7m1_prereq_err, file=sys.stderr) + return 1 + + e7m1_cross_ref_err = _prototype_e7m1_quest_cross_ref_gate(quest_id_to_row) + if e7m1_cross_ref_err: + print(e7m1_cross_ref_err, file=sys.stderr) + return 1 + + e7m1_chain_terminal_err = _prototype_e7m1_chain_terminal_gate(quest_id_to_row) + if e7m1_chain_terminal_err: + print(e7m1_chain_terminal_err, file=sys.stderr) + return 1 + print( "content OK: " f"{len(skill_files)} skill catalog file(s), " @@ -1549,6 +1844,7 @@ def main() -> int: f"{len(npc_behavior_files)} npc behavior catalog file(s), " f"{len(reward_table_files)} reward table catalog file(s), " f"{len(encounter_files)} encounter catalog file(s), " + f"{len(quest_files)} quest catalog file(s), " f"{len(seen_ids)} unique skill id(s), " f"{len(item_seen_ids)} unique item id(s), " f"{len(node_seen_ids)} unique nodeDefId(s), " @@ -1557,6 +1853,7 @@ def main() -> int: f"{len(npc_behavior_seen_ids)} unique npc behavior id(s), " f"{len(reward_table_seen_ids)} unique reward table id(s), " f"{len(encounter_seen_ids)} unique encounter id(s), " + f"{len(quest_seen_ids)} unique quest id(s), " f"{len(track_skill_ids)} mastery track(s)" ) return 0 From 765075f3cf2262054c2748a3fbd8164ab0120ffc Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 21:16:12 -0400 Subject: [PATCH 5/8] NEO-112: document quest catalog CI and reconcile implementation plan --- content/README.md | 3 +++ .../modules/CT_M1_ContentValidationPipeline.md | 2 ++ .../documentation_and_implementation_alignment.md | 2 +- docs/plans/E7M1-prototype-backlog.md | 8 ++++---- docs/plans/NEO-112-implementation-plan.md | 15 +++++++++++---- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/content/README.md b/content/README.md index 0017067..6c9f3d8 100644 --- a/content/README.md +++ b/content/README.md @@ -13,6 +13,7 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with ** | [`npc-behaviors/`](npc-behaviors/) | NPC behavior archetype catalogs; each row matches [`schemas/npc-behavior-def.schema.json`](schemas/npc-behavior-def.schema.json) — **stable `id`**, **`archetypeKind`**, aggro/leash radii, telegraph + attack timings for [E5.M2](../docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) | | [`encounters/`](encounters/) | Encounter templates; each row matches [`schemas/encounter-def.schema.json`](schemas/encounter-def.schema.json) — **stable `id`**, completion criteria, **`rewardTableId`** for [E5.M3](../docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md) | | [`reward-tables/`](reward-tables/) | Reward table catalogs; each row matches [`schemas/reward-table.schema.json`](schemas/reward-table.schema.json) — **stable `id`**, **`fixedGrants`** referencing frozen item ids for [E5.M3](../docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md) | +| [`quests/`](quests/) | Quest catalogs; each row matches [`schemas/quest-def.schema.json`](schemas/quest-def.schema.json) (steps via [`quest-step-def.schema.json`](schemas/quest-step-def.schema.json), objectives via [`quest-objective-def.schema.json`](schemas/quest-objective-def.schema.json)) — **stable `id`**, **`prerequisiteQuestIds`**, step/objective graph for [E7.M1](../docs/decomposition/modules/E7_M1_QuestStateMachine.md) | | [`resource-nodes/`](resource-nodes/) | Resource node + yield catalogs; node rows match [`schemas/resource-node-def.schema.json`](schemas/resource-node-def.schema.json), yield rows match [`schemas/resource-yield-row.schema.json`](schemas/resource-yield-row.schema.json) — **stable `nodeDefId`**, **`gatherLens`**, **`maxGathers`** for [E3.M1](../docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) | **Prototype Slice 1 (NEO-33):** CI expects **exactly three** skill rows with ids **`salvage`**, **`refine`**, **`intrusion`** (gather + process + tech). Each row must declare **`allowedXpSourceKinds`** (non-empty); see [E2.M1 — Designer note: `allowedXpSourceKinds`](../docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md#designer-note-allowedxpsourcekinds-and-grant-call-sites) for what each kind means for grant wiring. @@ -29,6 +30,8 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with ** **Prototype E5 Slice 3 — encounters + rewards (NEO-100):** CI expects **exactly one** encounter id **`prototype_combat_pocket`** and one reward table id **`prototype_combat_pocket_clear`** aligned to [E5.M3 encounter freeze](../docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md#prototype-slice-3-freeze-e5m3-01). **`requiredNpcInstanceIds`** must match the three E5.M2 NPC instance ids; **`fixedGrants`** reference frozen item ids only. **Do not rename** ids after ship—change **`displayName`** only. See [E5M3-prototype-backlog.md](../docs/plans/E5M3-prototype-backlog.md). +**Prototype E7 Slice 1 — quests (NEO-112):** CI expects **exactly four** quest ids aligned to [E7.M1 quest freeze](../docs/decomposition/modules/E7_M1_QuestStateMachine.md#prototype-slice-1-freeze-e7m1-01) — **`prototype_quest_gather_intro`**, **`prototype_quest_refine_intro`**, **`prototype_quest_combat_intro`**, **`prototype_quest_operator_chain`**. Objective **`itemId`** / **`recipeId`** / **`encounterId`** cross-refs must resolve to frozen item, recipe, and encounter catalogs; **`prerequisiteQuestIds`** must be acyclic and reference known quest ids; operator chain terminal step is **`inventory_has_item`** **`contract_handoff_token`** ×1. **Do not rename** quest `id` after ship—change **`displayName`** only. See [E7M1-prototype-backlog.md](../docs/plans/E7M1-prototype-backlog.md) and [NEO-112 plan](../docs/plans/NEO-112-implementation-plan.md). + **Prototype Slice 4 (NEO-45):** CI expects **exactly one** `MasteryTrack` for **`salvage`** only (`refine` / `intrusion` have no mastery rows yet). Tier 1 @ skill level **2** is a **mutually exclusive** branch pick (`scrap_efficiency` vs `bulk_haul`) with **no** perks; tier 2 @ level **4** unlocks one perk per branch path. **Do not rename** `branchId` / `perkId` after ship—change `displayName` only. See [E2.M3 — Designer note](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#designer-note-tiers-branches-and-level-gates) and [freeze table](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#prototype-slice-4-freeze--salvage-flagship-neo-45). Server load path and hot-reload policy are TBD; keep files versioned here as the source of truth. diff --git a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md index 1e89efd..44458c8 100644 --- a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md +++ b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md @@ -59,6 +59,8 @@ Content tooling **Slice 1** — schemas + CI; **Slice 4** — server parity hard **Encounter catalogs ([NEO-100](https://linear.app/neon-sprawl/issue/NEO-100)):** the same script validates each row in `content/encounters/*_encounters.json` against [`content/schemas/encounter-def.schema.json`](../../../content/schemas/encounter-def.schema.json), rejects **duplicate `id`** across files, cross-checks **`rewardTableId`** against reward table catalogs, and (E5 Slice 3) enforces the **frozen one-encounter** id set (`prototype_combat_pocket`) with **`requiredNpcInstanceIds`** matching the three E5.M2 NPC instance ids. +**Quest catalogs ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)):** the same script validates each row in `content/quests/*_quests.json` against [`content/schemas/quest-def.schema.json`](../../../content/schemas/quest-def.schema.json) (steps via [`quest-step-def.schema.json`](../../../content/schemas/quest-step-def.schema.json), objectives via [`quest-objective-def.schema.json`](../../../content/schemas/quest-objective-def.schema.json)), rejects **duplicate `id`** across files and duplicate step/objective ids within a quest, cross-checks objective **`itemId`** / **`recipeId`** / **`encounterId`** against frozen item, recipe, and encounter catalogs, enforces **acyclic `prerequisiteQuestIds`**, and (E7 Slice 1) enforces the **frozen four-quest** id set with operator chain terminal **`inventory_has_item`** **`contract_handoff_token`** ×1. + **Decomposition vocabulary:** the same workflow runs [`scripts/check_decomposition_language.py`](../../../scripts/check_decomposition_language.py) (stdlib only) over `docs/decomposition/**/*.md` to catch wording that treats **combat** as a leveled **`SkillDef`** line instead of **gig** progression (see script docstring for patterns). Adjust the script if a future doc needs a documented exception. ## Source anchors diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index 5b77d2d..fbd0a06 100644 --- a/docs/decomposition/modules/documentation_and_implementation_alignment.md +++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md @@ -59,7 +59,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not | E5.M1 | Ready | **NEO-76 landed:** frozen prototype four-ability catalog in [`content/abilities/prototype_abilities.json`](../../../content/abilities/prototype_abilities.json); [`ability-def.schema.json`](../../../content/schemas/ability-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) E5M1 four-id gate ([NEO-76](../../plans/NEO-76-implementation-plan.md)). **NEO-77 landed:** fail-fast server load of `content/abilities/*_abilities.json` at startup — `server/NeonSprawl.Server/Game/Combat/` ([NEO-77](../../plans/NEO-77-implementation-plan.md)); [server README — Ability catalog](../../../server/README.md#ability-catalog-contentabilities-neo-77). **NEO-79 landed:** injectable **`IAbilityDefinitionRegistry`** + DI; hotbar/cast use **`TryNormalizeKnown`** ([NEO-79](../../plans/NEO-79-implementation-plan.md)); Bruno `bruno/neon-sprawl-server/hotbar-loadout/` + `ability-cast/` unknown-ability deny smokes. **NEO-78 landed:** **`GET /game/world/ability-definitions`** — `AbilityDefinitionsWorldApi` + DTOs in `Game/Combat/` ([NEO-78](../../plans/NEO-78-implementation-plan.md)); [server README — Ability definitions (NEO-78)](../../../server/README.md#ability-definitions-neo-78); Bruno `bruno/neon-sprawl-server/ability-definitions/`. **NEO-80 landed:** **`ICombatEntityHealthStore`** / **`InMemoryCombatEntityHealthStore`** — lazy init (Slice 1 flat 100 HP; **superseded by [NEO-91](../../plans/NEO-91-implementation-plan.md)** per-archetype catalog max HP + three NPC instance ids); DI via **`AddCombatEntityHealthStore()`** ([NEO-80](../../plans/NEO-80-implementation-plan.md)); [server README — Combat entity health (NEO-80, NEO-91)](../../../server/README.md#combat-entity-health-neo-80-neo-91). **NEO-81 landed:** **`CombatOperations.TryResolve`** + **`CombatResult`** + **`CombatReasonCodes`** — defeated pre-check deny, catalog damage ([NEO-81](../../plans/NEO-81-implementation-plan.md)); [server README — Combat engine (NEO-81)](../../../server/README.md#combat-engine-neo-81). **NEO-82 landed:** **`AbilityCastApi`** → **`CombatOperations.TryResolve`**; nested wire **`combatResolution`** on accept; per-ability catalog cooldown; **`target_defeated`** cast deny ([NEO-82](../../plans/NEO-82-implementation-plan.md)); [server README — Ability cast (NEO-82)](../../../server/README.md#ability-cast-neo-31-neo-82); Bruno `bruno/neon-sprawl-server/ability-cast/` defeat spine. **NEO-83 landed:** **`GET /game/world/combat-targets`** — `CombatTargetsWorldApi` + DTOs; authoritative HP snapshot from **`ICombatEntityHealthStore`** ([NEO-83](../../plans/NEO-83-implementation-plan.md)); [server README — Combat targets snapshot (NEO-83)](../../../server/README.md#combat-targets-snapshot-neo-83); Bruno `bruno/neon-sprawl-server/combat-targets/`. **NEO-44 landed:** **`CombatDefeatGigXpGrant`** on cast **`targetDefeated`** — **25** gig XP to **`breach`** via **`IPlayerGigProgressionStore`** (V007); **`GET …/gig-progression`** ([NEO-44](../../plans/NEO-44-implementation-plan.md), [`NEO-44` manual QA](../../manual-qa/NEO-44.md)); [server README — Gig progression (NEO-44)](../../../server/README.md#gig-progression-snapshot-neo-44); Bruno `bruno/neon-sprawl-server/gig-progression/`. **NEO-84 landed:** comment-only **`ability_used`** / **`enemy_defeat`** telemetry hook sites in **`CombatOperations.TryResolve`**; reserved **`encounter_start`** / **`player_death`** ([NEO-84](../../plans/NEO-84-implementation-plan.md)); [server README — Combat telemetry hooks (NEO-84)](../../../server/README.md#combat-telemetry-hooks-neo-84). **NEO-85 landed:** client combat HUD — **`ability_cast_client.gd`** parses **`combatResolution`**; **`combat_targets_client.gd`**, **`CastFeedbackLabel`** resolution copy, **`CombatTargetHpLabel`**; event-driven GET refresh ([NEO-85](../../plans/NEO-85-implementation-plan.md), [`NEO-85` manual QA](../../manual-qa/NEO-85.md)); `client/README.md` combat HUD section. **NEO-86 landed:** playable combat capstone — **`gig_progression_client.gd`**, **`GigXpLabel`**, defeat-triggered gig GET refresh; capstone manual QA [`NEO-86`](../../manual-qa/NEO-86.md); `client/README.md` end-to-end combat loop section ([NEO-86](../../plans/NEO-86-implementation-plan.md)). **Epic 5 Slice 1 client capstone complete.** **Backlog decomposed:** Epic 5 Slice 1 — **E5M1-01**–**E5M1-12** landed. **Precursor landed:** E1.M4 cast funnel (NEO-28/31/32). | [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1](E5_M1_CombatRulesEngine.md), [NEO-77](../../plans/NEO-77-implementation-plan.md), [NEO-79](../../plans/NEO-79-implementation-plan.md), [NEO-78](../../plans/NEO-78-implementation-plan.md), [NEO-80](../../plans/NEO-80-implementation-plan.md), [NEO-81](../../plans/NEO-81-implementation-plan.md), [NEO-82](../../plans/NEO-82-implementation-plan.md), [NEO-83](../../plans/NEO-83-implementation-plan.md), [NEO-44](../../plans/NEO-44-implementation-plan.md), [NEO-84](../../plans/NEO-84-implementation-plan.md), [NEO-85](../../plans/NEO-85-implementation-plan.md), [NEO-86](../../plans/NEO-86-implementation-plan.md), [E1M4-prototype-backlog.md](../../plans/E1M4-prototype-backlog.md) | | E5.M2 | Ready | **NEO-87 landed:** frozen prototype three-behavior catalog in [`content/npc-behaviors/prototype_npc_behaviors.json`](../../../content/npc-behaviors/prototype_npc_behaviors.json); [`npc-behavior-def.schema.json`](../../../content/schemas/npc-behavior-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) E5M2 three-id gate ([NEO-87](../../plans/NEO-87-implementation-plan.md)). **NEO-88 landed:** fail-fast server load of `content/npc-behaviors/*_npc_behaviors.json` at startup — `server/NeonSprawl.Server/Game/Npc/` ([NEO-88](../../plans/NEO-88-implementation-plan.md)); [server README — NPC behavior catalog](../../../server/README.md#npc-behavior-catalog-contentnpc-behaviors-neo-88). **NEO-89 landed:** injectable **`INpcBehaviorDefinitionRegistry`** + DI; **`PrototypeNpcBehaviorRegistry`** id constants ([NEO-89](../../plans/NEO-89-implementation-plan.md)). **NEO-90 landed:** **`GET /game/world/npc-behavior-definitions`** — `NpcBehaviorDefinitionsWorldApi` + DTOs in `Game/Npc/` ([NEO-90](../../plans/NEO-90-implementation-plan.md)); [server README — NPC behavior definitions (NEO-90)](../../../server/README.md#npc-behavior-definitions-neo-90); Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`. **NEO-91 landed:** **`PrototypeNpcRegistry`** + combat-target migration — three NPC instance ids replace alpha/beta; per-archetype max HP from behavior catalog ([NEO-91](../../plans/NEO-91-implementation-plan.md)); [server README — Combat entity health (NEO-91)](../../../server/README.md#combat-entity-health-neo-80-neo-91). **NEO-92 landed:** **`IThreatStateStore`** + **`AggroOperations`** — first-hit acquire on damaging cast, leash clear on move/cast; cast/move/fixture hooks ([NEO-92](../../plans/NEO-92-implementation-plan.md)); [server README — Threat / aggro state (NEO-92)](../../../server/README.md#threat--aggro-state-neo-92). **NEO-93 landed:** **`INpcRuntimeStateStore`** + **`NpcRuntimeOperations.AdvanceAll`** — catalog-driven behavior state machine, lazy tick advance with delta cap; dev fixture reset ([NEO-93](../../plans/NEO-93-implementation-plan.md)); [server README — NPC runtime behavior state (NEO-93)](../../../server/README.md#npc-runtime-behavior-state-neo-93). **NEO-94 landed:** **`GET /game/world/npc-runtime-snapshot`** — `NpcRuntimeSnapshotWorldApi` + DTOs; lazy **`AdvanceAll`** on poll; nested **`activeTelegraph`** with server-computed **`windupRemainingSeconds`** ([NEO-94](../../plans/NEO-94-implementation-plan.md)); [server README — NPC runtime snapshot (NEO-94)](../../../server/README.md#npc-runtime-snapshot-neo-94); Bruno `bruno/neon-sprawl-server/npc-runtime-snapshot/`. **NEO-95 landed:** **`IPlayerCombatHealthStore`** + **`NpcAttackOperations.TryResolveTelegraphComplete`** — telegraph resolve via behavior **`attackAbilityId`** + ability **`maxRange`** / **`baseDamage`** ([NEO-95](../../plans/NEO-95-implementation-plan.md), [NEO-98](../../plans/NEO-98-implementation-plan.md)); [server README — Session player combat HP (NEO-95)](../../../server/README.md#session-player-combat-hp-neo-95); Bruno `bruno/neon-sprawl-server/combat-health/`. **NEO-96 landed:** comment-only **`telegraph_fired`** + **`npc_state_transition`** hook sites in **`NpcRuntimeOperations.AdvanceAll`** ([NEO-96](../../plans/NEO-96-implementation-plan.md)); [server README — NPC runtime telemetry hooks (NEO-96)](../../../server/README.md#npc-runtime-telemetry-hooks-neo-96). **NEO-97 landed:** client telegraph HUD — **`npc_runtime_client.gd`**, **`player_combat_health_client.gd`**, **`npc_runtime_hud_state.gd`**, **`TelegraphLabel`** / **`NpcStateLabel`** / **`PlayerCombatHpLabel`**; combat-active ~1 Hz poll ([NEO-97](../../plans/NEO-97-implementation-plan.md), [`NEO-97` manual QA](../../manual-qa/NEO-97.md)); [client README — NPC runtime + telegraph HUD (NEO-97)](../../../client/README.md#npc-runtime--telegraph-hud-neo-97). **NEO-98 landed:** playable NPC telegraph combat capstone — [`NEO-98` manual QA](../../manual-qa/NEO-98.md); `client/README.md` end-to-end NPC telegraph combat loop section ([NEO-98](../../plans/NEO-98-implementation-plan.md)). **NEO-98 server integration:** defeat clears aggro/runtime (**`TryStopOnTargetDefeat`**); telegraph damage range-gated via [`prototype_npc_abilities.json`](../../../content/abilities/prototype_npc_abilities.json) + behavior **`attackAbilityId`**. **Epic 5 Slice 2 client capstone complete.** **Backlog decomposed:** Epic 5 Slice 2 — [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md) **E5M2-01** [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98) **landed**. Three archetypes (`prototype_melee_pressure`, `prototype_ranged_control`, `prototype_elite_mini_boss`); replaces alpha/beta dummies with **`prototype_npc_melee` / `ranged` / `elite`**. Owns **`ThreatState`**, **`TelegraphEvent`**, **`GET …/npc-runtime-snapshot`**, session **`IPlayerCombatHealthStore`**. Upstream **E5.M1 Ready**. | [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2](E5_M2_NpcAiAndBehaviorProfiles.md), [NEO-87](../../plans/NEO-87-implementation-plan.md), [NEO-88](../../plans/NEO-88-implementation-plan.md), [NEO-89](../../plans/NEO-89-implementation-plan.md), [NEO-90](../../plans/NEO-90-implementation-plan.md), [NEO-91](../../plans/NEO-91-implementation-plan.md), [NEO-92](../../plans/NEO-92-implementation-plan.md), [NEO-93](../../plans/NEO-93-implementation-plan.md), [NEO-94](../../plans/NEO-94-implementation-plan.md), [NEO-95](../../plans/NEO-95-implementation-plan.md), [NEO-96](../../plans/NEO-96-implementation-plan.md), [NEO-97](../../plans/NEO-97-implementation-plan.md), [NEO-98](../../plans/NEO-98-implementation-plan.md), label **`E5.M2`** on NEO-87–NEO-98 | | E5.M3 | Ready | **E5M3-01 catalog landed ([NEO-100](https://linear.app/neon-sprawl/issue/NEO-100)):** encounter + reward-table schemas, `prototype_encounters.json`, `prototype_reward_tables.json`, CI gates. **E5M3-02 server load ([NEO-101](https://linear.app/neon-sprawl/issue/NEO-101)):** fail-fast startup loaders for encounter + reward-table catalogs (CI parity). **E5M3-03 registries ([NEO-102](https://linear.app/neon-sprawl/issue/NEO-102)):** `IEncounterDefinitionRegistry` + `IRewardTableDefinitionRegistry` + DI. **E5M3-04 HTTP read ([NEO-103](https://linear.app/neon-sprawl/issue/NEO-103)):** **`GET /game/world/encounter-definitions`** — `EncounterDefinitionsWorldApi` + nested **`rewardTable`** summary ([NEO-103](../../plans/NEO-103-implementation-plan.md)); [server README — Encounter definitions (NEO-103)](../../../server/README.md#encounter-definitions-neo-103); Bruno `bruno/neon-sprawl-server/encounter-definitions/`. **E5M3-05 stores ([NEO-104](https://linear.app/neon-sprawl/issue/NEO-104)):** **`IEncounterProgressStore`** + **`IEncounterCompletionStore`** + **`EncounterProgressOperations`** ([NEO-104](../../plans/NEO-104-implementation-plan.md)); [server README — Encounter progress (NEO-104)](../../../server/README.md#encounter-progress--completion-stores-neo-104). **E5M3-06 completion grants ([NEO-105](https://linear.app/neon-sprawl/issue/NEO-105)):** **`EncounterCompletionOperations`** + **`EncounterCompleteEvent`** result payload ([NEO-105](../../plans/NEO-105-implementation-plan.md)); [server README — Encounter completion (NEO-105)](../../../server/README.md#encounter-completion--inventory-grants-neo-105). **E5M3-07 combat wiring ([NEO-106](https://linear.app/neon-sprawl/issue/NEO-106)):** **`EncounterCombatWiring`** on **`AbilityCastApi`** ([NEO-106](../../plans/NEO-106-implementation-plan.md)); [server README — Encounter combat wiring (NEO-106)](../../../server/README.md#encounter-combat-wiring-neo-106). **E5M3-09 event record ([NEO-107](https://linear.app/neon-sprawl/issue/NEO-107)):** **`IEncounterCompleteEventStore`** + E7.M2 hook stub ([NEO-107](../../plans/NEO-107-implementation-plan.md)); [server README — Encounter complete event (NEO-107)](../../../server/README.md#encounter-complete-event-record-neo-107). **E5M3-08 per-player GET ([NEO-108](https://linear.app/neon-sprawl/issue/NEO-108)):** **`GET /game/players/{id}/encounter-progress`** — `EncounterProgressApi` + DTOs ([NEO-108](../../plans/NEO-108-implementation-plan.md)); [server README — Per-player encounter progress (NEO-108)](../../../server/README.md#per-player-encounter-progress-neo-108); Bruno `bruno/neon-sprawl-server/encounter-progress/`. **E5M3-10 telemetry ([NEO-109](https://linear.app/neon-sprawl/issue/NEO-109)):** comment-only **`encounter_start`**, **`encounter_complete`**, **`reward_attribution`**, **`encounter_complete_denied`** in **`EncounterProgressOperations`** / **`EncounterCompletionOperations`** ([NEO-109](../../plans/NEO-109-implementation-plan.md)); [server README — Encounter telemetry hooks (NEO-109)](../../../server/README.md#encounter-telemetry-hooks-neo-109). **E5M3-11 client HUD ([NEO-110](https://linear.app/neon-sprawl/issue/NEO-110)):** **`encounter_progress_client.gd`**, **`EncounterProgressLabel`** / **`EncounterCompleteLabel`**; boot + defeat-triggered GET; inventory refresh on **`completed`** ([NEO-110](../../plans/NEO-110-implementation-plan.md), [`NEO-110` manual QA](../../manual-qa/NEO-110.md)); [client README — Encounter progress + loot HUD (NEO-110)](../../../client/README.md#encounter-progress--loot-hud-neo-110). **E5M3-12 client capstone ([NEO-111](https://linear.app/neon-sprawl/issue/NEO-111)):** playable encounter clear loop — [`NEO-111` manual QA](../../manual-qa/NEO-111.md); [client README — End-to-end encounter clear loop (NEO-111)](../../../client/README.md#end-to-end-encounter-clear-loop-neo-111); plan [NEO-111](../../plans/NEO-111-implementation-plan.md). **Epic 5 Slice 3 client capstone complete.** Epic 5 Slice 3 backlog [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md) **E5M3-01** [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) → **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) **landed**. Prototype spine: **`prototype_combat_pocket`** → **`prototype_combat_pocket_clear`**; idempotent completion; per-defeat gig XP unchanged ([NEO-44](../../plans/NEO-44-implementation-plan.md)). Upstream **E5.M2 Ready**, **E3.M3** inventory landed. | [NEO-111 plan](../../plans/NEO-111-implementation-plan.md), [NEO-110 plan](../../plans/NEO-110-implementation-plan.md), [NEO-109 plan](../../plans/NEO-109-implementation-plan.md), [NEO-108 plan](../../plans/NEO-108-implementation-plan.md), [NEO-107 plan](../../plans/NEO-107-implementation-plan.md), [NEO-106 plan](../../plans/NEO-106-implementation-plan.md), [NEO-105 plan](../../plans/NEO-105-implementation-plan.md), [NEO-104 plan](../../plans/NEO-104-implementation-plan.md), [NEO-103 plan](../../plans/NEO-103-implementation-plan.md), [NEO-102 plan](../../plans/NEO-102-implementation-plan.md), [E5_M3](E5_M3_EncounterAndRewardTables.md), label **`E5.M3`** on NEO-100–NEO-111 | -| E7.M1 | Planned | **Backlog decomposed (not started):** Epic 7 Slice 1 — [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Four frozen quests (3 onboarding + 1 gather→craft→combat chain); solo only; state-only completion in Slice 1 (rewards deferred to E7.M2). Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **`contract_handoff_token`** + **`EncounterCompleteEvent`** **Ready**. | [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1](E7_M1_QuestStateMachine.md), label **`E7.M1`** on NEO-112–NEO-123 | +| E7.M1 | Planned | **E7M1-01 catalog landed ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)):** quest schemas, `prototype_quests.json`, CI gates (four frozen quest ids, objective cross-refs, acyclic prerequisites, chain terminal token). **Backlog decomposed (not started beyond catalog):** Epic 7 Slice 1 — [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Four frozen quests (3 onboarding + 1 gather→craft→combat chain); solo only; state-only completion in Slice 1 (rewards deferred to E7.M2). Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **`contract_handoff_token`** + **`EncounterCompleteEvent`** **Ready**. Register row stays **Planned** until **NEO-113** server load lands. | [NEO-112 plan](../../plans/NEO-112-implementation-plan.md), [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1](E7_M1_QuestStateMachine.md), label **`E7.M1`** on NEO-112–NEO-123 | --- diff --git a/docs/plans/E7M1-prototype-backlog.md b/docs/plans/E7M1-prototype-backlog.md index 2ce41c3..0c3e726 100644 --- a/docs/plans/E7M1-prototype-backlog.md +++ b/docs/plans/E7M1-prototype-backlog.md @@ -93,10 +93,10 @@ Working backlog for **Epic 7 — Slice 1** ([quest core and persistence](../deco **Acceptance criteria** -- [ ] 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. +- [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. **Client counterpart:** none (infrastructure-only). diff --git a/docs/plans/NEO-112-implementation-plan.md b/docs/plans/NEO-112-implementation-plan.md index 7230ff6..2aa97c4 100644 --- a/docs/plans/NEO-112-implementation-plan.md +++ b/docs/plans/NEO-112-implementation-plan.md @@ -41,10 +41,17 @@ ## 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). +- [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 From c55c373f7faa95313dce5c009e4b3173c71cf080 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 21:16:56 -0400 Subject: [PATCH 6/8] NEO-112: add code review for QuestDef catalog + CI gates --- docs/reviews/2026-05-31-NEO-112.md | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/reviews/2026-05-31-NEO-112.md diff --git a/docs/reviews/2026-05-31-NEO-112.md b/docs/reviews/2026-05-31-NEO-112.md new file mode 100644 index 0000000..bb25304 --- /dev/null +++ b/docs/reviews/2026-05-31-NEO-112.md @@ -0,0 +1,66 @@ +# Code review — NEO-112 (E7M1-01) + +**Date:** 2026-05-31 +**Scope:** Branch `NEO-112-e7m1-prototype-questdef-catalog-schemas-ci` vs `47105cc` (merge-base on `main`) — commits `be1dae3` … `765075f` +**Base:** `47105cc` (main at branch point, post NEO-111 merge) + +## Verdict + +**Approve with nits** + +## Summary + +NEO-112 delivers the Epic 7 Slice 1 **content foundation**: three JSON Schemas (`quest-def`, `quest-step-def`, `quest-objective-def`), a four-row `prototype_quests.json` catalog aligned to the E7.M1 freeze table, and ~300 lines of CI gates in `validate_content.py` (four-id allowlist, acyclic prerequisites, item/recipe/encounter cross-refs, operator-chain terminal token gate, duplicate quest/step/objective ids). Documentation is thorough—implementation plan reconciliation, CT.M1 paragraph, `content/README.md`, alignment register, and the full E7M1 prototype backlog land together. No server, C#, or Godot changes (correct for E7M1-01). Risk is low: pattern mirrors NEO-100/NEO-87; happy-path validation passes locally. + +## Documentation checked + +| Path | Result | +|------|--------| +| `docs/plans/NEO-112-implementation-plan.md` | **Matches** — acceptance checklist checked; reconciliation section accurate. | +| `docs/plans/E7M1-prototype-backlog.md` (E7M1-01) | **Matches** — AC checked; scope/out-of-scope correct; client counterpart none. | +| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | **Partially matches** — freeze table and CI enforcement line match shipped gates; Status still says “implementation not started” though E7M1-01 catalog is landed. | +| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | **Matches** — quest catalog PR gate paragraph describes shipped validators. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E7.M1 row notes NEO-112 catalog; register stays Planned until NEO-113. | +| `docs/decomposition/modules/module_dependency_register.md` | **Partially matches** — E7.M1 note added; “until **NEO-112** lands” is stale (should reference NEO-113 server load, per alignment row). | +| `content/README.md` | **Matches** — `quests/` table row + E7M1 prototype paragraph. | +| `docs/decomposition/epics/epic_07_quest_faction.md` | **Matches** — Slice 1 backlog link + Godot DoD clarification. | +| Full-stack epic decomposition | **N/A** — infrastructure-only story; no client counterpart required until NEO-122/NEO-123. | + +## Blocking issues + +(none) + +## Suggestions + +1. **Refresh `module_dependency_register.md` E7.M1 note** — Change “Register row stays **Planned** until **NEO-112** lands” to “until **NEO-113** server load lands” so it matches `documentation_and_implementation_alignment.md` after this merge. + +2. **Clarify E7.M1 module Status line** — Replace “implementation not started” with wording that E7M1-01 catalog (**NEO-112**) is landed and runtime work starts at **NEO-113**. + +## Nits + +- Nit: E7M1 kickoff table row “Onboarding order: gather → refine → combat → chain” oversimplifies prerequisites — **`prototype_quest_combat_intro`** has no prereq (parallel path); freeze table and catalog are correct. + +- Nit: `quest-objective-def.schema.json` lists all kind-specific fields in `properties` plus `oneOf` branches, so schema validation may accept wrong-field-at-wrong-kind (e.g. `recipeId` on `encounter_complete`) if both are present — plan adopted this tradeoff; CI cross-ref gate still catches unknown ids. + +- Nit: Duplicate step/objective ids are enforced **within** a quest only, not globally across quests — acceptable for prototype; E7M1-04 HTTP projection should namespace by quest id. + +- Nit: Branch bundles the full E7M1 backlog decomposition (~390 lines) with E7M1-01 implementation — large diff but intentional and documented. + +## Verification + +```bash +# Happy path (primary regression signal — also runs in pr-gate.yml) +python3 -m pip install -r scripts/requirements-content.txt +python3 scripts/validate_content.py + +# Manual negative cases (from NEO-112 plan) +# 1. Duplicate quest id across or within files → non-zero exit +# 2. Remove a frozen quest id from catalog → four-id gate error +# 3. Unknown itemId/recipeId/encounterId in objective → cross-ref error +# 4. Cyclic prerequisiteQuestIds → prerequisite gate error +# 5. Unknown prerequisite id → prerequisite gate error +# 6. Chain terminal step not inventory_has_item contract_handoff_token ×1 → chain gate error +# 7. Invalid kind or missing required field per kind → schema error +``` + +**Reviewer note:** Happy path executed successfully in review environment (`content OK` … `4 unique quest id(s)`). From 50956bac11ff93ebfd434665073311632c554cce Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 21:17:37 -0400 Subject: [PATCH 7/8] NEO-112: address code review doc alignment findings --- .../decomposition/modules/E7_M1_QuestStateMachine.md | 2 +- .../modules/module_dependency_register.md | 2 +- docs/plans/E7M1-prototype-backlog.md | 2 +- docs/reviews/2026-05-31-NEO-112.md | 12 +++++++----- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/decomposition/modules/E7_M1_QuestStateMachine.md b/docs/decomposition/modules/E7_M1_QuestStateMachine.md index 35cc282..c639563 100644 --- a/docs/decomposition/modules/E7_M1_QuestStateMachine.md +++ b/docs/decomposition/modules/E7_M1_QuestStateMachine.md @@ -7,7 +7,7 @@ | **Module ID** | E7.M1 | | **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) | | **Stage target** | Prototype | -| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) (decomposed; implementation not started) | +| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) catalog **landed** (schemas + CI); runtime from **E7M1-02** [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) → capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) | | **Linear** | Label **`E7.M1`** · [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) | ## Purpose diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index ad1dfd6..45546db 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -109,7 +109,7 @@ Gameplay **content and curves** default to **boot load** with optional **dev-onl |---|---|---|---|---|---| | E7.M1 | QuestStateMachine | E3.M2, E5.M1 | QuestDef, QuestStepState, QuestStateTransition | Prototype | Planned | -**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Register row stays **Planned** until **NEO-112** lands. +**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7M1-01 / NEO-112** catalog landed (schemas + CI); register row stays **Planned** until **NEO-113** server load lands. | E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Planned | | E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned | | E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned | diff --git a/docs/plans/E7M1-prototype-backlog.md b/docs/plans/E7M1-prototype-backlog.md index 0c3e726..ec395c0 100644 --- a/docs/plans/E7M1-prototype-backlog.md +++ b/docs/plans/E7M1-prototype-backlog.md @@ -66,7 +66,7 @@ Working backlog for **Epic 7 — Slice 1** ([quest core and persistence](../deco | Objective advance | **Server hooks** on gather/craft/encounter success | Client displays GET snapshot only ([client_server_authority.md](../decomposition/modules/client_server_authority.md)) | | Objective kinds (v1) | **`gather_item`**, **`craft_recipe`**, **`encounter_complete`**, **`inventory_has_item`** | Covers gather→craft→combat chain without custom scripting VM | | Chain quest | **4 sequential steps** in one `QuestDef` | Single HUD row with step index; epic “integrated chain” | -| Prerequisites | **`prerequisiteQuestIds`** enforced on accept | Onboarding order: gather → refine → combat → chain | +| Prerequisites | **`prerequisiteQuestIds`** enforced on accept | Gather and combat intros parallel (no prereq); refine requires gather intro; chain requires all three intros | | Rewards (Slice 1) | **State-only completion**; no duplicate item/XP grants | Loot stays on gather/craft/encounter paths; E7.M2 owns bundles | | Token handoff | Chain final step **`inventory_has_item`** **`contract_handoff_token`** | Uses E5.M3 encounter loot; no consume mutation in Slice 1 | | Idempotency | **Once per player per quest id** complete | Slice 1 AC: no duped quest completion | diff --git a/docs/reviews/2026-05-31-NEO-112.md b/docs/reviews/2026-05-31-NEO-112.md index bb25304..8d1d365 100644 --- a/docs/reviews/2026-05-31-NEO-112.md +++ b/docs/reviews/2026-05-31-NEO-112.md @@ -4,6 +4,8 @@ **Scope:** Branch `NEO-112-e7m1-prototype-questdef-catalog-schemas-ci` vs `47105cc` (merge-base on `main`) — commits `be1dae3` … `765075f` **Base:** `47105cc` (main at branch point, post NEO-111 merge) +**Follow-up:** Suggestions and the prerequisites nit below are **done** (strikethrough + **Done.**). + ## Verdict **Approve with nits** @@ -18,10 +20,10 @@ NEO-112 delivers the Epic 7 Slice 1 **content foundation**: three JSON Schemas ( |------|--------| | `docs/plans/NEO-112-implementation-plan.md` | **Matches** — acceptance checklist checked; reconciliation section accurate. | | `docs/plans/E7M1-prototype-backlog.md` (E7M1-01) | **Matches** — AC checked; scope/out-of-scope correct; client counterpart none. | -| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | **Partially matches** — freeze table and CI enforcement line match shipped gates; Status still says “implementation not started” though E7M1-01 catalog is landed. | +| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | **Matches** — freeze table, CI enforcement line, and Status note NEO-112 catalog landed / runtime from NEO-113. | | `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | **Matches** — quest catalog PR gate paragraph describes shipped validators. | | `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E7.M1 row notes NEO-112 catalog; register stays Planned until NEO-113. | -| `docs/decomposition/modules/module_dependency_register.md` | **Partially matches** — E7.M1 note added; “until **NEO-112** lands” is stale (should reference NEO-113 server load, per alignment row). | +| `docs/decomposition/modules/module_dependency_register.md` | **Matches** — E7.M1 note references NEO-112 catalog landed; register **Planned** until NEO-113 server load. | | `content/README.md` | **Matches** — `quests/` table row + E7M1 prototype paragraph. | | `docs/decomposition/epics/epic_07_quest_faction.md` | **Matches** — Slice 1 backlog link + Godot DoD clarification. | | Full-stack epic decomposition | **N/A** — infrastructure-only story; no client counterpart required until NEO-122/NEO-123. | @@ -32,13 +34,13 @@ NEO-112 delivers the Epic 7 Slice 1 **content foundation**: three JSON Schemas ( ## Suggestions -1. **Refresh `module_dependency_register.md` E7.M1 note** — Change “Register row stays **Planned** until **NEO-112** lands” to “until **NEO-113** server load lands” so it matches `documentation_and_implementation_alignment.md` after this merge. +1. ~~**Refresh `module_dependency_register.md` E7.M1 note** — Change “Register row stays **Planned** until **NEO-112** lands” to “until **NEO-113** server load lands” so it matches `documentation_and_implementation_alignment.md` after this merge.~~ **Done.** — `module_dependency_register.md` E7.M1 note updated. -2. **Clarify E7.M1 module Status line** — Replace “implementation not started” with wording that E7M1-01 catalog (**NEO-112**) is landed and runtime work starts at **NEO-113**. +2. ~~**Clarify E7.M1 module Status line** — Replace “implementation not started” with wording that E7M1-01 catalog (**NEO-112**) is landed and runtime work starts at **NEO-113**.~~ **Done.** — `E7_M1_QuestStateMachine.md` Status row updated. ## Nits -- Nit: E7M1 kickoff table row “Onboarding order: gather → refine → combat → chain” oversimplifies prerequisites — **`prototype_quest_combat_intro`** has no prereq (parallel path); freeze table and catalog are correct. +- ~~Nit: E7M1 kickoff table row “Onboarding order: gather → refine → combat → chain” oversimplifies prerequisites — **`prototype_quest_combat_intro`** has no prereq (parallel path); freeze table and catalog are correct.~~ **Done.** — `E7M1-prototype-backlog.md` kickoff Prerequisites row clarified. - Nit: `quest-objective-def.schema.json` lists all kind-specific fields in `properties` plus `oneOf` branches, so schema validation may accept wrong-field-at-wrong-kind (e.g. `recipeId` on `encounter_complete`) if both are present — plan adopted this tradeoff; CI cross-ref gate still catches unknown ids. From a4ed4abadbe666839a37d9b3233b84a0356c6685 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 21:23:45 -0400 Subject: [PATCH 8/8] NEO-112: enforce objective id uniqueness per quest in CI Move obj_ids_seen to quest scope so duplicate objective ids are rejected across steps, matching CT.M1 documentation and E7M1-04 HTTP projection expectations (Bugbot PR #151). --- docs/reviews/2026-05-31-NEO-112.md | 2 +- scripts/validate_content.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reviews/2026-05-31-NEO-112.md b/docs/reviews/2026-05-31-NEO-112.md index 8d1d365..614448a 100644 --- a/docs/reviews/2026-05-31-NEO-112.md +++ b/docs/reviews/2026-05-31-NEO-112.md @@ -44,7 +44,7 @@ NEO-112 delivers the Epic 7 Slice 1 **content foundation**: three JSON Schemas ( - Nit: `quest-objective-def.schema.json` lists all kind-specific fields in `properties` plus `oneOf` branches, so schema validation may accept wrong-field-at-wrong-kind (e.g. `recipeId` on `encounter_complete`) if both are present — plan adopted this tradeoff; CI cross-ref gate still catches unknown ids. -- Nit: Duplicate step/objective ids are enforced **within** a quest only, not globally across quests — acceptable for prototype; E7M1-04 HTTP projection should namespace by quest id. +- ~~Nit: Duplicate step/objective ids are enforced **within** a quest only, not globally across quests — acceptable for prototype; E7M1-04 HTTP projection should namespace by quest id.~~ **Superseded (Bugbot):** objective duplicate check was incorrectly scoped per-step; fixed in `validate_content.py` to quest-level (step ids remain per-quest; objective ids now per-quest too). Cross-quest uniqueness still not enforced (acceptable for prototype). - Nit: Branch bundles the full E7M1 backlog decomposition (~390 lines) with E7M1-01 implementation — large diff but intentional and documented. diff --git a/scripts/validate_content.py b/scripts/validate_content.py index 76f5de2..b1287b6 100644 --- a/scripts/validate_content.py +++ b/scripts/validate_content.py @@ -905,6 +905,7 @@ def _validate_quest_catalogs( steps = row.get("steps") if isinstance(steps, list): step_ids_seen: set[str] = set() + obj_ids_seen: set[str] = set() for si, step in enumerate(steps): if not isinstance(step, dict): continue @@ -920,7 +921,6 @@ def _validate_quest_catalogs( step_ids_seen.add(sid) objectives = step.get("objectives") if isinstance(objectives, list): - obj_ids_seen: set[str] = set() for oi, obj in enumerate(objectives): if not isinstance(obj, dict): continue @@ -929,7 +929,7 @@ def _validate_quest_catalogs( if oid in obj_ids_seen: print( f"error: {rel} quests[{i}] steps[{si}] objectives[{oi}]: " - f"duplicate objective id {oid!r}", + f"duplicate objective id {oid!r} within quest", file=sys.stderr, ) errors += 1