diff --git a/notes/dev/plans/2026-06-24-rack-shelves.md b/notes/dev/plans/2026-06-24-rack-shelves.md
new file mode 100644
index 0000000..c751342
--- /dev/null
+++ b/notes/dev/plans/2026-06-24-rack-shelves.md
@@ -0,0 +1,677 @@
+# 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 "U37–U46" 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("")` 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''
+ )
+ p.append(
+ f'{_esc(mname)}'
+ )
+ p.append(
+ f''
+ )
+ p.append(
+ f'{_esc(sname)}'
+ )
+
+ 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 U37–U46). `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 "U37–U46" 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 6–7. ✔
+- 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 U37–U46; srv01/srv02 front 1–2, 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.