diff --git a/docs/plans/NEO-65-implementation-plan.md b/docs/plans/NEO-65-implementation-plan.md new file mode 100644 index 0000000..c14da8c --- /dev/null +++ b/docs/plans/NEO-65-implementation-plan.md @@ -0,0 +1,129 @@ +# NEO-65 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-65 | +| **Title** | E3.M2: Prototype RecipeDef catalog + schemas + CI | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-65/e3m2-prototype-recipedef-catalog-schemas-ci | +| **Module** | [E3.M2 — RefinementAndRecipeExecution](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) · Epic 3 Slice 3 · backlog **E3M2-01** | +| **Branch** | `NEO-65-e3m2-prototype-recipedef-catalog-schemas-ci` | +| **Upstream** | [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) — frozen six-item catalog; prototype skill trio includes **`refine`**. | +| **Pattern** | [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) / [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57) — row JSON Schema + catalog envelope + `scripts/validate_content.py` Slice gate | +| **Blocks** | [NEO-66](https://linear.app/neon-sprawl/issue/NEO-66) — server recipe catalog load | + +## Kickoff clarifications + +**No blocking clarifications needed.** Linear AC, [E3M2-prototype-backlog — E3M2-01](E3M2-prototype-backlog.md#e3m2-01--prototype-recipedef-catalog--schemas--ci), [E3.M2 kickoff decisions](E3M2-prototype-backlog.md#kickoff-decisions-decomposition-defaults), and the [Slice 3 freeze table](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md#prototype-slice-3-freeze-e3m2-01) fully specify recipe count, ids, I/O quantities, `recipeKind`, and `requiredSkillId`. Schema shape follows NEO-50 / NEO-57 precedent: + +| Topic | Agent recommendation | Status | +|--------|----------------------|--------| +| **Catalog envelope** | `{ "schemaVersion": 1, "recipes": [ … ] }` in `content/recipes/prototype_recipes.json` (mirror items / resource-nodes). | **Adopted** (NEO-50 precedent) | +| **`displayName` on `RecipeDef`** | **Required** — mirror `ItemDef`; supports E3M2-04 GET without schema churn. | **Adopted** (NEO-50 / NEO-57 precedent) | +| **Optional forward-compat stubs** | Optional `requiredSkillLevel` (int ≥ 0) and `stationTag` (string) on `recipe-def.schema.json`; **omitted** on all eight prototype rows (Slice 3 has no gates / stations). | **Adopted** (NEO-50 stub pattern) | +| **`requiredSkillLevel` in prototype rows** | **Omitted** — decomposition default; no level gates in Slice 3. | **Adopted** (backlog kickoff table) | + +## Goal, scope, and out-of-scope + +**Goal:** Lock content shape and CI validation for **eight** frozen `RecipeDef` rows before server load (NEO-66+). + +**In scope (from Linear + [E3M2-01](E3M2-prototype-backlog.md#e3m2-01--prototype-recipedef-catalog--schemas--ci)):** + +- `content/schemas/recipe-def.schema.json` and `content/schemas/recipe-io-row.schema.json`. +- `content/recipes/prototype_recipes.json` — eight frozen ids (freeze table below). +- `scripts/validate_content.py` — schema validation, duplicate `id`, exact eight-recipe allowlist, `recipeKind` coverage (**`process`** + **`make`**), cross-check every input/output **`itemId`** against item catalogs, every **`requiredSkillId`** against skill catalogs (prototype gate: all **`refine`**). +- Designer note in **E3.M2** module doc + `content/README.md` Slice 3 paragraph. +- **CT.M1** PR gate paragraph for recipe catalogs. + +**Out of scope (from Linear):** + +- Server loader, craft engine, HTTP (NEO-66+). + +## Acceptance criteria checklist + +- [ ] PR gate validates recipe JSON against schema. +- [ ] Exactly eight prototype recipe ids; duplicate `id` fails CI. +- [ ] Every input/output references a valid frozen item id; every recipe references **`refine`**. +- [ ] Stable id list documented in module doc freeze box. + +## Technical approach + +1. **Row schema — I/O row (`recipe-io-row.schema.json`):** Draft 2020-12 object; `additionalProperties: false`. Required: `itemId`, `quantity`. `itemId` pattern `^[a-z][a-z0-9_]*$`. `quantity` integer ≥ 1. + +2. **Row schema — `RecipeDef` (`recipe-def.schema.json`):** Draft 2020-12 object; `additionalProperties: false`. Required: `id`, `displayName`, `recipeKind`, `requiredSkillId`, `inputs`, `outputs`. `id` pattern `^[a-z][a-z0-9_]*$`. `recipeKind` enum: `process` \| `make`. `requiredSkillId` string (same id pattern). `inputs` / `outputs`: arrays with `minItems: 1`; each element validates against `recipe-io-row.schema.json` (via `$ref`). Optional stubs (unused in prototype rows): `requiredSkillLevel` (int ≥ 0), `stationTag` (string). + +3. **Catalog file:** `content/recipes/prototype_recipes.json` with `schemaVersion: 1` and eight rows from the freeze table; placeholder `displayName` strings acceptable. + +4. **Frozen content values (prototype Slice 3):** + + | `RecipeDef.id` | `recipeKind` | inputs → outputs | + |----------------|--------------|------------------| + | `refine_scrap_standard` | `process` | 5× `scrap_metal_bulk` → 1× `refined_plate_stock` | + | `refine_scrap_efficient` | `process` | 10× `scrap_metal_bulk` → 2× `refined_plate_stock` | + | `make_field_stim_mk0` | `make` | 2× `refined_plate_stock` + 1× `scrap_metal_bulk` → 1× `field_stim_mk0` | + | `make_field_stim_batch` | `make` | 5× `refined_plate_stock` + 3× `scrap_metal_bulk` → 3× `field_stim_mk0` | + | `make_prototype_armor` | `make` | 8× `refined_plate_stock` + 5× `scrap_metal_bulk` → 1× `prototype_armor_shell` | + | `make_armor_quick` | `make` | 4× `refined_plate_stock` + 3× `scrap_metal_bulk` → 1× `prototype_armor_shell` | + | `make_survey_drone_kit` | `make` | 3× `refined_plate_stock` + 2× `scrap_metal_bulk` → 1× `survey_drone_kit` | + | `make_contract_token` | `make` | 2× `refined_plate_stock` + 1× `scrap_metal_bulk` → 1× `contract_handoff_token` | + + All rows: `requiredSkillId: "refine"`. No `requiredSkillLevel` or `stationTag` on prototype rows. + +5. **`validate_content.py`:** + - Add `RECIPE_SCHEMA`, `RECIPE_IO_ROW_SCHEMA`, `RECIPES_DIR`; discover `content/recipes/*_recipes.json`. + - `_validate_recipe_catalogs`: envelope `schemaVersion: 1`, top-level `recipes` array; validate each row against `recipe-def.schema.json`; track `seen_ids`; reject duplicates across files. + - Post-schema cross-checks per row: every I/O `itemId` ∈ loaded item ids; `requiredSkillId` ∈ loaded skill ids. + - **`_prototype_slice3_recipe_gate`:** ids must equal `PROTOTYPE_SLICE3_RECIPE_IDS`; `recipeKind` set must include **`process`** and **`make`**; every row `requiredSkillId` must be **`refine`** (prototype Slice 3 contract). + - Run recipe validation **after** skill + item catalogs succeed (reuse `seen_ids` / `item_seen_ids`). + - Require recipe schema files in `main()` startup checks; extend success summary with recipe file + id counts. + - Keep `PROTOTYPE_SLICE3_RECIPE_IDS` in sync with [E3.M2 freeze table](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md#prototype-slice-3-freeze-e3m2-01) and future NEO-66 server loader. + +6. **Docs:** + - `content/README.md` — ensure `recipes/` table row + Slice 3 freeze paragraph (may already exist on branch from decomposition prep). + - `E3_M2_RefinementAndRecipeExecution.md` — ensure freeze table + CI rules line (may already exist on branch). + - `CT_M1_ContentValidationPipeline.md` — recipe catalog PR gate paragraph. + - `documentation_and_implementation_alignment.md` — E3.M2 row → **In Progress** / NEO-65. + - `E3M2-prototype-backlog.md` — E3M2-01 checkboxes when implementation completes. + +7. **No server/C#/client changes** in this story. + +## Files to add + +| Path | Purpose | +|------|---------| +| `content/schemas/recipe-io-row.schema.json` | JSON Schema for a single recipe input/output row. | +| `content/schemas/recipe-def.schema.json` | JSON Schema for a single `RecipeDef` row. | +| `content/recipes/prototype_recipes.json` | Prototype eight-recipe catalog (`schemaVersion` + `recipes` array). | +| `docs/plans/NEO-65-implementation-plan.md` | This plan. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `scripts/validate_content.py` | Load/validate recipe catalogs; duplicate `id`; Slice 3 eight-id + `recipeKind` + `refine` gates; I/O item + skill cross-checks; success summary. | +| `content/README.md` | Document `content/recipes/`, schema paths, Slice 3 freeze (active catalog). | +| `docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md` | Confirm freeze table + CI enforcement note; link to plan when complete. | +| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for recipe catalog validation. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M2 / NEO-65 status after catalog lands. | +| `docs/plans/E3M2-prototype-backlog.md` | E3M2-01 checkboxes + landed note when complete. | + +## Tests + +| Item | Coverage | +|------|----------| +| **CI / PR gate** | `.github/workflows/pr-gate.yml` already runs `python scripts/validate_content.py` — extended script is the regression signal. | +| **Manual negative cases** | From repo root after implementation: (1) duplicate recipe `id` → non-zero exit; (2) remove one frozen id → gate error; (3) unknown `itemId` in inputs → cross-check error; (4) `requiredSkillId: "salvage"` → Slice 3 gate error; (5) break schema (empty `outputs`) → schema error. | +| **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports recipe catalog + existing catalogs OK. | + +No dedicated pytest module in-repo today (same as NEO-50 / NEO-57). + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| **Catalog growth** | Slice 3 gate requires **exactly** the eight frozen ids; expanding the roster needs a follow-up issue to change `PROTOTYPE_SLICE3_RECIPE_IDS`. | **adopted** | +| **C# vs Python drift** | None until NEO-66; document “keep in sync” constant names in both places when server load lands. | **adopted** | +| **Branch carries E3M2 decomposition prep** | Commit decomposition doc deltas (freeze table, backlog, README) with implementation or as first doc batch on this branch. | **pending** | + +None blocking.