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

12 KiB
Raw Blame History

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 · Epic 7 Slice 1 · backlog E7M1-01
Branch NEO-112-e7m1-prototype-questdef-catalog-schemas-ci
Precursor E3.M1 gather Ready · E3.M2 craft Ready · E5.M3 encounter Ready (frozen item/recipe/encounter ids)
Pattern NEO-100 — row JSON Schema + catalog envelope + scripts/validate_content.py gate
Blocks 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, E7.M1 freeze table, and E7M1-01 backlog entry. 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):

  • 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 (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 / 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).

Implementation reconciliation (shipped)

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 — itemIdPROTOTYPE_SLICE1_ITEM_IDS; recipeIdPROTOTYPE_SLICE3_RECIPE_IDS; encounterIdPROTOTYPE_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