From 0ce952131adb507eeef025efa6c02ed15e736e28 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 17 May 2026 21:29:32 -0400 Subject: [PATCH] =?UTF-8?q?NEO-50:=20Address=20review=20=E2=80=94=20equip?= =?UTF-8?q?=5Fstub=20gate=20by=20role,=20backlog=20sync.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/E3M3-prototype-backlog.md | 10 ++++++---- docs/reviews/2026-05-17-NEO-50.md | 6 ++++-- scripts/validate_content.py | 16 ++++++++-------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/plans/E3M3-prototype-backlog.md b/docs/plans/E3M3-prototype-backlog.md index 8eb0b90..2146690 100644 --- a/docs/plans/E3M3-prototype-backlog.md +++ b/docs/plans/E3M3-prototype-backlog.md @@ -48,7 +48,7 @@ Working backlog for **Epic 3 — Slice 1** ([items and inventory MVP](../decompo - `content/schemas/item-def.schema.json` (or equivalent single-catalog schema). - `content/items/prototype_items.json` with stable ids: `scrap_metal_bulk`, `refined_plate_stock`, `field_stim_mk0`, `survey_drone_kit`, `contract_handoff_token`, `prototype_armor_shell` (names/display may change; **ids frozen**). -- `scripts/validate_content.py`: schema validation, duplicate `id`, required bucket/tag fields, exact six-id allowlist for prototype. +- `scripts/validate_content.py`: schema validation, duplicate `id`, required `prototypeRole` coverage (one row per archetype), exact six-id allowlist for prototype. - Designer note in [E3_M3](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) + `content/README.md`: stack rules, slot kinds (bag vs equip stub), v1 scope (no durability mutation). **Out of scope** @@ -57,9 +57,11 @@ Working backlog for **Epic 3 — Slice 1** ([items and inventory MVP](../decompo **Acceptance criteria** -- [ ] PR gate validates item JSON against schema. -- [ ] Exactly six prototype item ids; duplicate `id` fails CI. -- [ ] Stable id list documented in module doc freeze box. +- [x] PR gate validates item JSON against schema. +- [x] Exactly six prototype item ids; duplicate `id` fails CI. +- [x] Stable id list documented in module doc freeze box. + +**Landed ([NEO-50](https://linear.app/neon-sprawl/issue/NEO-50)):** [`content/items/prototype_items.json`](../../content/items/prototype_items.json), [`item-def.schema.json`](../../content/schemas/item-def.schema.json), CI gates in [`validate_content.py`](../../scripts/validate_content.py); plan [NEO-50-implementation-plan.md](NEO-50-implementation-plan.md). --- diff --git a/docs/reviews/2026-05-17-NEO-50.md b/docs/reviews/2026-05-17-NEO-50.md index 4904456..4c257f2 100644 --- a/docs/reviews/2026-05-17-NEO-50.md +++ b/docs/reviews/2026-05-17-NEO-50.md @@ -8,6 +8,8 @@ **Approve** +**Follow-up:** Review suggestions below are **done** (strikethrough + **Done.**). + ## Summary NEO-50 delivers a **content-only** Slice 1 spine: `item-def.schema.json`, six-row `prototype_items.json`, and extended `validate_content.py` with per-row schema validation, cross-file duplicate `id` detection, frozen six-id allowlist, one-row-per-`prototypeRole` coverage, and post-schema checks that `prototype_armor_shell` uses `equipment` + `stackMax: 1` when it carries `equip_stub`. Documentation (E3.M3 freeze table, `content/README.md`, CT.M1 CI paragraph, alignment register, implementation plan) matches kickoff decisions (`prototypeRole`, explicit `stackMax` / `inventorySlotKind`, optional forward-compat stubs). No server/C#/client surface — risk is low; PR gate already runs the script. One **should fix** tightens the equip-stub slot gate so a role/id swap cannot bypass equipment rules; backlog wording still mentions bucket/tags. @@ -30,8 +32,8 @@ None. ## Suggestions -1. **Equip-stub slot gate by role, not id** — `_prototype_slice1_item_gate` only enforces `inventorySlotKind: equipment` and `stackMax: 1` when `prototype_armor_shell` has `prototypeRole: equip_stub`. Swapping `prototypeRole` between frozen ids (e.g. `survey_drone_kit` → `equip_stub` with `bag`) would pass CI while violating the designer contract. Resolve the row where `id_to_role[*] == "equip_stub"` (there is exactly one) and assert slot/stack on that id. -2. **Backlog doc sync** — Update [`E3M3-prototype-backlog.md`](../plans/E3M3-prototype-backlog.md) E3M3-01 bullet from “bucket/tag fields” to `prototypeRole` (+ optional tick acceptance criteria) so backlog matches the implementation plan and shipped validator. +1. ~~**Equip-stub slot gate by role, not id** — `_prototype_slice1_item_gate` only enforces `inventorySlotKind: equipment` and `stackMax: 1` when `prototype_armor_shell` has `prototypeRole: equip_stub`. Swapping `prototypeRole` between frozen ids (e.g. `survey_drone_kit` → `equip_stub` with `bag`) would pass CI while violating the designer contract. Resolve the row where `id_to_role[*] == "equip_stub"` (there is exactly one) and assert slot/stack on that id.~~ **Done.** Gate now resolves the row with `prototypeRole == "equip_stub"` and asserts `equipment` + `stackMax: 1` on that id (`scripts/validate_content.py`). +2. ~~**Backlog doc sync** — Update [`E3M3-prototype-backlog.md`](../plans/E3M3-prototype-backlog.md) E3M3-01 bullet from “bucket/tag fields” to `prototypeRole` (+ optional tick acceptance criteria) so backlog matches the implementation plan and shipped validator.~~ **Done.** E3M3-01 in-scope text, ticked acceptance criteria, and NEO-50 landed note. ## Nits diff --git a/scripts/validate_content.py b/scripts/validate_content.py index 4b8de83..6ab35f9 100644 --- a/scripts/validate_content.py +++ b/scripts/validate_content.py @@ -284,17 +284,17 @@ def _prototype_slice1_item_gate( ) if len(id_to_role) != len(roles): return "error: prototype Slice 1 duplicate prototypeRole across item rows" - armor_id = "prototype_armor_shell" - if id_to_role.get(armor_id) == "equip_stub": - if id_to_slot_kind.get(armor_id) != "equipment": + equip_stub_id = next((iid for iid, role in id_to_role.items() if role == "equip_stub"), None) + if equip_stub_id is not None: + if id_to_slot_kind.get(equip_stub_id) != "equipment": return ( - f"error: {armor_id!r} (equip_stub) must use inventorySlotKind 'equipment', " - f"got {id_to_slot_kind.get(armor_id)!r}" + f"error: {equip_stub_id!r} (equip_stub) must use inventorySlotKind 'equipment', " + f"got {id_to_slot_kind.get(equip_stub_id)!r}" ) - if id_to_stack_max.get(armor_id) != 1: + if id_to_stack_max.get(equip_stub_id) != 1: return ( - f"error: {armor_id!r} (equip_stub) must use stackMax 1, " - f"got {id_to_stack_max.get(armor_id)!r}" + f"error: {equip_stub_id!r} (equip_stub) must use stackMax 1, " + f"got {id_to_stack_max.get(equip_stub_id)!r}" ) return None