MakerFLOSS/notes/dev/plans/2026-06-24-rack-shelves.md

677 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Shelf-Mounted Devices Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Model cabinet/tower PCs that sit on a rack shelf — the shelf reserves a U-range, occupants attach via `mounted_on`/`shelf_face`/`shelf_slot` — and render them as side-by-side boxes in the elevation.
**Architecture:** Extend `scripts/gen_rack.py`: a `mounted_on` branch in `validate_item`, a skip in `check_overlaps`, a new `check_shelves` cross-item validator, and shelf rendering in `render_svg` (a shelf strip plus per-occupant boxes subdividing the column) and `render_page` (occupancy rows for mounted devices). Then populate the worked example. No generator-config or CI changes.
**Tech Stack:** Python 3 (stdlib + PyYAML only), pytest, MkDocs Material, Forgejo Actions CI.
**Spec:** `notes/dev/specs/2026-06-24-rack-shelves-design.md`.
## Global Constraints
- Scripts use **stdlib + PyYAML only**; deterministic and offline (copy existing `gen_rack.py` style). No randomness/time in generated output.
- `re` and `yaml` are already imported in `scripts/gen_rack.py`; do not add new imports.
- Validation failures raise `SchemaError`; `generate` prints `ERROR: …` to stderr and returns `1`, **writing nothing** on failure.
- A **mounted device** declares `mounted_on` (str), `shelf_face` ∈ {front, rear}, `shelf_slot` (int ≥ 1), and **omits** `rack_u`/`u_height`/`rack_face`. A **shelf** (`kind: shelf`) is placed normally (`rack_u`/`u_height`/`rack_face: both`) and reserves the assembly's U-range.
- Peer/PDU/grouping fields (`power:`, `links:`, `cluster:`) on a mounted device are unchanged — they key off hostname, not placement.
- Reuse the existing `item()` and `_write_item` test helpers in `tests/test_gen_rack.py`; add a local `shelf()` helper where noted.
- `isinstance(x, int)` style (bool-is-int acceptable, matching existing code).
- Provisional placeholder data only (matches the existing `srvNN` positions and power/network demos).
- **No edits** to `mkdocs.yml`, `Makefile`, `.forgejo/workflows/docs.yml`, or `scripts/overview_config.yml` (`shelf`/`server` already in the enum; drift already covers `racks/`).
- `mkdocs build --strict` must pass; `make docs-check` must exit 0 after regeneration.
---
### Task 1: Validation — mounted branch, overlap skip, `check_shelves` (TDD)
**Files:**
- Modify: `scripts/gen_rack.py` (add `SHELF_FACES`; `validate_item` mounted branch; `check_overlaps` skip; new `check_shelves`; call it in `generate`)
- Modify: `tests/test_gen_rack.py` (append tests + a `shelf()` helper)
**Interfaces:**
- Consumes: `SchemaError`, `item()`/`_write_item` helpers, `generate`.
- Produces:
- `SHELF_FACES = {"front", "rear"}` (constant)
- `check_shelves(items: list[dict]) -> None` — raises `SchemaError` on a bad mount.
- `validate_item` and `check_overlaps` gain mounted-item handling.
- [ ] **Step 1: Append a `shelf()` helper and failing tests to `tests/test_gen_rack.py`**
Add near the top, just after the existing `item()` helper:
```python
def shelf(**kw):
base = {"hostname": "shf01", "kind": "shelf", "status": "in-use",
"rack": "rack01", "rack_u": 37, "u_height": 10, "rack_face": "both"}
base.update(kw)
return base
```
Append these tests at the end of the file:
```python
def test_validate_accepts_mounted_item():
gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
shelf_face="front", shelf_slot=1))
def test_validate_rejects_mounted_with_rack_u():
with pytest.raises(gen_rack.SchemaError):
gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
shelf_face="front", shelf_slot=1, rack_u=5))
def test_validate_rejects_mounted_bad_shelf_face():
with pytest.raises(gen_rack.SchemaError):
gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
shelf_face="left", shelf_slot=1))
def test_validate_rejects_mounted_bad_slot():
with pytest.raises(gen_rack.SchemaError):
gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
shelf_face="front", shelf_slot=0))
def test_overlaps_skips_mounted_items():
items = [
item(hostname="a", mounted_on="shf01", shelf_face="front", shelf_slot=1),
item(hostname="b", mounted_on="shf01", shelf_face="front", shelf_slot=2),
]
gen_rack.check_overlaps(items) # no raise — mounted items claim no U-range
def test_check_shelves_accepts_valid_mount():
items = [shelf(),
item(hostname="srv01", mounted_on="shf01",
shelf_face="front", shelf_slot=1)]
gen_rack.check_shelves(items)
def test_check_shelves_rejects_missing_shelf():
items = [item(hostname="srv01", mounted_on="ghost",
shelf_face="front", shelf_slot=1)]
with pytest.raises(gen_rack.SchemaError):
gen_rack.check_shelves(items)
def test_check_shelves_rejects_non_shelf_target():
items = [
item(hostname="sw01", kind="switch", rack_u=10, u_height=1,
rack_face="front"),
item(hostname="srv01", mounted_on="sw01",
shelf_face="front", shelf_slot=1),
]
with pytest.raises(gen_rack.SchemaError):
gen_rack.check_shelves(items)
def test_check_shelves_rejects_duplicate_slot():
items = [shelf(),
item(hostname="srv01", mounted_on="shf01",
shelf_face="front", shelf_slot=1),
item(hostname="srv02", mounted_on="shf01",
shelf_face="front", shelf_slot=1)]
with pytest.raises(gen_rack.SchemaError):
gen_rack.check_shelves(items)
def test_generate_returns_1_on_dangling_mount(tmp_path):
hw = tmp_path / "hardware"
out = tmp_path / "out"
hw.mkdir()
_write_item(
hw, "srv01",
"---\nhostname: srv01\nkind: server\nstatus: in-use\n"
"rack: rack01\nmounted_on: ghost\nshelf_face: front\nshelf_slot: 1\n---\n",
)
rc = gen_rack.generate(hw, out)
assert rc == 1
assert not (out / "rack01.md").exists()
```
- [ ] **Step 2: Run to verify failure**
Run: `pytest tests/test_gen_rack.py -q`
Expected: FAIL — `AttributeError: module 'gen_rack' has no attribute 'check_shelves'` (and the mounted-validation tests fail because `validate_item` rejects the missing `rack_face`).
- [ ] **Step 3: Add the `SHELF_FACES` constant**
In `scripts/gen_rack.py`, just after the existing `ZERO_U_FACES = {"left", "right"}` line, add:
```python
SHELF_FACES = {"front", "rear"}
```
- [ ] **Step 4: Add the mounted branch to `validate_item`**
In `validate_item`, the function currently begins:
```python
def validate_item(fm: dict) -> None:
name = fm.get("hostname") or fm.get("_path", "?")
rack = fm.get("rack")
if not isinstance(rack, str) or not rack:
raise SchemaError(f"{name}: rack must be a non-empty string")
face = fm.get("rack_face")
```
Insert the mounted branch between the `rack` check and the `face = …` line, so it reads:
```python
def validate_item(fm: dict) -> None:
name = fm.get("hostname") or fm.get("_path", "?")
rack = fm.get("rack")
if not isinstance(rack, str) or not rack:
raise SchemaError(f"{name}: rack must be a non-empty string")
if "mounted_on" in fm:
mounted_on = fm.get("mounted_on")
if not isinstance(mounted_on, str) or not mounted_on:
raise SchemaError(f"{name}: mounted_on must be a non-empty string")
for forbidden in ("rack_u", "u_height", "rack_face"):
if forbidden in fm:
raise SchemaError(
f"{name}: mounted item must omit {forbidden}"
)
sface = fm.get("shelf_face")
if sface not in SHELF_FACES:
raise SchemaError(
f"{name}: shelf_face={sface!r} not in {sorted(SHELF_FACES)}"
)
slot = fm.get("shelf_slot")
if not isinstance(slot, int) or slot < 1:
raise SchemaError(f"{name}: shelf_slot must be an integer >= 1")
return
face = fm.get("rack_face")
```
(The rest of `validate_item` — the `face`/0U/U-range checks — is unchanged.)
- [ ] **Step 5: Skip mounted items in `check_overlaps`**
In `check_overlaps`, the loop body currently starts:
```python
for fm in items:
face = fm.get("rack_face")
if face in ZERO_U_FACES:
continue
```
Add a mounted skip as the first thing in the loop:
```python
for fm in items:
if "mounted_on" in fm:
continue
face = fm.get("rack_face")
if face in ZERO_U_FACES:
continue
```
- [ ] **Step 6: Add `check_shelves` after `check_overlaps`**
Add this function immediately after `check_overlaps` in `scripts/gen_rack.py`:
```python
def check_shelves(items: list[dict]) -> None:
"""Validate shelf-mounted devices within one rack.
Every mounted_on resolves to a placed kind:shelf item in the same rack;
no two devices share (shelf, face, slot).
"""
by_host = {fm.get("hostname"): fm for fm in items}
occupied: dict[tuple[str, str, int], str] = {}
for fm in items:
if "mounted_on" not in fm:
continue
name = fm.get("hostname", "?")
shelf_name = fm["mounted_on"]
target = by_host.get(shelf_name)
if target is None:
raise SchemaError(
f"{name}: mounted_on={shelf_name!r} is not in this rack"
)
if target.get("kind") != "shelf":
raise SchemaError(
f"{name}: mounted_on={shelf_name!r} is not a kind:shelf item"
)
if not isinstance(target.get("rack_u"), int) or not isinstance(
target.get("u_height"), int
):
raise SchemaError(
f"{name}: shelf {shelf_name!r} is not placed (needs rack_u/u_height)"
)
key = (shelf_name, fm["shelf_face"], fm["shelf_slot"])
if key in occupied:
raise SchemaError(
f"{shelf_name} {fm['shelf_face']} slot {fm['shelf_slot']}: "
f"{name} overlaps {occupied[key]}"
)
occupied[key] = name
```
- [ ] **Step 7: Wire `check_shelves` into `generate`**
In `generate`, the per-rack validation loop currently reads:
```python
try:
check_overlaps(ritems)
validate_power(ritems)
validate_links(ritems, hw_index)
except SchemaError as e:
errors.append(f"{rack}: {e}")
```
Add `check_shelves(ritems)`:
```python
try:
check_overlaps(ritems)
validate_power(ritems)
validate_links(ritems, hw_index)
check_shelves(ritems)
except SchemaError as e:
errors.append(f"{rack}: {e}")
```
- [ ] **Step 8: Run to verify pass**
Run: `pytest tests/test_gen_rack.py -q`
Expected: PASS (all prior tests + 10 new).
- [ ] **Step 9: Commit**
```bash
git add scripts/gen_rack.py tests/test_gen_rack.py
git commit -m "feat(rack): validate shelf-mounted devices (mounted_on/shelf_face/shelf_slot)"
```
---
### Task 2: Rendering — shelf strip + occupant boxes + occupancy rows (TDD)
**Files:**
- Modify: `scripts/gen_rack.py` (`render_svg` shelf drawing; `render_page` occupancy)
- Modify: `tests/test_gen_rack.py` (append tests)
**Interfaces:**
- Consumes: `render_svg`, `render_page`, the `shelf()` helper (Task 1).
- Produces: `render_svg`/`render_page` render shelves and mounted occupants. No new public function.
- [ ] **Step 1: Append failing tests to `tests/test_gen_rack.py`**
```python
def test_render_svg_draws_shelf_and_occupants():
items = [
shelf(),
item(hostname="srv01", mounted_on="shf01", shelf_face="front", shelf_slot=1),
item(hostname="srv02", mounted_on="shf01", shelf_face="front", shelf_slot=2),
item(hostname="srv03", mounted_on="shf01", shelf_face="rear", shelf_slot=1),
]
svg = gen_rack.render_svg("rack01", items)
assert "shf01" in svg
assert "srv01" in svg and "srv02" in svg and "srv03" in svg
# the shelf is NOT drawn as a generic full-height device box
assert "shf01 (U37" not in svg
def test_render_svg_shelf_is_deterministic():
base = [
shelf(),
item(hostname="srv02", mounted_on="shf01", shelf_face="front", shelf_slot=2),
item(hostname="srv01", mounted_on="shf01", shelf_face="front", shelf_slot=1),
]
assert gen_rack.render_svg("rack01", base) == gen_rack.render_svg(
"rack01", list(reversed(base))
)
def test_render_page_lists_mounted_devices():
items = [shelf(),
item(hostname="srv01", mounted_on="shf01",
shelf_face="front", shelf_slot=1)]
page = gen_rack.render_page("rack01", items)
assert "../../hardware/srv01.md" in page
assert "front · shf01/1" in page
assert "U37U46" in page # mounted device shows its shelf's U-range
```
- [ ] **Step 2: Run to verify failure**
Run: `pytest tests/test_gen_rack.py -q`
Expected: FAIL — `test_render_page_lists_mounted_devices` raises `KeyError: 'rack_u'` (occupancy loop has no mounted handling) and the SVG tests fail (`shf01`/occupant boxes not drawn; the shelf is drawn as a generic box so `"shf01 (U37"` is present).
- [ ] **Step 3: Render shelves in `render_svg`**
In `render_svg`, the generic device loop currently reads:
```python
for fm in items:
face = fm.get("rack_face")
if face in ("front", "both"):
draw_device(fm, front_x)
if face in ("rear", "both"):
draw_device(fm, rear_x)
```
Replace it with a version that skips shelves and mounted items:
```python
for fm in items:
if fm.get("kind") == "shelf" or "mounted_on" in fm:
continue
face = fm.get("rack_face")
if face in ("front", "both"):
draw_device(fm, front_x)
if face in ("rear", "both"):
draw_device(fm, rear_x)
```
Then, immediately before the final `p.append("</svg>")` line, add the shelf drawing:
```python
SHELF_STRIP_H = 6
shelves = [i for i in items if i.get("kind") == "shelf"]
mounted = [i for i in items if "mounted_on" in i]
def draw_shelf(fm: dict) -> None:
u = fm["rack_u"]
h = fm["u_height"]
y = u_y(u)
block_h = h * U_H
strip_y = y + block_h - SHELF_STRIP_H
avail_h = block_h - SHELF_STRIP_H
shelf_color = KIND_COLORS.get("shelf", DEFAULT_COLOR)
sname = fm.get("hostname", "?")
for col_x, sface in ((front_x, "front"), (rear_x, "rear")):
occ = sorted(
(m for m in mounted
if m.get("mounted_on") == sname
and m.get("shelf_face") == sface),
key=lambda m: (m.get("shelf_slot", 0), m.get("hostname", "")),
)
n = len(occ)
for idx, m in enumerate(occ):
sub_w = COL_W // n
bx = col_x + idx * sub_w
bw = (COL_W - idx * sub_w) if idx == n - 1 else sub_w
mcolor = KIND_COLORS.get(m.get("kind", ""), DEFAULT_COLOR)
mname = m.get("hostname", "?")
p.append(
f'<rect x="{bx + 1}" y="{y + 1}" width="{bw - 2}" '
f'height="{avail_h - 2}" rx="3" fill="{mcolor}" stroke="#333"/>'
)
p.append(
f'<text x="{bx + bw // 2}" y="{y + avail_h // 2 + 4}" '
f'text-anchor="middle" fill="#ffffff">{_esc(mname)}</text>'
)
p.append(
f'<rect x="{col_x}" y="{strip_y}" width="{COL_W}" '
f'height="{SHELF_STRIP_H}" fill="{shelf_color}" stroke="#333"/>'
)
p.append(
f'<text x="{front_x + COL_W // 2}" y="{strip_y + SHELF_STRIP_H - 1}" '
f'text-anchor="middle" fill="#333" font-size="9">{_esc(sname)}</text>'
)
for fm in sorted(shelves, key=lambda s: s.get("hostname", "")):
draw_shelf(fm)
```
- [ ] **Step 4: Render mounted devices in the `render_page` occupancy table**
In `render_page`, the occupancy loop currently reads:
```python
for fm in items:
name = fm.get("hostname", "?")
link = f"[{name}](../../hardware/{name}.md)"
face = fm.get("rack_face", "")
if face in ZERO_U_FACES:
urange = "0U"
else:
u = fm["rack_u"]
h = fm["u_height"]
urange = f"U{u}" if h == 1 else f"U{u}U{u + h - 1}"
lines.append(
f"| {urange} | {link} | {fm.get('kind', '')} | {face} "
f"| {fm.get('status', '')} |"
)
```
Replace that whole loop with one that orders mounted devices right after their
shelf and renders their shelf-relative position:
```python
by_host = {fm.get("hostname"): fm for fm in items}
mounted_by_shelf: dict[str, list[dict]] = {}
for fm in items:
if "mounted_on" in fm:
mounted_by_shelf.setdefault(fm["mounted_on"], []).append(fm)
def occ_row(fm: dict) -> str:
name = fm.get("hostname", "?")
link = f"[{name}](../../hardware/{name}.md)"
if "mounted_on" in fm:
target = by_host.get(fm["mounted_on"])
if target and isinstance(target.get("rack_u"), int):
su = target["rack_u"]
sh = target["u_height"]
urange = f"U{su}" if sh == 1 else f"U{su}U{su + sh - 1}"
else:
urange = "—"
face = (
f"{fm.get('shelf_face', '')} · "
f"{fm['mounted_on']}/{fm.get('shelf_slot', '')}"
)
else:
face = fm.get("rack_face", "")
if face in ZERO_U_FACES:
urange = "0U"
else:
u = fm["rack_u"]
h = fm["u_height"]
urange = f"U{u}" if h == 1 else f"U{u}U{u + h - 1}"
return (
f"| {urange} | {link} | {fm.get('kind', '')} | {face} "
f"| {fm.get('status', '')} |"
)
for fm in _sorted_items([i for i in items if "mounted_on" not in i]):
lines.append(occ_row(fm))
if fm.get("kind") == "shelf":
occ = sorted(
mounted_by_shelf.get(fm.get("hostname"), []),
key=lambda m: (m.get("shelf_face", ""), m.get("shelf_slot", 0)),
)
for m in occ:
lines.append(occ_row(m))
```
(The `items = _sorted_items(items)` line at the top of `render_page` and the
graph sections above are unchanged.)
- [ ] **Step 5: Run to verify pass**
Run: `pytest tests/test_gen_rack.py -q`
Expected: PASS (all prior tests + 3 new).
- [ ] **Step 6: Commit**
```bash
git add scripts/gen_rack.py tests/test_gen_rack.py
git commit -m "feat(rack): render shelf strip, occupant boxes, and mounted occupancy rows"
```
---
### Task 3: Populate the shelf demo, regenerate
**Files:**
- Create: `docs/hardware/shf01.md`
- Modify: `docs/hardware/srv01.md`, `srv02.md`, `srv03.md` (convert to mounted)
- Regenerate: `docs/hardware/index.md`, `docs/infrastructure/racks/rack01.md`, `docs/infrastructure/racks/rack01-elevation.svg`
**Interfaces:**
- Consumes: `make docs-index`, `make test`, `mkdocs build --strict`, `make docs-check`.
> **Operator note — provisional data.** The shelf placement and the front/rear/slot assignments are placeholders matching the worked example (TaPPaaS: srv01/srv02 front, srv03 rear, on a shelf reserving U37U46). `check_shelves`/`check_overlaps` reject inconsistent data loudly.
- [ ] **Step 1: Create the shelf file**
Create `docs/hardware/shf01.md`:
```markdown
---
hostname: shf01
kind: shelf
status: in-use
rack: rack01
rack_u: 37
u_height: 10
rack_face: both
cluster: tappaas
---
## Notes
- Provisional placeholder shelf holding the TaPPaaS nodes (srv01/srv02 front, srv03 rear).
```
- [ ] **Step 2: Convert `srv01.md` to mounted**
In `docs/hardware/srv01.md`, replace these three frontmatter lines:
```yaml
rack_u: 1
u_height: 1
rack_face: front
```
with:
```yaml
mounted_on: shf01
shelf_face: front
shelf_slot: 1
```
Leave everything else (including `cluster: tappaas`, `power:`, `links:`) unchanged.
- [ ] **Step 3: Convert `srv02.md` to mounted and tag its cluster**
In `docs/hardware/srv02.md`, replace these three frontmatter lines:
```yaml
rack_u: 2
u_height: 1
rack_face: front
```
with:
```yaml
mounted_on: shf01
shelf_face: front
shelf_slot: 2
```
Then add a `cluster: tappaas` line immediately after the `status: staging` line, so the top reads:
```yaml
hostname: srv02
kind: server
status: staging
cluster: tappaas
location: The pile
```
- [ ] **Step 4: Convert `srv03.md` to mounted and tag its cluster**
In `docs/hardware/srv03.md`, replace these three frontmatter lines:
```yaml
rack_u: 3
u_height: 1
rack_face: front
```
with:
```yaml
mounted_on: shf01
shelf_face: rear
shelf_slot: 1
```
Then add a `cluster: tappaas` line immediately after the `status: staging` line:
```yaml
hostname: srv03
kind: server
status: staging
cluster: tappaas
location: The pile
```
- [ ] **Step 5: Regenerate all indices and rack artifacts**
Run: `make docs-index`
Expected: `gen_overview.py` rewrites `docs/hardware/index.md` (now listing `shf01` under Shelves); `gen_rack.py` prints `Wrote rack01.md + rack01-elevation.svg (10 item(s))`, exit 0 (no schema error).
- [ ] **Step 6: Confirm the shelf and mounted devices rendered**
Run: `grep -c "shf01/" docs/infrastructure/racks/rack01.md`
Expected: `3` (occupancy notes `front · shf01/1`, `front · shf01/2`, `rear · shf01/1`).
Run: `grep -q "shf01" docs/infrastructure/racks/rack01-elevation.svg && echo OK`
Expected: `OK` (shelf strip drawn).
Run: `grep -q "U37U46" docs/infrastructure/racks/rack01.md && echo OK`
Expected: `OK` (mounted devices show the shelf's U-range).
- [ ] **Step 7: Run the full test suite**
Run: `make test`
Expected: PASS.
- [ ] **Step 8: Build the site strictly**
Run: `mkdocs build --strict` (or `python3 -m mkdocs build --strict` if `mkdocs` is not on PATH)
Expected: build succeeds with no warnings-as-errors.
- [ ] **Step 9: Confirm the drift guard is satisfied**
Run: `make docs-check`
Expected: exit 0.
- [ ] **Step 10: Commit**
```bash
git add docs/hardware/ docs/infrastructure/racks/
git commit -m "feat(rack): place TaPPaaS nodes on shelf shf01 (provisional)"
```
---
## Self-Review
**Spec coverage (`2026-06-24-rack-shelves-design.md`):**
- Container model: shelf placed + reserves U-range; mounted devices via `mounted_on`/`shelf_face`/`shelf_slot` — Task 1 (validation), Task 3 (data). ✔
- `validate_item` mounted branch (forbid rack_u/u_height/rack_face; require shelf_face/shelf_slot) — Task 1 Step 4. ✔
- `check_overlaps` skips mounted items — Task 1 Step 5. ✔
- `check_shelves` (resolves to placed kind:shelf in rack; unique slot) wired into `generate` — Task 1 Steps 67. ✔
- SVG shelf strip + subdivided occupant boxes — Task 2 Step 3. ✔
- Occupancy rows for mounted devices (shelf U-range + `face · shelf/slot`, ordered after the shelf) — Task 2 Step 4. ✔
- Power/network unchanged — no edits to `render_power`/`render_network`; mounted devices keep `power:`/`links:`. ✔
- Demo data (shf01 U37U46; srv01/srv02 front 12, srv03 rear 1; cluster tappaas) — Task 3. ✔
- No mkdocs/Makefile/CI/overview_config changes — Global Constraints. ✔
**Placeholder scan:** No "TBD"/"handle edge cases"/"similar to Task N". The provisional demo placements are real-data-dependent and explicitly bounded.
**Type consistency:** `SHELF_FACES: set`; `check_shelves(items) -> None` (raises `SchemaError`), called per-rack alongside `check_overlaps`/`validate_power`/`validate_links`; `render_svg`/`render_page` -> `str`. Mounted items are identified uniformly by `"mounted_on" in fm` across `validate_item`, `check_overlaps`, `check_shelves`, `render_svg`, and `render_page`. The `shelf()` test helper (Task 1) is reused in Task 2. Field names (`mounted_on`, `shelf_face`, `shelf_slot`) match across tasks, tests, and demo data.