# 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. |
