# NEO-50 — Implementation plan ## Story reference | Field | Value | |--------|--------| | **Key** | NEO-50 | | **Title** | E3.M3: Lock prototype ItemDef starter set + schemas + CI | | **Linear** | https://linear.app/neon-sprawl/issue/NEO-50/e3m3-lock-prototype-itemdef-starter-set-schemas-ci | | **Module** | [E3.M3 — ItemizationAndInventorySchema](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) · Epic 3 Slice 1 · backlog **E3M3-01** | | **Branch** | `NEO-50-itemdef-starter-set-schemas-ci` | ## Kickoff clarifications | Topic | Question | Answer | |--------|----------|--------| | **Prototype role field** | `prototypeRole` vs `designBucket` vs `tags` only? | **Agent recommendation (accepted):** `prototypeRole` enum — one value per frozen archetype (`material` \| `intermediate` \| `consumable` \| `utility` \| `quest_token` \| `equip_stub`). CI requires **exactly one row per role** (same pattern as skill category coverage). User asked for suggestion; design buckets in [items.md](../game-design/items.md) overlap and are not 1:1 with the six-id spine. | | **Stack + slot metadata** | Explicit fields vs deferred? | **Agent recommendation (accepted):** **Explicit per row:** `stackMax` (int ≥ 1) and `inventorySlotKind` (`bag` \| `equipment`). Locks stack/slot rules in content before NEO-51 server load and E3M3-05 inventory engine. | | **Forward-compat stubs** | Optional `rarity` / `bindPolicy` / `durabilityMax` on schema? | **Yes** — optional fields on `ItemDef`; omit on all six prototype rows (documented unused in v1). User confirmed. | | **Catalog envelope** | File shape? | **Mirror skills:** `{ "schemaVersion": 1, "items": [ … ] }` in `content/items/prototype_items.json`; each row validates against `content/schemas/item-def.schema.json`. | | **Base branch** | Branch from `main` after PR #84? | **Yes** — `main` includes [E3M3-prototype-backlog](E3M3-prototype-backlog.md) and README freeze pointers. | ### Frozen prototype ids (Slice 1 — item spine) | `id` | `prototypeRole` | `inventorySlotKind` | `stackMax` (initial) | |------|-----------------|---------------------|----------------------| | `scrap_metal_bulk` | `material` | `bag` | `999` | | `refined_plate_stock` | `intermediate` | `bag` | `999` | | `field_stim_mk0` | `consumable` | `bag` | `20` | | `survey_drone_kit` | `utility` | `bag` | `1` | | `contract_handoff_token` | `quest_token` | `bag` | `1` | | `prototype_armor_shell` | `equip_stub` | `equipment` | `1` | **Rules:** Do **not** rename `id` values after ship — change `displayName` only, or add a new `id` in a follow-up issue. Relaxing the six-id or six-role CI gate requires a new Linear issue. ## Goal, scope, and out-of-scope **Goal:** Lock the **six-item** prototype catalog under `content/items/` with JSON Schema and CI validation (`scripts/validate_content.py`) so ids, stack rules, and slot metadata are stable before server load (NEO-51+). **In scope (from Linear / [E3M3-prototype-backlog](E3M3-prototype-backlog.md#e3m3-01--prototype-itemdef-starter-set--schemas--ci)):** - `content/schemas/item-def.schema.json` — single-row `ItemDef` contract. - `content/items/prototype_items.json` — six frozen ids (table above). - `scripts/validate_content.py` — schema validation, duplicate `id`, `prototypeRole` coverage, exact six-id allowlist. - Designer note in **E3.M3** module doc + `content/README.md`: stack rules, bag vs equipment slot, v1 scope (no durability mutation). **Out of scope (from Linear):** - Server runtime load, inventory store, HTTP, client HUD (NEO-51+). ## Acceptance criteria checklist - [x] PR gate / `validate_content.py` fails on invalid rows, schema mismatch, or duplicate item `id`. - [x] Exactly **six** prototype item ids; wrong id set fails CI. - [x] Each `prototypeRole` appears **exactly once** across the catalog. - [x] Six prototype ids documented as **frozen** in E3.M3 module doc + `content/README.md`. ## Technical approach 1. **Row schema (`item-def.schema.json`):** Draft 2020-12 object; `additionalProperties: false`. Required: `id`, `displayName`, `prototypeRole`, `stackMax`, `inventorySlotKind`. Enums: `prototypeRole` (six values above), `inventorySlotKind` (`bag` \| `equipment`). `id` pattern `^[a-z][a-z0-9_]*$` (same as skills). Optional stubs: `rarity` (`common` \| `uncommon` \| `rare` \| `epic`), `bindPolicy` (`none` \| `bind_on_acquire` \| `bind_on_equip`), `durabilityMax` (integer ≥ 0) — all optional, unused in prototype rows. 2. **Catalog file:** `content/items/prototype_items.json` with `schemaVersion: 1` and six rows matching the freeze table; placeholder `displayName` strings acceptable. 3. **`validate_content.py`:** After existing skill / level-curve / mastery validation (or in parallel before final success): - Discover `content/items/*_items.json`. - Validate each row against `item-def.schema.json`. - Track `seen_ids` across files; reject duplicates. - **`_prototype_slice1_item_gate`:** ids must equal `PROTOTYPE_SLICE1_ITEM_IDS`; roles must cover each `prototypeRole` exactly once; `equip_stub` row must use `inventorySlotKind: equipment` and `stackMax: 1` (post-schema sanity). 4. **Docs:** E3.M3 — **Prototype Slice 1 freeze** subsection + designer note (bag vs equipment, stackMax, no durability mutation in v1). `content/README.md` — promote items path from “planned” to active with freeze list. CT.M1 — add item catalog paragraph. `documentation_and_implementation_alignment.md` — E3.M3 row → **In Progress** / NEO-50. 5. **No server/C#/client changes** in this story. ## Files to add | Path | Purpose | |------|---------| | `content/schemas/item-def.schema.json` | JSON Schema for a single `ItemDef` row. | | `content/items/prototype_items.json` | Prototype six-item catalog (`schemaVersion` + `items` array). | ## Files to modify | Path | Rationale | |------|-----------| | `scripts/validate_content.py` | Load/validate item catalogs; duplicate `id`; Slice 1 six-id + six-role gates; update success summary line. | | `content/README.md` | Document `content/items/`, schema path, Slice 1 freeze (active, not “planned”). | | `docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md` | Freeze table, designer note (stack/slot/v1 scope), link to plan. | | `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for item catalog validation. | | `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M3 / NEO-50 status after catalog lands. | ## 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 `id` in catalog → non-zero exit; (2) remove one frozen id → gate error; (3) break schema (invalid `stackMax`) → schema error; (4) swap two `prototypeRole` values → role coverage error. | | **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports item catalog + existing catalogs OK. | No dedicated pytest module in-repo today (same as NEO-33 / NEO-45). ## Open questions / risks - **Catalog growth:** Slice 1 gate requires **exactly** the six frozen ids; expanding the roster needs a follow-up issue to change `PROTOTYPE_SLICE1_ITEM_IDS` and role gate. - **C# vs Python drift:** None until NEO-51; document “keep in sync” constant names in both places when server load lands. - **None** otherwise at kickoff. ## Decisions | Decision | Rationale | |----------|-----------| | `prototypeRole` enum + per-role CI | User requested agent recommendation; maps 1:1 to six-id vertical slice. | | Explicit `stackMax` + `inventorySlotKind` | User requested agent recommendation; backlog requires stack/slot metadata before server load. | | Optional `rarity` / `bindPolicy` / `durabilityMax` on schema | User chose **yes_stubs** for forward-compat without populating prototype rows. |