diff --git a/docs/plans/NEO-50-implementation-plan.md b/docs/plans/NEO-50-implementation-plan.md new file mode 100644 index 0000000..75b4941 --- /dev/null +++ b/docs/plans/NEO-50-implementation-plan.md @@ -0,0 +1,109 @@ +# 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 + +- [ ] PR gate / `validate_content.py` fails on invalid rows, schema mismatch, or duplicate item `id`. +- [ ] Exactly **six** prototype item ids; wrong id set fails CI. +- [ ] Each `prototypeRole` appears **exactly once** across the catalog. +- [ ] 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. |