130 lines
9.9 KiB
Markdown
130 lines
9.9 KiB
Markdown
# 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
|
||
|
||
- [x] PR gate validates recipe JSON against schema.
|
||
- [x] Exactly eight prototype recipe ids; duplicate `id` fails CI.
|
||
- [x] Every input/output references a valid frozen item id; every recipe references **`refine`**.
|
||
- [x] 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. | **done** — prep landed in prior commits; catalog + CI in this story. |
|
||
|
||
None blocking.
|