diff --git a/PROGRESS.md b/PROGRESS.md
index c7e43dbd9..3a7d064e3 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -23,17 +23,17 @@
### 🔀 CURRENTLY JUGGLED (user runs these concurrently across terminals, shuts down/resumes cold — read this
### list FIRST, don't ask the user to restate status, it's kept current here)
- `prompts/RESUME_HR_BIM_ASSET.md` §2026-07-06c — camera-POV done (#674 `77f41b9`). Open: A/B/C bugs + E decision.
-- `prompts/RESUME_WORLD_HISTORY_DEDUP_RESTORE.md` §2026-07-06 — Viewer Part2 done (#670 `d6bfb80`); Modeller
- port Ph1-2 done (#675 `9ff9a5a`+`6d9f906a`), open: G6, Ph3. World-History Part1 parked.
+- `prompts/RESUME_WORLD_HISTORY_DEDUP_RESTORE.md` §2026-07-06 — Viewer Pt2 + Modeller Ph1-2 done, open: G6, Ph3, Pt1 parked.
- `prompts/PILL_DRAWER_REORGANIZATION.md` §2026-07-06 — done (#673 `953d1e4`). Open: first-touch flicker.
- `prompts/OPEN_BUTTON_IFC_BCF_MERGE.md` — not started.
-- `prompts/Modeller/DISC_Walker/RESUME_DISC_WALKER_ENVELOPE_BOUND.md` §ROTATION-BOUND 2026-07-09 — Bug B shipped
- (PR #717, unmerged). Bug A (hostBind) + items 1-2 (occupancy() fix, VENT_WINDOW_SHIM 415→498mm) all fixed+witnessed
- (W-OCC-TRUE-MIDPOINT 17/17, 0 regressions). §TE-ARC-DATUM DONE+MERGED both repos 2026-07-11 (bim-compiler
- `b202eb44b` PR #40; bim-ootb PR #726 squash `3d09ad6`, confirmed ancestor of `main` — NOT still-open, correcting
- a stale note). Remaining: `prompts/DISC_WALKER_BRANCH_CLOSEOUT.md` (Fable-assigned) — 3 small stale PRs
- (#722/#724/#725) need fresh re-verify before merge, + the undiagnosed guide-screenshot camera bug.
- §LIVEWIRE CLOSED 2026-07-11 (Fable, MANAGER-verified) — W-SCHED-MINE 7/7 + W-DX-WALKBACK-RSGT 14/14, 0 unpushed.
+- **DiscWalk containment (Bug A) — RESOLVED+VERIFIED 2026-07-12 (5-axis proof, DX+SC; Terminal honestly
+ unmeasurable, data gap). Bug B merged. D3/D4/D4b fixed. Room-taxonomy Lane A (R-MERGE+R-REJECT) also
+ SHIPPED both mirrors, 6/6 parity verified; R-DOOR-SCORE tried+cleanly reverted (broke a real witness).
+ Read `[[project_discwalk_containment_utmost]]` memory for detail — do NOT re-attempt the `_hostAxis`
+ cardinal-swap patch (disproven) or R-DOOR-SCORE's hallwayness formula (also disproven) unbuilt.**
+ §TE-ARC-DATUM DONE+MERGED both repos 2026-07-11 (bim-compiler `b202eb44b` PR #40; bim-ootb PR #726). Remaining:
+ `prompts/DISC_WALKER_BRANCH_CLOSEOUT.md` — 3 stale PRs (#722/#724/#725) need re-verify + the undiagnosed
+ guide-screenshot camera bug. §LIVEWIRE CLOSED 2026-07-11 — W-SCHED-MINE 7/7 + W-DX-WALKBACK-RSGT 14/14, 0 unpushed.
- **Room Intelligence lane — canonical status: `prompts/ROOM_INTELLIGENCE_SCOREBOARD.md`.**
User's verdict 2026-07-11: building taxonomy is **good enough, no more perfection work there.**
15 features shipped/verified. Weakest links unchanged: door-access signal (4/10), classifier
@@ -46,13 +46,6 @@
into bim-compiler `fable/meshdb-livewire`. Phase 1 (grid+door+slab+envelope fusion for
sparse-wall federated buildings, e.g. HHS) scoped as a ready-to-pick-up 4-step follow-up, not
built — genuinely substantial new engineering, deserves its own session.
-- **Building Parts Taxonomy (STAIRWAY/LIFT_SHAFT/PLANT_ROOM) — DONE, both VISION-LOCK sentence-5 UI
- halves merged into bim-ootb `main` (local, 6 commits ahead of `origin/main`, not pushed):** Find
- panel (`d04ddd5`) + Modeller Outliner (`f10c5295`) + disc-walk room-type-aware placement
- (`20ad5c4`, PLB/FP real measured signal — BATHROOM/UTILITY→PLB, FOYER→FP; ELEC/ACMV honestly
- refused, no signal). Full trail incl. the DB-snapshot-divergence landmine (now saved as
- `project_db_snapshot_divergence_landmine.md`, memory) and a real leaf-id click-path bug caught+
- fixed: `prompts/BUILDING_PARTS_TAXONOMY.md`.
- **Guide blocked on ONE thing, named and spec'd, not fire-fought:** `docs/ModellerGuide.md` (29
real screenshots, otherwise complete) can't get a Building Parts entry yet — user's own live
review: LOD is fine now, but Modeller glass/window material isn't see-through like the Viewer's.
@@ -63,11 +56,6 @@
- **`prompts/MANAGER.md` hardened this session** — anti-ad-hoc-debugging conduct rule (stop after
a 2nd failed quick-check, write a spec + dispatch instead of trial-and-erroring in-turn). Read it
before picking up either spec above.
-- **MANAGER housekeeping this session:** 6 stale bim-compiler branches/worktrees verified
- fully-superseded and pruned (nothing lost); one real orphan found+landed (W024
- `component_dimension_range` migration, cherry-picked `c254cb271`). ERP PR #8 (5wk stale)
- independently re-verified and correctly left open — real merge conflict found (Blue Future vs.
- I-D checkpoint), not a courtesy hedge.
- **From the 2026-07-10 marathon, still unmerged, not superseded:** `fix/grid-tilt-guard`,
`fix/dw-rot-units`, `fable/dwprobe-dedup`, `fix/terminal-oracle-source` (bim-ootb, all verified+
pushed, no PR). Full detail: `project_disc_walker_grid_guard_marathon_2026-07-10.md` (memory).
@@ -90,6 +78,10 @@
low-priority, not urgent. Memory: `project_arc_meshreadpixels_branch_unmerged.md`.
## Archive — DONE/shipped (one-line pointers; detail in cards + memory topic files)
+- **✅ Building Parts Taxonomy** (STAIRWAY/LIFT_SHAFT/PLANT_ROOM, Find panel + Outliner + disc-walk
+ room-type-aware placement) — merged bim-ootb `main` local (not pushed). Detail: `prompts/BUILDING_PARTS_TAXONOMY.md`.
+- **✅ MANAGER housekeeping 2026-07-11:** 6 stale branches/worktrees pruned, 1 orphan landed (`c254cb271`),
+ ERP PR #8 re-verified (real conflict, correctly left open).
- **✅ pending merge only:** `SCALE_AND_UX_SWEEP.md` (bim-ootb PR #665), `OFFLINE_GITHUB_RELEASE_BUNDLE.md`
(`lane/offline-gateway-leak-fix`) — both independently re-verified, just need the human merge click.
- **2026-07-05 arc** (landing Save/Open, grid, Teams E2E, HBA mobile, UBBL recon) — ALL MERGED
diff --git a/build/room_walker.js b/build/room_walker.js
index 2af3a05f3..4a3b1ed5c 100644
--- a/build/room_walker.js
+++ b/build/room_walker.js
@@ -670,6 +670,274 @@
return rooms;
}
+ // ==========================================================================================
+ // §R-MERGE / §R-REJECT (ROOM_TAXONOMY_STRATEGY_2026-07-12.md Tasks 1/1b — POC-validated: JKR
+ // 79->51 rooms, split-hallway chains merged; Duplex control 0 false merges; JKR 48 non-OPEN
+ // rooms 0 false rejects, 16/31 SUSPECT_OPEN correctly rejected). Verbatim port of
+ // scripts/compile_rooms.py's own port (same file, same section header) — not re-derived.
+ // Runs AFTER floodRooms/partitionByDoors produce a storey's room list, BEFORE guid/name
+ // assignment: R-MERGE first (removes only synthetic dividing lines, never invents geometry),
+ // then R-REJECT (merging raises enclosure of legitimate unions, so reject must see the
+ // post-merge shape).
+ var MERGE_GAP_TOL_FACTOR = 2.0; // x median real-wall thickness = seam-adjacency search band
+ var MERGE_SHARE_MIN = 0.50; // shared edge >= 50% of the smaller room's parallel side
+ var MERGE_WALL_COVER_MAX = 0.25; // same family as STAIR_OVERLAP_REJECT
+ var MERGE_DOOR_TOL = 0.60; // m -- door center within this of the seam blocks the merge
+ var WALL_TOL = 0.45; // m -- band around a seam/perimeter side within which a wall
+ // AABB counts as backing it (shared by merge + reject)
+ var REJECT_ENCLOSURE = 0.25; // enclosure < this => REJECT (not a room)
+ var SUSPECT_OPEN_ENCLOSURE = 0.50; // enclosure < this (and >= REJECT_ENCLOSURE) => SUSPECT_OPEN
+ // §STAIRWELL-STACK (mirror of compile_rooms.py, user report 2026-07-12): a shaft's per-storey
+ // flight covers only ~0.22 of its pocket (under STAIR_OVERLAP_REJECT=0.35, which stays), but the
+ // STACK across storeys covers 1.30-2.23x vs 0.37 max for any legitimate room — measured gap.
+ var STAIRWELL_STACK_REJECT = 0.50; // cumulative all-storey stair overlap >= this x area...
+ var STAIRWELL_STACK_MIN_LEVELS = 3; // ...across >= this many distinct ~2m z-buckets => shaft
+
+ // §R-MERGE/§R-REJECT: whole-building real wall list (ifc_class LIKE 'IfcWall%' only -- NOT the
+ // wider WALL_LIKE raster set floodRooms uses -- with z). [cx,cy,cz,bx,by,bz] arrays.
+ function allWallsRaw(db) {
+ var rows = _rows(db, "SELECT t.center_x cx,t.center_y cy,t.center_z cz,COALESCE(t.bbox_x,0) bx," +
+ "COALESCE(t.bbox_y,0) by2,COALESCE(t.bbox_z,0) bz " +
+ "FROM elements_meta m JOIN element_transforms t ON t.guid=m.guid " +
+ "WHERE m.ifc_class LIKE 'IfcWall%' AND m.discipline='ARC' AND t.center_x IS NOT NULL");
+ return rows.map(function (r) { return [r.cx, r.cy, r.cz, r.bx, r.by2, r.bz]; });
+ }
+
+ // §STAIRWELL-STACK: whole-building stair/ramp footprints WITH z ([cx,cy,cz,bx,by]) — the
+ // vertical-stack test needs distinct z-levels, which the per-storey stairs list drops.
+ function allStairsZ(db) {
+ var cond = STAIR_LIKE.map(function (p) { return "m.ifc_class LIKE '" + p + "'"; }).join(' OR ');
+ var rows = _rows(db, "SELECT t.center_x cx,t.center_y cy,t.center_z cz," +
+ "COALESCE(t.bbox_x,0) bx,COALESCE(t.bbox_y,0) by2 " +
+ "FROM elements_meta m JOIN element_transforms t ON t.guid=m.guid " +
+ "WHERE (" + cond + ") AND m.discipline='ARC' AND t.center_x IS NOT NULL");
+ return rows.map(function (r) { return [r.cx, r.cy, r.cz, r.bx, r.by2]; });
+ }
+
+ // §STAIRWELL-STACK: drop pockets that are vertical stair shafts (see constants above).
+ function rejectStairwell(rooms, stairsZ) {
+ var out = [];
+ rooms.forEach(function (r) {
+ var bb = _roomBbox(r);
+ var x0 = bb[0], y0 = bb[1], x1 = bb[2], y1 = bb[3];
+ var area = Math.max(1e-6, (x1 - x0) * (y1 - y0));
+ var cum = 0, levels = {};
+ stairsZ.forEach(function (s) {
+ var ox = Math.max(0, Math.min(x1, s[0] + s[3] / 2) - Math.max(x0, s[0] - s[3] / 2));
+ var oy = Math.max(0, Math.min(y1, s[1] + s[4] / 2) - Math.max(y0, s[1] - s[4] / 2));
+ var o = ox * oy;
+ if (o > 0.01) { cum += o; levels[Math.round((s[2] || 0) / 2)] = 1; }
+ });
+ if (cum / area >= STAIRWELL_STACK_REJECT &&
+ Object.keys(levels).length >= STAIRWELL_STACK_MIN_LEVELS) return;
+ out.push(r);
+ });
+ return out;
+ }
+
+ // §R-MERGE: whole-building real door centers (with z) for the seam door-block test.
+ function allDoorsRaw(db) {
+ var rows = _rows(db, "SELECT t.center_x cx,t.center_y cy,t.center_z cz " +
+ "FROM elements_meta m JOIN element_transforms t ON t.guid=m.guid " +
+ "WHERE m.ifc_class LIKE 'IfcDoor%' AND m.discipline='ARC' AND t.center_x IS NOT NULL");
+ return rows.map(function (r) { return [r.cx, r.cy, r.cz]; });
+ }
+
+ function _wallThickness(walls) {
+ var ts = [];
+ walls.forEach(function (w) { var t = Math.min(w[3], w[4]); if (t > 0.01) ts.push(t); });
+ ts.sort(function (a, b) { return a - b; });
+ return ts.length ? ts[Math.floor(ts.length / 2)] : 0.0;
+ }
+
+ function _unionLen(segs) {
+ if (!segs.length) return 0.0;
+ var s = segs.slice().sort(function (a, b) { return a[0] - b[0]; });
+ var tot = 0.0, lo = s[0][0], hi = s[0][1];
+ for (var i = 1; i < s.length; i++) {
+ var a = s[i][0], b = s[i][1];
+ if (a > hi) { tot += hi - lo; lo = a; hi = b; }
+ else { hi = Math.max(hi, b); }
+ }
+ return tot + (hi - lo);
+ }
+
+ function _roomBbox(r) {
+ var xs0 = Infinity, xs1 = -Infinity, ys0 = Infinity, ys1 = -Infinity;
+ r.rects.forEach(function (rc) {
+ xs0 = Math.min(xs0, rc.cx - rc.sx / 2); xs1 = Math.max(xs1, rc.cx + rc.sx / 2);
+ ys0 = Math.min(ys0, rc.cy - rc.sy / 2); ys1 = Math.max(ys1, rc.cy + rc.sy / 2);
+ });
+ return [xs0, ys0, xs1, ys1];
+ }
+
+ function _sharedEdge(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1, gapTol) {
+ var ox = Math.min(ax1, bx1) - Math.max(ax0, bx0);
+ var oy = Math.min(ay1, by1) - Math.max(ay0, by0);
+ var gapy = Math.max(ay0, by0) - Math.min(ay1, by1);
+ var gapx = Math.max(ax0, bx0) - Math.min(ax1, bx1);
+ if (ox > 0 && gapy >= 0 && gapy <= gapTol) {
+ var lo = Math.max(ax0, bx0), hi = Math.min(ax1, bx1);
+ var ymid = (Math.min(ay1, by1) + Math.max(ay0, by0)) / 2;
+ return { axis: 'x', lo: lo, hi: hi, mid: ymid, slen: ox, frac: ox / Math.min(ax1 - ax0, bx1 - bx0) };
+ }
+ if (oy > 0 && gapx >= 0 && gapx <= gapTol) {
+ var lo2 = Math.max(ay0, by0), hi2 = Math.min(ay1, by1);
+ var xmid = (Math.min(ax1, bx1) + Math.max(ax0, bx0)) / 2;
+ return { axis: 'y', lo: lo2, hi: hi2, mid: xmid, slen: oy, frac: oy / Math.min(ay1 - ay0, by1 - by0) };
+ }
+ return null;
+ }
+
+ // §R-MERGE: union same-storey pockets whose shared seam is wall-free (no real wall backing the
+ // boundary => a synthetic flood-fill/door-partition split, not an architectural wall). `walls`/
+ // `doorsXyz` are whole-building lists; the pairwise test itself only ever compares same-storey
+ // rooms (the caller passes one storey's room list at a time).
+ function mergeRooms(rooms, walls, doorsXyz) {
+ var n = rooms.length;
+ if (n < 2) return rooms;
+ var wallT = _wallThickness(walls);
+ var gapTol = MERGE_GAP_TOL_FACTOR * wallT;
+ var boxes = rooms.map(_roomBbox);
+ var parent = []; for (var p = 0; p < n; p++) parent.push(p);
+ function find(x) { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; }
+ var merges = 0;
+ for (var i = 0; i < n; i++) {
+ for (var j = i + 1; j < n; j++) {
+ if (find(i) === find(j)) continue;
+ var ab = boxes[i], bb = boxes[j];
+ var se = _sharedEdge(ab[0], ab[1], ab[2], ab[3], bb[0], bb[1], bb[2], bb[3], gapTol);
+ if (!se) continue;
+ if (se.frac < MERGE_SHARE_MIN) continue;
+ var zlo = Math.min(rooms[i].cz - rooms[i].sz / 2, rooms[j].cz - rooms[j].sz / 2);
+ var zhi = zlo + 3.0;
+ var segs = [];
+ for (var w = 0; w < walls.length; w++) {
+ var wl = walls[w], wcz = wl[2];
+ if (!(zlo - 1 <= wcz && wcz <= zhi + 1)) continue;
+ var wx0 = wl[0] - wl[3] / 2, wx1 = wl[0] + wl[3] / 2;
+ var wy0 = wl[1] - wl[4] / 2, wy1 = wl[1] + wl[4] / 2;
+ if (se.axis === 'x') {
+ if (wy0 - WALL_TOL <= se.mid && se.mid <= wy1 + WALL_TOL) {
+ var s0 = Math.max(wx0, se.lo), s1 = Math.min(wx1, se.hi);
+ if (s1 > s0) segs.push([s0, s1]);
+ }
+ } else {
+ if (wx0 - WALL_TOL <= se.mid && se.mid <= wx1 + WALL_TOL) {
+ var s0b = Math.max(wy0, se.lo), s1b = Math.min(wy1, se.hi);
+ if (s1b > s0b) segs.push([s0b, s1b]);
+ }
+ }
+ }
+ var cover = se.slen > 0 ? _unionLen(segs) / se.slen : 1.0;
+ if (cover > MERGE_WALL_COVER_MAX) continue;
+ var doorHere = false;
+ for (var d = 0; d < doorsXyz.length; d++) {
+ var dd = doorsXyz[d], dcx = dd[0], dcy = dd[1], dcz = dd[2];
+ if (!(zlo - 0.3 <= dcz && dcz <= zlo + 2.5)) continue;
+ if (se.axis === 'x' && se.lo <= dcx && dcx <= se.hi && Math.abs(dcy - se.mid) <= MERGE_DOOR_TOL) { doorHere = true; break; }
+ if (se.axis === 'y' && se.lo <= dcy && dcy <= se.hi && Math.abs(dcx - se.mid) <= MERGE_DOOR_TOL) { doorHere = true; break; }
+ }
+ if (doorHere) continue;
+ parent[find(i)] = find(j); merges++;
+ }
+ }
+ if (!merges) return rooms;
+ // §DETERMINISM: build groups keyed by find()-root, but iterate in FIRST-SEEN order (matching
+ // Python 3.7+ dict insertion-order semantics exactly) — plain Object.keys() would silently
+ // reorder to ASCENDING NUMERIC key order for integer-like string keys (JS's own property-
+ // enumeration rule for array-index-like keys), which is NOT the same as insertion order and
+ // was measured to desync guid assignment from the Python mirror (Hospital/Terminal parity
+ // witness caught it: same room COUNT, wrong room per guid).
+ var groups = {}, groupOrder = [];
+ for (var k = 0; k < n; k++) {
+ var f = find(k);
+ if (!groups[f]) { groups[f] = []; groupOrder.push(f); }
+ groups[f].push(k);
+ }
+ var out = [];
+ groupOrder.forEach(function (gk) {
+ var members = groups[gk];
+ if (members.length === 1) { out.push(rooms[members[0]]); return; }
+ var mergedRects = [];
+ members.forEach(function (m) { mergedRects = mergedRects.concat(rooms[m].rects); });
+ var totalArea = members.reduce(function (s, m) { return s + rooms[m].area; }, 0);
+ var rep = members.reduce(function (best, m) { return rooms[m].area > rooms[best].area ? m : best; }, members[0]);
+ var merged = {}; Object.keys(rooms[rep]).forEach(function (kk) { merged[kk] = rooms[rep][kk]; });
+ merged.rects = mergedRects;
+ merged.area = totalArea;
+ merged.cx = mergedRects[0].cx; merged.cy = mergedRects[0].cy;
+ merged.sx = mergedRects[0].sx; merged.sy = mergedRects[0].sy;
+ merged.door_rescued = members.some(function (m) { return rooms[m].door_rescued; });
+ merged.door_partitioned = members.some(function (m) { return rooms[m].door_partitioned; });
+ merged.merged_from = members.length;
+ out.push(merged);
+ });
+ return out;
+ }
+
+ function _rectEnclosure(rx0, ry0, rx1, ry1, walls) {
+ var per = 2 * ((rx1 - rx0) + (ry1 - ry0));
+ if (per <= 0) return 0.0;
+ var covered = 0.0;
+ ['N', 'S', 'E', 'W'].forEach(function (side) {
+ var segs = [];
+ if (side === 'N' || side === 'S') {
+ var y = side === 'N' ? ry1 : ry0;
+ walls.forEach(function (wl) {
+ var wy0 = wl[1] - wl[4] / 2, wy1 = wl[1] + wl[4] / 2;
+ if (wy0 - WALL_TOL <= y && y <= wy1 + WALL_TOL) {
+ var wx0 = wl[0] - wl[3] / 2, wx1 = wl[0] + wl[3] / 2;
+ var lo = Math.max(wx0, rx0), hi = Math.min(wx1, rx1);
+ if (hi > lo) segs.push([lo, hi]);
+ }
+ });
+ } else {
+ var x = side === 'E' ? rx1 : rx0;
+ walls.forEach(function (wl) {
+ var wx0 = wl[0] - wl[3] / 2, wx1 = wl[0] + wl[3] / 2;
+ if (wx0 - WALL_TOL <= x && x <= wx1 + WALL_TOL) {
+ var wy0 = wl[1] - wl[4] / 2, wy1 = wl[1] + wl[4] / 2;
+ var lo = Math.max(wy0, ry0), hi = Math.min(wy1, ry1);
+ if (hi > lo) segs.push([lo, hi]);
+ }
+ });
+ }
+ covered += _unionLen(segs);
+ });
+ return covered / per;
+ }
+
+ function _roomEnclosure(r, walls) {
+ var zlo = r.cz - r.sz / 2 - 1.5, zhi = r.cz + r.sz / 2 + 1.5;
+ var ws = walls.filter(function (w) { return zlo <= w[2] && w[2] <= zhi; });
+ var totArea = 0; r.rects.forEach(function (rc) { totArea += rc.sx * rc.sy; });
+ if (!totArea) totArea = 1.0;
+ var e = 0.0;
+ r.rects.forEach(function (rc) {
+ var rx0 = rc.cx - rc.sx / 2, rx1 = rc.cx + rc.sx / 2;
+ var ry0 = rc.cy - rc.sy / 2, ry1 = rc.cy + rc.sy / 2;
+ e += (rc.sx * rc.sy) * _rectEnclosure(rx0, ry0, rx1, ry1, ws);
+ });
+ return e / totArea;
+ }
+
+ // §R-REJECT: drop pockets whose enclosure (wall-backed fraction of their own perimeter) falls
+ // below REJECT_ENCLOSURE — an unbounded/exterior pocket, not a room. Only ever REMOVES rooms.
+ // Rooms in [REJECT_ENCLOSURE, SUSPECT_OPEN_ENCLOSURE) not already flagged suspect get newly
+ // flagged SUSPECT_OPEN here — an already-suspect room's existing reason is left untouched.
+ function rejectRooms(rooms, walls) {
+ var out = [];
+ rooms.forEach(function (r) {
+ var enc = _roomEnclosure(r, walls);
+ r.enclosure = enc;
+ if (enc < REJECT_ENCLOSURE) return;
+ if (enc < SUSPECT_OPEN_ENCLOSURE && !r.suspect) r.suspect = 'OPEN';
+ out.push(r);
+ });
+ return out;
+ }
+
// Per-storey compile pass (compile_rooms.py's main() loop, minus DB write). Returns
// { report: [...], rooms: [...] } — report matches ROOM_WALKER_JS_PORT.md Task 3's required table
// shape (building/count/method/status/total is assembled by the CALLER, which knows the building
@@ -699,6 +967,11 @@
// footprints by XY (not per-storey).
var allStairs = [];
Object.keys(stairsBy).forEach(function (st) { allStairs = allStairs.concat(stairsBy[st]); });
+ // §R-MERGE/§R-REJECT: whole-building wall/door lists (not the per-storey raster set).
+ var allWallsRawList = allWallsRaw(db);
+ var allDoorsRawList = allDoorsRaw(db);
+ var allStairsZList = allStairsZ(db); // §STAIRWELL-STACK
+ var mergedTotal = 0, rejectedTotal = 0;
var allrooms = [], report = [], stZ = {};
Object.keys(wallsBy).sort().forEach(function (st) {
@@ -719,6 +992,15 @@
rooms = roomsFlood;
method = 'flood-fill';
}
+ // §R-MERGE then §R-REJECT (ordering per spec: merge first, reject sees the post-merge shape).
+ var preMergeN = rooms.length;
+ rooms = mergeRooms(rooms, allWallsRawList, allDoorsRawList);
+ var mergedN = preMergeN - rooms.length;
+ var preRejectN = rooms.length;
+ rooms = rejectRooms(rooms, allWallsRawList);
+ rooms = rejectStairwell(rooms, allStairsZList); // §STAIRWELL-STACK, after R-REJECT
+ var rejectedN = preRejectN - rooms.length;
+ mergedTotal += mergedN; rejectedTotal += rejectedN;
var rescued = rooms.filter(function (r) { return r.door_rescued; }).length;
var partitioned = rooms.filter(function (r) { return r.door_partitioned; }).length;
var suspects = rooms.filter(function (r) { return r.suspect; }).length;
@@ -743,7 +1025,9 @@
var doorRescuedTotal = allrooms.filter(function (r) { return r.door_rescued; }).length;
var doorPartitionTotal = allrooms.filter(function (r) { return r.door_partitioned; }).length;
var suspectTotal = allrooms.filter(function (r) { return r.suspect; }).length;
- return { report: report, rooms: allrooms, stZ: stZ, total: total, doorRescuedTotal: doorRescuedTotal, doorPartitionTotal: doorPartitionTotal, suspectTotal: suspectTotal };
+ return { report: report, rooms: allrooms, stZ: stZ, total: total, doorRescuedTotal: doorRescuedTotal,
+ doorPartitionTotal: doorPartitionTotal, suspectTotal: suspectTotal,
+ mergedTotal: mergedTotal, rejectedTotal: rejectedTotal };
}
// Persist a compileRooms() result into spatial_structure + rel_contained_in_space (the --write
@@ -836,7 +1120,7 @@
var compiled = compileRooms(db);
var result = { report: compiled.report, total: compiled.total,
doorRescuedTotal: compiled.doorRescuedTotal, doorPartitionTotal: compiled.doorPartitionTotal,
- suspectTotal: compiled.suspectTotal };
+ suspectTotal: compiled.suspectTotal, mergedTotal: compiled.mergedTotal, rejectedTotal: compiled.rejectedTotal };
if (opts.write) {
var w = writeRooms(db, compiled);
result.roomsWritten = w.roomsWritten; result.rectRowsWritten = w.rectRowsWritten; result.relWritten = w.relWritten;
@@ -849,9 +1133,13 @@
doorStats: doorStats, storeyZAnchors: storeyZAnchors,
doorAdjacent: doorAdjacent, stairOverlapFrac: stairOverlapFrac,
floodRooms: floodRooms, partitionByDoors: partitionByDoors,
+ mergeRooms: mergeRooms, rejectRooms: rejectRooms, allWallsRaw: allWallsRaw, allDoorsRaw: allDoorsRaw,
compileRooms: compileRooms, writeRooms: writeRooms, walk: walk,
RES: RES, MIN_AREA: MIN_AREA, DOOR_SHORTFALL_RATIO: DOOR_SHORTFALL_RATIO,
- VERT_FACTOR: VERT_FACTOR, OPEN_PERIM_FACTOR: OPEN_PERIM_FACTOR
+ VERT_FACTOR: VERT_FACTOR, OPEN_PERIM_FACTOR: OPEN_PERIM_FACTOR,
+ MERGE_GAP_TOL_FACTOR: MERGE_GAP_TOL_FACTOR, MERGE_SHARE_MIN: MERGE_SHARE_MIN,
+ MERGE_WALL_COVER_MAX: MERGE_WALL_COVER_MAX, MERGE_DOOR_TOL: MERGE_DOOR_TOL, WALL_TOL: WALL_TOL,
+ REJECT_ENCLOSURE: REJECT_ENCLOSURE, SUSPECT_OPEN_ENCLOSURE: SUSPECT_OPEN_ENCLOSURE
};
ROOT.RoomWalker = API;
if (typeof module !== 'undefined' && module.exports) module.exports = API;
diff --git a/docs/BIMUserGuide.md b/docs/BIMUserGuide.md
index 3ba87fd20..e82b65135 100644
--- a/docs/BIMUserGuide.md
+++ b/docs/BIMUserGuide.md
@@ -122,7 +122,8 @@ All panels collapse with **−/+**.
- Click any element → IFC class, GUID, storey, discipline, material
- Fly-tour — auto-orbits rendered buildings, click to stop
- Indoor walk-through — follows IfcSpace/door graph through the building
-- X-Ray mode (Alt+Z) — transparent view, see structure through walls
+- X-Ray mode (Alt+Z) — a 3-state cycle: **Off → X-Ray → Bounding Boxes → Off**. X-Ray is the transparent
+ see-through-walls view; Bounding Boxes swaps that for each element's envelope box instead — press again to cycle
- Measure tool — tap two points, get distance in metres
- Section cut — horizontal clip plane, slider to cut through floors
- Storey filter — isolate a single floor
@@ -191,13 +192,26 @@ has that kind of data (no data, no empty axis):
|------|----------------|----------------|
| **Storey** | Always | Elements grouped by building level/storey. Expand a storey to see the rooms/spaces on it (or, if the building has no room data, its most common IFC classes). Tapping a storey or room isolates it in the 3D view. |
| **Discipline** | Always | Elements grouped by discipline (ARC/STR/MEP/ELEC, etc). Expand a discipline to see its IFC classes; tap a class to highlight just those elements. |
-| **Room** | Only if the building has volumetric room (IfcSpace) data | A highlight lens: the model is X-rayed and a translucent box is drawn over each room; tap a room to zoom to it. Has its own **Storey / Type** sub-toggle to group rooms by floor or by room type. (On a building without volume data, this falls back to a plain isolate-by-room list instead.) |
+| **Room** | Only if the building has volumetric room (IfcSpace) data | A highlight lens: the model is X-rayed and a translucent box is drawn over each room. Has its own **Storey / Type / Path** sub-toggle: group rooms by floor, by the compiler's own confidence tier (see [Room health](#room-health-the-type-sub-toggle-verified-live-on-terminal) below), or route between two rooms the way a person walks. (On a building without volume data, this falls back to a plain isolate-by-room list instead.) |
| **Material** | Only if material data is present | Elements grouped by material name, or — via a **Material / Category** sub-toggle — by a derived construction category (Concrete, Metal, Wood, Glass, Drywall/Partition, Masonry, Insulation, Tile, Finish, Membrane, Flooring, Generic, Other). Categories are a keyword-derived heuristic, not an extracted IFC property, and are labelled accordingly in the panel. Highlight lens, same X-ray-and-box behavior as Room. |
| **Phase** | Only if a construction timeline can be generated for the building | Elements grouped by construction phase/task, generated on the fly (a short "Timeline generating…" message appears first). |
| **Parts** *(new)* | Only if the building has stairway, lift-shaft, or plant-room elements | Elements grouped into up to three building-part categories: **Stairway** (stair/ramp classes), **Lift Shaft** (elements named for lifts/elevators), **Plant Room** (HVAC-plant elements — vents, ducts, fans, AHUs, dampers, chillers, pumps). Each category is itself data-gated — it only appears if the building actually has a match. Tapping a category, or a single item inside it, isolates it in the 3D view (hides the rest of the model). |

+**Room highlight, verified live on HHS Office.** Level 2 alone compiles 31 real rooms (105 across the
+whole building) — tapping one X-rays the model and draws a clean, correctly-bounded translucent box
+over just that room, confirming the room's geometry is well-formed and doesn't overlap its neighbours.
+On a larger building, the biggest rooms on a floor stand in for a "hall"-scale space until a real
+labelled corridor/hall example is captured (HHS's own rooms carry no such label yet — a future guide
+pass).
+
+
+
+*Known gap, not glossed over:* tapping a room does not currently reframe the camera to it (confirmed
+live, 2026-07-12) — the highlight is accurate, but you may need to manually orbit/zoom to see a small
+room clearly on a large building. Tracked for a future fix.
+
> The Parts axis is the newest addition (bim-ootb `d04ddd5`) — see
> `prompts/VIEWER_FIND_PANEL_PARTS_VERIFICATION.md` for its live verification on Duplex/SampleCastle data.
> A false-positive/missing-class-gate bug found after that verification (`prompts/FIND_PANEL_PLANT_ROOM_GATE_FIX.md`)
@@ -217,6 +231,167 @@ height) is only added by the plain-search code path, not the NL-query path — s
actually appears while typing a recognized phrase; the query still runs correctly on **Enter**. The guide
text above describes the observed (silent) behavior, not an invented visible hint.
+#### Room health — the Type sub-toggle (verified live on Terminal)
+
+Switch the Room axis's grouping from **Storey** to **Type** and rooms are grouped by the compiler's
+own confidence in them, not by a floor: **INTERNAL** / **INTERNAL_SMALL** (ordinary enclosed rooms,
+split by size) and **SUSPECT_OPEN** / **SUSPECT_NO_DOOR** (rooms the compiler could bound
+geometrically but flags for a human rather than silently accepting). This is the compile-with-honesty
+principle made visible — a low-confidence room is *shown* as low-confidence, never quietly promoted
+to "room" just because it fits inside walls.
+
+**Terminal, the hard case, verified live.** A user-reported screenshot showed a stairwell counted as
+a room — the compiler doesn't know "stairwell" as a concept, only geometry, and a tall vertical shaft
+can look room-shaped from a single floor's footprint alone. The fix (**STAIRWELL-STACK reject**)
+rejects a room candidate whose footprint recurs, stacked, through multiple storeys with a real stair
+inside it — the signature of a shaft, not a room — checked against both compiler mirrors, 6/6 parity.
+Reloading Terminal applies the healed patch over whatever the browser had cached already: **59 rooms →
+40 rooms, 73 rects, no room anywhere near the shaft threshold** — the stairwell is gone from both the
+Room list and the 3D view.
+
+
+
+That SUSPECT_NO_DOOR pick is the demo frame: an empty pocket next to the stair, no door found
+bounding it, so the compiler says exactly what it knows and stops — it doesn't invent a door to make
+the room look finished.
+
+*Known gap, named not hidden:* Type is a **health** taxonomy (how sure the compiler is this is a
+room), not a **semantic** one — it doesn't yet know "corridor" from "office." Terminal's concourses
+show no CORRIDOR band today because the only measured corridor template on file is Duplex's 10.4m²
+hallway (n=2 samples), and stretching that onto a terminal concourse would be inventing, not
+compiling. The room-path routing below already produces a building-relative, *measurable* definition
+of a corridor — the elongated, many-doored room every route keeps passing through — scoped as a
+future CIRCULATION_DISPLAY pass, not built yet.
+
+#### Room-to-room paths & escape routes
+
+With the **Room** axis selected, the **Path** sub-mode routes between any two rooms the way a
+person actually walks — out the door, along the corridor or concourse, and up or down the stairs
+when the two rooms are on different floors. The route draws as a line through the real doors and
+stair flights it uses, and the rooms along the way stay highlighted.
+
+*Verified live (2026-07-12):* a real route across Terminal's floors returns several stair-crossing
+legs in sequence, not a single best-guess hop — each leg names the room and the door or stair flight
+it passes through.
+
+*Future feature — fire escape:* the same routing will pin a **Fire Escape** entry at the top of the
+path list — one tap from any room to the nearest building exit.
+
+*Future feature — mobile:* scan a QR code posted beside a door to fetch that building's lightweight
+architecture model on your phone and see the escape route in Walk mode from exactly where you stand.
+
+### The rest of the Navigate drawer — World History, Page History & Home
+
+The **Navigate** drawer (sailboat icon — see [Find panel](#find-panel-search-voice-query-and-axis-lenses)
+above for how to open it) has three more rows besides Find / Navigate:
+
+- **World History** (shortcut **W**) — a cross-page timeline: every significant action across *every*
+ page — Viewer, ERP, Gravity — in one place. Opens a card with a **Whole / This page** toggle, day-by-day
+ navigation (**‹ day** / **day ›**), and one entry per action (what happened, where, and when).
+ 
+- **Page History** (shortcut **Z**) — this page's own compact dot-timeline, a small step-back/step-forward
+ bar. It only lights up once you've made edits in this session — a fresh session shows an empty strip
+ (the pair of arrows either side of a single dot), as captured here.
+ 
+- **Home** — returns to the front-door hub (the same page the [Quick Start](#quick-start-your-first-building)
+ walkthrough starts from). Installed as a standalone app (PWA), it opens the live hub online, or falls
+ back to the cached hub offline.
+
+### Inspect drawer — Measure, Clash, X-Ray, Section, Time Machine, 4D/5D, Fly Tour
+
+The **Inspect** drawer (compass icon, next to Navigate on the toolbar rail) bundles seven tools behind
+one icon:
+
+
+
+- **Measure**, **X-Ray / Bbox**, **Section Cut**, and **Fly Tour** are covered above, under
+ [Viewer Features](#viewer-features).
+- **Clash Matrix** (key **C**) opens the clash-detection engine (discipline-pair grid, tolerance, Review /
+ Resolve / Accept status, HTML + CSV export) — full coverage: **[Clash Detection guide](CLASH_DETECTION.md)**.
+- **Time Machine** (key **T**) opens the 4D construction timeline — author a schedule, play it back,
+ try a What-if slip, share a `?tm=play` link. Full authoring/playback walkthrough already lives in
+ **[Kernel-ERP User Guide → Time Machine](ERPUserGuide.md)** (not re-documented here — same building,
+ same feature, reached from either app).
+- **4D / 5D** (key **4**) opens the analytics dashboard (`boq_charts.html`) for the loaded building in a
+ new tab — full coverage: **[4D/5D Analysis guide](4D5DAnalysis.md)**.
+
+### Camera / View drawer
+
+The **Camera / View** drawer (camera icon) bundles three camera-control toggles:
+
+
+
+- **Precision (Fine)** (Caps Lock) — slows orbit/pan/zoom for fine, deliberate camera moves (e.g. lining
+ up a screenshot or a measurement).
+- **Reset Camera** (key **A**) — snaps the camera back to its default orbit position.
+- **Auto-Pivot** (key **Q**) — toggles automatic pivot-point recentring as you orbit, so the camera keeps
+ turning around whatever's in view instead of a fixed point.
+
+### Display options — Palette, Night, Shadow + Ground, Background, Sound FX
+
+The **Palette** pill (key **P**) opens one panel for every visual-appearance control — five lighting
+sliders, plus four more toggles appended below them:
+
+
+
+| Control | Shortcut | What it does |
+|---|---|---|
+| Ambience / Sun / Exposure / Ambient / Hemisphere | — | Five sliders — overall scene lighting, sun intensity, camera exposure, ambient fill light, and sky/ground hemisphere light. |
+| **Night** | **N** | Toggles a night lighting preset. |
+| **Shadow + Ground** | **H** | Cycles **Off → Grass → Earth → Paved** — a real ground-texture swatch under the building, with matching shadows. |
+| **Background** | **B** | Reverses the background (dark ↔ light/white). |
+| **Sound FX** | **V** | Toggles synthesized UI/Time-Machine/Fly-Tour sound cues — no audio files, off by default. |
+
+### Settings
+
+The **Settings** pill (key **=**) opens a panel with four sections:
+
+
+
+- **Pill Icons** — show/hide/reorder every toolbar action, and see each one's current shortcut key at a
+ glance (this is also how a hidden action like a data-gated drawer row becomes visible once its data
+ exists). A **Reset Pill Icons** button restores the defaults.
+- **Edit Project JSON** — a power-user hub: open and edit any of the project's config files directly
+ in-browser (auto-inferred form fields, not raw text), then **Download** the edited file to commit back
+ to the repo, or **Reset** to discard the override. Six files are registered: **Corporate / Branding**,
+ **Grid Rules**, **Clash Rules**, **ERP Globe Bubbles**, **Sound Effects** (the audio *parameters* file —
+ distinct from the Display-options Sound FX on/off toggle above), and a **read-only** view of the
+ **4D Schedule** captured for the currently-open building (the same data Time Machine authors).
+- **5D Rate Pack** — pick which cost-rate pack is active (the same rate pack the Find panel's
+ `total cost` query and the 4D/5D dashboard both price against).
+- **Cache Info** — see how much this building's data is using in IndexedDB, and clear it.
+
+ 
+
+### Save & Open a building
+
+Two toolbar pills, both native-dialog verbs — distinct from the Hub's building-open flow in
+[Quick Start](#quick-start-your-first-building):
+
+
+
+- **Save Building** (**Ctrl+S**) — saves the currently open building, including any session edits
+ (clash resolutions, captured 4D schedule, etc.), to a `.db` file via the browser's native Save As dialog.
+- **Open Building** (**Ctrl+O**) — opens a previously-saved `.db` file via a native Open dialog, replacing
+ the current scene.
+
+### Share
+
+The **Share** pill (key **/**) is a step up from the plain deep-link URL: on mobile, it hands the current
+view to the device's native share sheet with a snapshot photo attached; on desktop (no native share API),
+it shows a preview card — a live snapshot, the building name, the same deep-link URL described above, and
+**Copy Link** / **Cancel** buttons. If a clash is open when you tap Share, the shared text and photo are
+about that specific clash instead of the general view.
+
+
+
+### Pick Walk — warehouse / logistics buildings
+
+A data-gated pill (only appears when the loaded building carries locator-GUID bins, e.g. a warehouse
+building like GardenWorld) that walks a picking route over the bins: fly to the next bin in order, scan
+a bin's QR/type code, and record a signed pick group per bin. Not covered further here — it needs a
+warehouse-class building loaded to demonstrate, outside this general viewer guide's scope.
+
### FM / Operate lenses — HR_BIM_Asset *(ALPHA)*
@@ -256,6 +431,7 @@ and dashboard graphs — off by default, pixel-identical until you turn it on.
| **Roof** | Roof plan view |
| **Alt+Z** | Toggle X-ray mode |
| **F11** | Toggle fullscreen |
+| **F1** | Help — the full, live list of every toolbar action and its shortcut key |
> Authoring — editing the structural grid, sketching, extruding — lives in the **[DAGeVu Modeller](ModellerGuide.md)**, not the Viewer.
diff --git a/docs/ModellerGuide.md b/docs/ModellerGuide.md
index fe738434e..9e11bab2d 100644
--- a/docs/ModellerGuide.md
+++ b/docs/ModellerGuide.md
@@ -341,13 +341,17 @@ it: it places that trade's elements at the **measured cadence** of a real coordi
runs it can, gates the clashes, and **honestly refuses** when the building has nothing to hang the trade
on. Nothing is invented — every placement uses a spacing/clearance rule *mined from a real IFC model*.
-
-
-*Why the fixtures render as plain blocks:* a walked placement is real (its position and count are exact,
-mined from a real building), but its **mesh** is still a stand-in — a box sized to that fixture class's own
-measured median dimensions, not a finished fixture model. That's a deliberate, honest choice, not a bug:
-the alternative would be guessing at a fixture's real shape, which this project's non-invent rule forbids.
-A finer mesh swap for these is on the roadmap; today, trust the placement and count, not the silhouette.
+
+
+*Why you can't see any fixtures in this shot:* a real electrical outlet or light lives inside a room, not
+poking through an exterior wall — from outside the sealed shell it's naturally occluded, same as it would
+be in a real building. This is the corrected view (2026-07-12): a previous version of this pipeline had a
+containment bug where roughly a quarter of placements landed outside the building's own walls — fixed
+(mesh-recovered true-midpoint host binding) and independently verified 5 separate ways (containment count,
+real-oracle walk-back match, measured-pattern conformance, wall-clearance margin, mirror-symmetry residual
+on the Duplex's own A/B twin layout) before this screenshot was retaken. The fixture mesh itself is still a
+box stand-in sized to each class's own measured dimensions, not a finished fixture model — a deliberate,
+honest choice: guessing at a fixture's real shape is exactly what this project's non-invent rule forbids.
**One engine, two standards.** A single walker drives every discipline; the discipline is just a data
filter. It carries two measured rule-sets and auto-selects by building class:
@@ -445,7 +449,7 @@ After walking a discipline, route its **service trunk** from a real entry.
A corridor-aware trunk is routed from that entry through the walked fixtures — around walls, through real
doors, up risers between storeys.
-
+
> **Deeper proof.** The full mining, round-trip, boundary and generalization analysis lives in the resume
> cards `prompts/RESUME_DX_MEP_RESIDENTIAL_STANDARD.md`, `RESUME_TERMINAL_RULE_MINING.md` and
diff --git a/docs/img/modeller/seedtrunk-trunk.png b/docs/img/modeller/seedtrunk-trunk.png
index 89521552d..e9b516702 100644
Binary files a/docs/img/modeller/seedtrunk-trunk.png and b/docs/img/modeller/seedtrunk-trunk.png differ
diff --git a/docs/img/modeller/walk-fixtures.png b/docs/img/modeller/walk-fixtures.png
index 6a02659a2..d275904d2 100644
Binary files a/docs/img/modeller/walk-fixtures.png and b/docs/img/modeller/walk-fixtures.png differ
diff --git a/docs/img/viewer/find-room-highlight-hhs.png b/docs/img/viewer/find-room-highlight-hhs.png
new file mode 100644
index 000000000..6e21d14fb
Binary files /dev/null and b/docs/img/viewer/find-room-highlight-hhs.png differ
diff --git a/docs/img/viewer/pill-camview-drawer.png b/docs/img/viewer/pill-camview-drawer.png
new file mode 100644
index 000000000..7e7ada752
Binary files /dev/null and b/docs/img/viewer/pill-camview-drawer.png differ
diff --git a/docs/img/viewer/pill-display-options.png b/docs/img/viewer/pill-display-options.png
new file mode 100644
index 000000000..2ccdad5ec
Binary files /dev/null and b/docs/img/viewer/pill-display-options.png differ
diff --git a/docs/img/viewer/pill-inspect-drawer.png b/docs/img/viewer/pill-inspect-drawer.png
new file mode 100644
index 000000000..933e38932
Binary files /dev/null and b/docs/img/viewer/pill-inspect-drawer.png differ
diff --git a/docs/img/viewer/pill-page-history.png b/docs/img/viewer/pill-page-history.png
new file mode 100644
index 000000000..0b667a6b6
Binary files /dev/null and b/docs/img/viewer/pill-page-history.png differ
diff --git a/docs/img/viewer/pill-save-open.png b/docs/img/viewer/pill-save-open.png
new file mode 100644
index 000000000..1acae47ca
Binary files /dev/null and b/docs/img/viewer/pill-save-open.png differ
diff --git a/docs/img/viewer/pill-settings-json-hub.png b/docs/img/viewer/pill-settings-json-hub.png
new file mode 100644
index 000000000..7b3cff4a3
Binary files /dev/null and b/docs/img/viewer/pill-settings-json-hub.png differ
diff --git a/docs/img/viewer/pill-settings-panel.png b/docs/img/viewer/pill-settings-panel.png
new file mode 100644
index 000000000..46a4ffc9b
Binary files /dev/null and b/docs/img/viewer/pill-settings-panel.png differ
diff --git a/docs/img/viewer/pill-share-preview.png b/docs/img/viewer/pill-share-preview.png
new file mode 100644
index 000000000..aac1dcf7f
Binary files /dev/null and b/docs/img/viewer/pill-share-preview.png differ
diff --git a/docs/img/viewer/pill-world-history.png b/docs/img/viewer/pill-world-history.png
new file mode 100644
index 000000000..d69897698
Binary files /dev/null and b/docs/img/viewer/pill-world-history.png differ
diff --git a/docs/img/viewer/type-suspect-no-door-terminal.png b/docs/img/viewer/type-suspect-no-door-terminal.png
new file mode 100644
index 000000000..f4704aa3e
Binary files /dev/null and b/docs/img/viewer/type-suspect-no-door-terminal.png differ
diff --git a/prompts/BIMUSERGUIDE_PILL_COVERAGE_AUDIT.md b/prompts/BIMUSERGUIDE_PILL_COVERAGE_AUDIT.md
new file mode 100644
index 000000000..c5984963e
--- /dev/null
+++ b/prompts/BIMUSERGUIDE_PILL_COVERAGE_AUDIT.md
@@ -0,0 +1,131 @@
+
+# BIMUSERGUIDE PILL COVERAGE AUDIT — fill the missing toolbar features (2026-07-12, dispatch to Agent)
+
+```
+# ⚠ DO NOT REMOVE
+SCOPE: bim-compiler `docs/BIMUserGuide.md` + `docs/img/viewer/*` only — a completeness pass, not a
+rewrite. User's own words (2026-07-12): "I see Viewer guide lots of missing ie on the whole pill
+list of other features besides Time machine ERP which can link back to ERP guide." Read this whole
+file, triage first (see Task 1), then execute. Read the log after every run. PUSH PAUSE LIFTED for
+this repo — commit locally, verify on localhost, push, no PR needed for docs-only (same pattern as
+every docs commit this session — direct push to `fable/meshdb-livewire`, no separate branch/PR
+ceremony for pure docs changes, that convention has held all session).
+```
+
+## Ground truth — the real pill list vs what's documented (confirmed via source, don't re-derive)
+`common/pill_builder.js` + `viewer/panels.js` (bim-ootb) define these `id:` actions — the full real
+toolbar/drawer surface:
+`audio, background, bbox, cam-pivot, cam-reset, camview, clash, clash_rules, corporate, docHist,
+find, fly, fullscreen, grid_rules, hbaFM, help, home, initbubble, inspect, issues, json-editor,
+measure, navigate, night, open, palette, precision, report, save, schedule, section, settings, sfx,
+shadow, share, tm, walk, whwalk, worldhist, xray`
+
+`docs/BIMUserGuide.md` currently documents, explicitly, only: X-Ray, Measure, Section-cut, Screenshot,
+Storey filter, Discipline toggle, Deep-link URL, IndexedDB cache, City mode, Find panel (fully, just
+added), FM/Operate lenses (cross-linked to `HRBIMAssetGuide.md`), Teams overlay (cross-linked to
+`TeamsOverlayGuide.md`), and a mobile-only list. Roughly a dozen of ~40 real actions are covered.
+
+## Task 1 — triage every undocumented id BEFORE writing anything (don't blindly document all 40)
+For each id not already covered, read its `fn`/behavior in `panels.js`/`pill_builder.js` and sort
+into exactly one bucket:
+1. **Real user-facing feature, needs guide coverage** (most of these, likely): `tm` (Time Machine),
+ `clash`/`clash_rules` (clash detection), `schedule`, `share`, `worldhist`/`docHist` (World/Doc
+ History — note this project already has extensive History-lane work, check
+ `project_whole_history_timeline.md`-style memory/prompts before writing from scratch), `walk`
+ (indoor walkthrough — may already be covered under "Viewer Features" bullets, verify), `whwalk`,
+ `save`/`open`, `night`/`shadow`/`background`/`palette` (visual/display options), `precision`,
+ `issues`, `report`, `sfx` (sound), `inspect`, `cam-pivot`/`cam-reset`/`camview`, `home`,
+ `fullscreen`.
+2. **Already covered elsewhere, needs only a cross-link** (the user's own example): `tm` (Time
+ Machine) — `docs/ERPUserGuide.md` already has full authoring/playback/What-if coverage (lines
+ ~162-230+, confirmed this session). Do NOT re-document Time Machine in BIMUserGuide.md — add a
+ short pointer sentence + link, same pattern already used for FM/Operate lenses and Teams overlay
+ in this same doc (see how those two are handled — one paragraph + "Full walkthrough: [link]").
+ Check EVERY id for this pattern before writing fresh prose — `worldhist`/`docHist` may also
+ already be covered by history-lane docs elsewhere, check before assuming it needs new content.
+3. **Internal/dev-only, out of scope for an end-user guide** — `json-editor`, `corporate`,
+ `initbubble`, `grid_rules` look like dev/admin tooling by name alone, but VERIFY by reading what
+ they actually do before excluding (don't guess from the id string) — a wrong exclusion silently
+ drops a real feature, a wrong inclusion documents an internal tool as if it's for end users.
+4. Report the full triage (id → bucket + one-line reason) as the FIRST thing committed, before any
+ prose/screenshot work — so the scope is visible and reviewable before the bulk of the work happens.
+
+## Task 2 — write + screenshot bucket-1 items, cross-link bucket-2 items
+Same quality bar as every guide addition this session (see the Find panel section already in this
+doc as the template): numbered step-by-step navigation (which pill, where, what it does), real
+Playwright screenshots (`deviceScaleFactor:2`, tight clips, 0 console errors — not placeholders),
+placed under `## Viewer Features` alongside the existing sub-sections. Group related small features
+into one sub-section rather than one heading per pill where it reads better (e.g. `night`/`shadow`/
+`background`/`palette` are all "display appearance" toggles — one "Display options" sub-section with
+a small table, not four separate headings).
+
+## Explicitly out of scope
+- `docs/ModellerGuide.md` — separate doc, not this task (unless a bucket-3 review finds something
+ that's genuinely Modeller-only mislabeled here, name it, don't silently move it).
+- Rewriting any ALREADY-documented section (X-Ray, Measure, Section-cut, Find panel, etc.) — this is
+ purely filling gaps, not a full-doc revision.
+- Any code changes — this is docs-only, even if a triage step reveals a pill that seems broken or
+ oddly named; name it as a finding, don't fix it here.
+
+## DONE WHEN
+Task 1's full triage table committed first (reviewable checkpoint). Every bucket-1 id has a real,
+screenshotted section; every bucket-2 id has a one-sentence cross-link (no duplication); every
+bucket-3 exclusion is justified with what was actually read, not guessed. Pushed to
+`fable/meshdb-livewire` per this session's docs convention.
+
+## Task 1 — full triage table (2026-07-12, Agent)
+
+Read every id's real `fn`/behavior in `common/pill_builder.js` + `viewer/panels.js` (bim-ootb main,
+verified fresh — `home`/`worldhist`/`docHist`/`walk` etc. all read directly, not guessed from the id
+string) and cross-checked every candidate cross-link target actually exists and covers the topic
+before citing it. Already-documented ids are listed too (no action) so the table is a complete audit,
+not just the gap list.
+
+| id | Bucket | Reason (what was actually read) |
+|---|---|---|
+| `find` | 0 already documented | Full Find panel section, just added — no action. |
+| `xray` | 0 already documented | X-Ray bullet + Keyboard cheat-sheet — no action. |
+| `measure` | 0 already documented | Measure tool bullet — no action. |
+| `section` | 0 already documented | Section cut bullet — no action. |
+| `fly` | 0 already documented | "Fly-tour — auto-orbits…" bullet — no action. |
+| `fullscreen` | 0 already documented | 3D Navigation table + Toolbar table + Keyboard cheat-sheet (3 places) — no action. |
+| `hbaFM` | 0 already documented | FM/Operate lenses section, cross-linked to `HRBIMAssetGuide.md` — no action. |
+| `walk` | 0 already documented | `A.toggleWalkMode` (walk.js) is the real GPS/device-orientation blue-dot mode, genuinely `platform:'mobile'`-gated — matches the existing "GPS Walk Mode — blue dot tracks position" mobile-only bullet exactly. No action. |
+| `issues` | 0 already documented *(finding, not fixed)* | "Issue log" is in the Mobile-only bullet list. Read `A.toggleIssues` (issues.js) — it has **no** platform gate in code, works identically on desktop. Existing placement is technically incomplete, but rewriting that list is explicitly out of scope for this pass — flagging only. |
+| `tm` | 2 cross-link | `docs/ERPUserGuide.md` lines ~162-230+ already has full Time Machine authoring/playback/What-if coverage (confirmed this session, per dispatch). Adding a pointer paragraph, not new prose. |
+| `clash` | 2 cross-link | `A.export4D5D`/clash flow already linked from `docs/BIMUserGuide.md`'s Further Reading table → `CLASH_DETECTION.md`. That table-row link exists but is never tied to the actual toolbar pill (key `c`, lives in the Inspect drawer) anywhere in the doc body — adding an in-context pointer sentence where the Inspect drawer is introduced, not new clash-detection prose. |
+| `report` | 2 cross-link | `A.export4D5D` (tools.js) opens `boq_charts.html` — confirmed this is the same analytics the Further Reading table already links as `4D5DAnalysis.md`. Same treatment as `clash`: in-context pointer in the Inspect drawer section. |
+| `worldhist` | 1 needs coverage | No end-user doc exists anywhere (checked `docs/*.md` for "World History"/"worldhist" — zero hits outside the Find panel section's own drawer-row listing, which only NAMES the row, never describes what tapping it does). The `prompts/HISTORY_*.md` files are dev implementation specs, not end-user docs — not a valid cross-link target. Real gap. |
+| `docHist` | 1 needs coverage | Same as `worldhist` — named as a Navigate-drawer row in the Find panel section, never functionally described. Real gap. |
+| `home` | 1 needs coverage | Same pattern — named as a drawer row, `fn` (panels.js ~1277) never described (returns to the front-door hub, PWA-aware). Real gap. |
+| `save` / `open` | 1 needs coverage | `A.saveModelDb`/`A.openModelDb` — real native-dialog save/restore of the current session's `.db` (distinct from the Hub's building-open flow already covered in Quick Start). Never mentioned anywhere in the doc. Real gap. |
+| `share` | 1 needs coverage | `A.quickShare` (share.js) — a real, distinct pill: native share sheet (photo attached) on mobile, a copy/share preview card on desktop, plus clash-aware sharing when a clash is open. More than the existing "Deep-link URL" bullet describes (that bullet only names the underlying URL mechanism, not this pill/UI). Real gap. |
+| `navigate` | 0 already documented | The Find panel section already fully documents opening this drawer — no action beyond the grouped `worldhist`/`docHist`/`home` mini-section above (its OTHER rows). |
+| `inspect` | 1 needs coverage | Drawer master, never documented as an entry point. Bundles `measure`/`clash`/`xray`/`section`/`tm`/`report`/`fly` (`_inspectDrawer` subIds, panels.js ~1529) — most already covered individually; this section documents the drawer itself + carries the `tm`/`clash`/`report` cross-link pointers. |
+| `camview` | 1 needs coverage | Drawer master, never documented. Bundles `precision`/`cam-reset`/`cam-pivot` (panels.js ~1530). |
+| `cam-reset` | 1 needs coverage | `window.resetCamOrbit` — real, undocumented. Folds into the `camview` section. |
+| `cam-pivot` | 1 needs coverage | `window.toggleCamPivot` (Auto-Pivot) — real, undocumented. Folds into the `camview` section. |
+| `precision` | 1 needs coverage | `window.togglePrecisionFine` (Caps Lock) — fine-movement camera mode, undocumented. Folds into the `camview` section. |
+| `palette` | 1 needs coverage | `A.toggleSunglass` opens a real panel: 5 lighting sliders (Ambience/Sun/Exposure/Ambient/Hemisphere) — verified by reading `A._buildSunglassPanel` (panels.js ~290-377). Container for the grouped "Display options" section below. |
+| `night` / `shadow` / `background` / `audio` | 1 needs coverage, **grouped** | Verified via `_extendVisualFxPanel` (panels.js ~1536-1554): these 4 are literally appended as rows INSIDE the same Palette panel opened by `palette` (not separate panels) — Night toggle, Shadow+Ground 4-state cycle (Off→Grass→Earth→Paved), Background (white/dark reverse), Sound FX on/off. One "Display options" sub-section, per the prompt's own grouping instruction — this is the concrete case it was pointing at. |
+| `settings` | 1 needs coverage | `_openSettingsPanel` (panels.js ~1559) — real panel: Pill Icons customizer (show/hide/reorder the toolbar), 5D Rate Pack picker, Cache Info + clear, Reset Pill Icons, and the "Edit Project JSON" hub. Never documented. |
+| `json-editor` | 1 needs coverage | The "Edit Project JSON" hub itself (`_buildJsonHub`, panels.js ~1976) — a real end-user-reachable panel (via Settings, no auth wall/flag), NOT a hidden dev tool. Folds into the `settings` section as one described feature (its own bullet list is the 6 entries below). |
+| `corporate` | 1 needs coverage, minor | Verified via `_jsonRegistry` (panels.js ~1962): "Corporate / Branding" — edits `corporate.json` (site branding) through the same reachable Settings→JSON hub. Real, not internal-only by the letter of "who can reach it" — but the audience is a project admin, not a day-to-day viewer, so it gets one bullet inside the `settings` section, not its own screenshot/heading. |
+| `grid_rules` | 1 needs coverage, minor | Same hub, "Grid Rules" — edits `grid_rules.json`. Same treatment as `corporate`. |
+| `clash_rules` | 1 needs coverage, minor | Same hub, "Clash Rules" — edits `clash_rules.json` (tolerance/rules used by the Clash Matrix). Same treatment. |
+| `initbubble` | 1 needs coverage, minor | Same hub, "ERP Globe Bubbles" — edits `initbubble.json`. Same treatment. |
+| `sfx` | 1 needs coverage, minor | Same hub, "Sound Effects" — edits `sfx.json` (synthesized-audio parameters). **Distinct from `audio`** (the on/off toggle inside the Display-options Palette panel) — this is the JSON tuning file behind it. Same treatment. |
+| `schedule` | 1 needs coverage, minor | Same hub, "4D Schedule (this building)" — a **read-only** projection of the captured Time Machine schedule as JSON (`source:'db'`, `readonly:true`). Same treatment; also gets a one-clause mention alongside the `tm` cross-link since it's the same underlying data. |
+| `whwalk` | 1 needs coverage, minor/niche | `WHWalk.toggle` — real, but `pill:false`-gated until the loaded building carries locator-GUID bins (warehouse/logistics buildings only, e.g. GardenWorld). No existing warehouse/logistics guide doc exists in `docs/` to cross-link. One short paragraph, no screenshot — a real building with this data would be a separate side-quest disproportionate to one pill in a general viewer guide; noting the scoping choice here rather than silently skipping it. |
+| `bbox` | 1 needs coverage, minor (not a new section) | `window.toggleGhostXray` — confirmed absorbed into the SAME `xray` key/button as a 3-state cycle (Off→X-Ray→Bbox→Off, panels.js ~1230-1239 comment: "Alt+X retired"). No separate UI entry point exists any more. One clarifying sentence added to the *existing* X-Ray bullet (a gap-fill, not the "rewriting an already-documented section" this task excludes) rather than a new heading. |
+| `help` | 1 needs coverage, minor (not a new section) | `showCommandPalette` (F1) — opens the live pill/shortcut list. One line added to the Keyboard & Mouse Cheat-Sheet section, not a dedicated heading. |
+
+**Bucket-3 (internal/dev-only, excluded) check:** every id the prompt flagged as a *guess* —
+`json-editor`, `corporate`, `initbubble`, `grid_rules` — turned out, on actually reading the code, to
+be reachable by any user through Settings with no auth wall or feature flag, i.e. genuinely bucket-1
+(minor), not bucket-3. **There is no bucket-3 exclusion in this audit** — nothing was found to be
+truly hidden/dev-only among the 40 real ids. Named here explicitly so the "wrong exclusion" failure
+mode the prompt warned about is visibly not what happened.
+
+**Scope note:** `docs/ModellerGuide.md` was not touched — every id above belongs to the Viewer's own
+`panels.js`/`pill_builder.js` surface, not a Modeller-mislabeled one.
diff --git a/prompts/FIND_PANEL_ISOLATE_NO_CAMERA_ZOOM.md b/prompts/FIND_PANEL_ISOLATE_NO_CAMERA_ZOOM.md
new file mode 100644
index 000000000..5495c6eff
--- /dev/null
+++ b/prompts/FIND_PANEL_ISOLATE_NO_CAMERA_ZOOM.md
@@ -0,0 +1,101 @@
+
+# FIND PANEL — isolate-tap has no camera zoom/fit, "zoom" is illusory (2026-07-12)
+
+```
+# ⚠ DO NOT REMOVE
+SCOPE: bim-ootb `viewer/navigate_find.js` — user-reported (2026-07-12, live on localhost): "clicking
+on them does not show consistent zooming in as expected" for Parts/Stairway tree items. Traced to
+source before writing this file (see below) — read this whole file before touching code. User wants
+to pick this one up personally ("let me iterate with it separately but earnestly") — treat this as a
+handoff spec, not a dispatch target, unless told otherwise. PUSH PAUSE LIFTED for this repo — commit
+locally, verify on localhost, push + PR with auto-merge once done, same convention as this session.
+```
+
+## Root cause, confirmed from source (don't re-derive)
+Every axis's "tap a group/leaf to isolate" path — `_isolatePartsGroup` (Parts), `_isolateLensGroup`
+(Room/Material), `applyIsolate`, `isolateLeaf` — ultimately calls the SAME `_emitIsolate(set, by)`
+(`viewer/navigate_find.js` ~line 2527):
+```js
+function _emitIsolate(set, by) {
+ A.filterByGuids(set);
+ ... // just logs visible/hidden/total counts, no camera code at all
+}
+```
+**There is no camera-fit/zoom-to call anywhere in this function, for ANY axis.** Isolating a group
+only toggles visibility (`A.filterByGuids`) — it never moves or reframes the camera. What reads as
+"zoom" is an illusion: when the isolated element(s) happen to already be inside the current camera
+frustum, hiding everything else makes them visually pop/fill more of the view, which LOOKS like a
+zoom. When the isolated element is off to the side, behind the camera, or far from the current view,
+nothing visually changes except stuff disappearing — no reframing occurs, so it looks like "nothing
+happened" or "isolate is broken." **This exactly matches the reported inconsistency** — it isn't
+flaky, it's a deterministic function of whether the target already happens to be on-screen.
+
+**A real zoom-to-selection feature already exists elsewhere in this codebase** — confirmed live this
+session in the Modeller Outliner's own witness (`W-E2E-OL-GROUPSELECT` K3: "that same click fired
+#711's `§ZOOM-SEL` fill line... zoomToSelection ran off selectMany → setSelectionIds"). So the
+Modeller side has a working `zoomToSelection`-style call already wired to its own group-select path
+— the Viewer's Find panel isolate path was simply never connected to the equivalent Viewer-side
+camera-fit function (if one exists — check first, see Task 1).
+
+## Task 1 — find the Viewer's own camera-fit primitive (don't assume it's absent, check)
+1. Search `viewer/scene.js`/`viewer/streaming.js`/wherever the Viewer's camera logic lives for an
+ existing "fit camera to a set of GUIDs/bbox" function — the Room axis's OWN highlight-lens mode
+ (`_roomBoxes`, ~line 828 area) already computes a bbox per room from `spatial_structure` center/
+ size; check whether ANYTHING in that code path, or elsewhere, already does a camera-frame-to-bbox
+ operation for some other feature (e.g. a "zoom to selection" toolbar action, a double-click-to-
+ frame on a picked element) that could be reused rather than invented fresh.
+2. If a real, working camera-fit primitive exists: wire `_emitIsolate` (or each caller, if a shared
+ wire-up isn't clean) to call it with the isolated set's real bbox (computed the same way Room's
+ highlight lens already computes room bboxes from `spatial_structure`/`element_transforms`, or
+ however the found primitive expects its input — cite it).
+3. If NO such primitive exists in the Viewer: this becomes a small new feature (compute bbox of the
+ isolated GUID set from `element_transforms`, animate/set camera to frame it), not a bug fix —
+ name it as that distinction plainly, since it changes scope/effort.
+
+## Task 2 — verify it doesn't fight Room/Material's existing highlight-lens camera behavior
+Room and Material already have SOME camera behavior on tap (per the guide's own doc text: "tap a
+room to zoom to it" — verify this claim against actual code too, it may be equally illusory/aspirational
+prose, not a promise already kept). If Room DOES already zoom correctly and Parts/Material don't,
+find what Room does differently and reuse it rather than building a second mechanism. If Room's own
+"zoom to it" turns out to be the SAME illusion, that's a bigger, guide-text-correcting finding — name
+it, don't just patch Parts in isolation and leave the doc's claim for Room silently unverified.
+
+## Explicitly out of scope
+- Any change to `A.filterByGuids` itself (the visibility mechanism is correct and working, per the
+ original bug report — only the missing camera reframe is the issue).
+- The Plant Room/keyword/class-gate logic — unrelated, already fixed (#740/#742).
+
+## DONE WHEN
+Either: a real camera-fit is wired to isolate-tap for Parts (and Room/Material if Task 2 finds they
+need it too), verified live (tap an off-screen Stairway item, confirm the camera actually reframes to
+it, not just visibility-filters) — or, if no primitive exists and building one is out of the
+immediate appetite, a clear written finding + effort estimate so the user can decide whether to
+proceed, without code changes forced prematurely.
+
+## Task 1 + 2 findings (2026-07-12) — primitive confirmed to exist, not built yet
+**A camera-fit primitive already exists in the Viewer itself, in this SAME file** — no need to reuse
+Modeller's `zoomToSelection` cross-repo, it's closer than that:
+- `_bboxOfGuids(set)` (~line 2186) — world-space bbox of a guid set from `element_transforms`.
+- `_zoomToGroup(set)` / `_zoomToGuids(set, factor)` (~line 1786/2205) — call `_bboxOfGuids` then
+ `_zoomToBoxFill`/`_zoomToBox` to fly the camera to frame it. Both already exist, already tested by
+ use (see next point), zero new code needed for the math.
+- `A.focusElement(guids, opts)` (~line 3711) — the higher-level "neutral shared focus primitive":
+ ghosts the rest, highlights the target, and (unless `opts.frame === false`) calls `_zoomToGuids`/
+ `_zoomToGroup` to reframe. Already wired to 3D tap-to-pick (picking.js), the Find **drill**
+ (`_drillSelect`, line ~2340), and history-restore.
+
+**`_emitIsolate` (line 2527) is the one path in this file that was never connected to any of them** —
+confirmed it's still just `A.filterByGuids(set)` + a console log, exactly as originally traced.
+
+**Task 2 answered:** Room/Material's GROUP-header tap in the Find tree (`_isolateLensGroup`, line 807)
+ALSO calls `_emitIsolate` directly — same non-zoom illusion as Parts/Stairs, not special-cased. But
+Room has a SEPARATE interaction people may be thinking of — `_roomSelect` (tapping a room to see its
+*contents*, the x-ray drill path) — which DOES zoom, via `_zoomToBoxFill`. So "does Room zoom on tap"
+depends on WHICH room tap: content-drill zooms today, group-isolate (the tree header) does not. Not a
+blanket claim to correct — a real split in current behavior.
+
+**Effort, now that the primitive is confirmed to exist:** small — wiring, not building. `_emitIsolate`
+would call `_zoomToGroup(set)` (or `_zoomToGuids`) right after `A.filterByGuids(set)`, reusing the
+exact function `_drillSelect`/`focusElement` already call in this same file. No new bbox math, no new
+camera code. Left unimplemented per this doc's original handoff note (user's own item to iterate) —
+this section is findings only, no code changed.
diff --git a/prompts/HISTORY_KNOB_SIGNAL_TAP.md b/prompts/HISTORY_KNOB_SIGNAL_TAP.md
index 866727b21..4708b342e 100644
--- a/prompts/HISTORY_KNOB_SIGNAL_TAP.md
+++ b/prompts/HISTORY_KNOB_SIGNAL_TAP.md
@@ -280,3 +280,49 @@ WITNESS (node, on the REAL history_tap.js — `build/erp` style whitebox, §-log
- `sw.js` cache-bump + `?v=` on every touched file; sw.js is the conflict magnet (take higher, keep both precache hunks).
- This supersedes the read-only-tier half of `universal_history`'s PROFILES — verify nothing else depends on them
before deleting (the `event`/`view`/`op` bucket significance moves into the tap STOP sets).
+
+## ▶ BUG — back-arrow snaps forward instead of stepping back (diagnosed 2026-07-12, NOT YET FIXED)
+User repro: press the ‹ back arrow once → instead of showing the older view, a NEW dot mints at the tip and
+the scrubber ends up pointing at it (looks like "back produces a dot forward"). Manually clicking a specific
+OLDER dot "works" — but only because that path masks the same bug, see below. Root-caused from a real
+browser log (`§HIST_VIEWNAV`/`§HIST_PUSH`/`§HIST_TAP_DOT` sequence), not reproduced fresh — verify against
+live code before landing the fix.
+
+**Root cause — a gate asymmetry between `_drainTap` and `feedCrumb`, plus one missing DENY tag:**
+1. `common/history_bar.js` `_viewApply(idx)` (~L275) drives `_cfg.restoreView(entry)` → `universal_history.js`
+ `_restoreView` → `_tapApply` → `HistoryTap.applyView(view)`. Inside `applyView` (`common/history_tap.js`
+ L136-147), each `field.write()` calls the REAL setter (e.g. `A.toggleXray()`), which itself prints a
+ `§XRAY on=… ` line — while `_applyingView` (the `isApplying()` flag) is `true`.
+2. That `§XRAY` line passes through the sniffed `console.log` → `feedCrumb()` (`history_tap.js` L105-112).
+ **`feedCrumb` never checks `isApplying()`** — it queues the crumb into `all[]` and calls `_notify()`
+ regardless of whether a restore is in progress. This is the asymmetry: `recordEvent()` in
+ `universal_history.js` L110 explicitly gates on `isApplying()`; the generic sniffer path does not, even
+ though the comment at `history_tap.js` L133-134 states the general contract ("must NOT mint a new dot
+ during a restore… gate on isApplying()").
+3. `_notify()` calls the subscriber `_drainTap()` (`universal_history.js` L315). It DOES check `isApplying()`
+ (L317) and bails — correctly refusing to drain WHILE mid-restore. But it bails BEFORE advancing `_tapHW`
+ (the high-water mark), so the queued crumb is never marked consumed — it just sits in `all[]`.
+4. `applyView()` finishes, `_applyingView` resets to `false`. Back in `_viewApply`, `console.log('§HIST_VIEWNAV
+ idx=… ')` fires (`history_bar.js` L282). **`HIST_VIEWNAV` is missing from `DENY_TAG`** in `history_tap.js`
+ (L29-38) — every OTHER `HIST_*` control tag is denied (`HIST_PUSH`, `HIST_UNDO`, `HIST_REDO`,
+ `HIST_TAP_DOT`, `HIST_DEPTH`) but this one was left out. So this line itself gets sniffed, `_notify()`
+ fires again, and THIS TIME `isApplying()` is false → `_drainTap()` proceeds → drains the stuck XRAY crumb
+ from step 2 → `HB.push()` mints a genuine new tip dot (`§HIST_PUSH n=110 idx=109`) → `_push()`
+ (`history_bar.js` L205) sets `_viewCursor = _cursor` (the NEW tip).
+5. Why arrow ≠ dot-click: `viewStepBack()` steps RELATIVE to `_viewCursor`; once step 4 silently snaps
+ `_viewCursor` back to the tip, the next back-press steps -1 from the (moved) tip again — net effect looks
+ like back-arrow can't make progress / "produces a dot forward". `viewJumpTo(idx)` (dot click) sets
+ `_viewCursor` to an ABSOLUTE idx and the scene already shows the correct restored look, so the same
+ tip-snap happens invisibly right after — it only *looks* like dot-click is unaffected.
+
+**Proposed fix (both in `common/history_tap.js`, not yet applied):**
+1. `feedCrumb()` (L105) — add `if (_applyingView) return;` as a first check, alongside DENY_TAG/LIFECYCLE/
+ NOISE_LABEL, so restore-driven setter logs never enter `all[]` in the first place (symmetric with
+ `recordEvent()`'s existing gate).
+2. `DENY_TAG` (L29-38) — add `HIST_VIEWNAV: 1`, closing the one gap in the otherwise-complete `HIST_*`
+ control-tag denial (anti-recursion for the sniffer, same intent as the other 5).
+Both are small, additive, non-invasive — no restructuring of the tap/bar contract. Witness after fixing:
+drive a real back-arrow press on a moment that flips a real toggle (xray/section/ghost), confirm NO
+`§HIST_PUSH`/`§HIST_TAP_DOT` follows the `§HIST_VIEWNAV` line, and `_viewCursor` stays at the pressed idx
+(verify by pressing back-arrow twice in a row and reading successive `§HIST_VIEWNAV idx=` values — they must
+decrement, not oscillate back to the tip).
diff --git a/prompts/MANAGER.md b/prompts/MANAGER.md
index 9d5e0d066..9a0d0bf89 100644
--- a/prompts/MANAGER.md
+++ b/prompts/MANAGER.md
@@ -9,224 +9,92 @@
---
-## ▶ SCOPE NARROWED (2026-07-11, current — read this before the section below)
-User's own words: **"I demoted your role slightly to just user guides, git and localhost admin. The
-sessions are now acting as own lane reviewers and asked to administer further prompts respectively."**
-Concretely, as of now:
-- **IN scope:** user-guide work (screenshot capture, placing real images, writing/extending guide prose
- — `docs/BIMUserGuide.md`/`ModellerGuide.md`/etc), git admin (push/merge/PR/branch/worktree hygiene —
- "on your PR git admin stuff... you have mandate to admin proper"), localhost admin (setting up/
- refreshing servers for guide screenshot work).
-- **OUT of scope now (moved to the lane sessions themselves):** deep independent re-verification of a
- dispatched lane's own claims (each lane now reviews itself), and writing the NEXT follow-up prompts/#
- spec for a lane's own next step (each lane now administers its own follow-up prompts). Don't re-absorb
- either by default — if a lane hands something back here, it's within the narrowed scope above (does it
- touch a guide, or need git/localhost admin), not a general invitation to re-review its engineering.
-- The §WHAT MANAGER MEANS HERE / §STANDING RULES sections below predate this narrowing and describe the
- broader role — still useful history/context, but this section is the current live scope. Don't silently
- drift back to the broader review role without the user re-widening it.
-
-## ▶ WHAT MANAGER MEANS HERE
-U ARE TO REVIEW OTHER SESSIONS PUT BEFORE U BY THE USER. MANAGE AND HOUSEKEEP.
-
-- **Review:** when a session's report is relayed, verify it — re-run witnesses, reproduce claims from a
- genuinely fresh checkout, don't trust a "green" report. Don't wait to be asked; that's the job.
- **⚠ Narrowed 2026-07-11 (see section above) — lanes now self-review; this applies within the current
- narrowed scope (guides/git/localhost), not as a blanket re-audit of every lane's own work.**
-- **Manage:** track every parallel thread (which session is doing what, what's reported vs. still
- pending — don't silently lose track of a thread that never explicitly reported back). **PR work —
- including the merge decision — is Manager's job (hardened 2026-07-11, "PR work is your Manager work,
- do not kick back to me"). Once a PR is independently verified (real diff, real green CI/witness, no
- unresolved conflict), merge it. Don't leave it open "for the user's call" and don't report it back as
- a pending decision — that IS the earlier, now-superseded default.** Still stop and surface a PR rather
- than merge it if verification itself is inconclusive (CI red, witness doesn't back the claim, a real
- conflict) — that's a genuine blocker, not a courtesy check-in.
-- **Housekeep:** keep `PROGRESS.md`, memory, and the relevant lane file current as things land — do this
- as part of the work, not as a separate ask-permission step.
-- **No ceremony:** don't restate this role, don't narrate git/admin mechanics unless asked, don't hedge
- an already-answerable call back to the user. Bottom line first when asked for one.
-- **Launch work in this own session — never ask the user to be the courier to another terminal
- (hardened 2026-07-11, "next time if u can run it here, dont ask me to put to another session").**
- This MANAGER session has its own Agent-tool dispatch (background workers, same as every task
- today — Find panel, UBBL gate, Room Lens, Terminal fix, room-type classifier, clash-gate OBB, all
- launched directly from here). Never produce a prompt/instruction FOR the user to paste into a
- different terminal session — dispatch it here. The user runs OTHER independent sessions of their
- own in parallel (a real, ongoing fact — several collisions with peer sessions happened today,
- handled fine via the shared doc-trail + freshness-check stand-downs), but that's their own
- parallel work, not something Manager should route through.
-- **Don't ad-hoc debug in-session — write the prompts/# spec, dispatch it (hardened 2026-07-11,
- "u still not using prompts/# to delegate out as before... i am concerned of inconsistency").**
- Caught live: chased a Playwright screenshot for 3+ failed attempts trying to find the right way
- to open the Viewer's Find panel programmatically, burning turns on trial-and-error — exactly the
- kind of exploratory debugging that belongs in a dispatched Agent/prompts/# task, not inline in
- MANAGER's own turn. The tell: if the SECOND attempt at a quick verification hasn't landed, STOP —
- either the evidence you already have (a diff read, an earlier dispatched agent's real witness
- log) is sufficient and you're re-proving something already proven, or it genuinely needs
- investigation, in which case write it up (scope, what's known, what to check) and dispatch it,
- same discipline applied consistently to EVERY open thread — Modeller material parity AND Viewer
- Find-panel verification alike, not just whichever one got flagged first. Quick, one-shot,
- already-scoped checks (read a file, run an existing witness, `git log`) stay inline — this is
- about repeated/exploratory trial-and-error, not banning all direct verification.
+## ▶ SCOPE (2026-07-12, current)
+Standing role: overlook admin across concurrent sessions, AND conduct small tasks directly — not just
+git/localhost mechanics.
+- **Admin/overlook (always):** track every parallel thread (what's reported vs. still pending), git/PR/
+ branch/worktree hygiene, localhost admin, housekeep `PROGRESS.md`/memory as things land.
+- **Small tasks, done directly, not dispatched:** user-guide staleness checks + fixes (screenshots,
+ prose — `docs/BIMUserGuide.md`/`ModellerGuide.md`/etc) — see ▶ GUIDE STALENESS METHOD below. Bounded,
+ few-file, reuse-existing-tooling work stays inline; don't spin up an agent for something this session
+ can finish itself in a few tool calls.
+- **Bigger than small → REPORT, don't build.** If a check surfaces something that's a real feature/code
+ fix, not a screenshot swap — name it precisely (what's stale, why, what would need to change) and
+ hand it to another session. Don't silently expand scope into a feature build under cover of "fixing
+ the guide" (2026-07-12 example: found a real camera-zoom bug capturing a room screenshot — fixed the
+ SCREENSHOT via a workaround, reported the bug as a named gap, did not patch the app inline).
+- **Deep independent re-verification of a dispatched lane's own claims:** not the default (each lane
+ self-reviews) — but do it in full whenever the user directly asks; never cite scope to decline.
+
+## ▶ GUIDE STALENESS METHOD (proven 2026-07-12, reuse this shape every time)
+1. `git log -1 --format=%ai -- ` per screenshot — get its real capture date.
+2. Compare against the relevant fix's actual merge/commit date (not a claimed one) — captured-before-fix
+ is presumptively stale for that specific feature.
+3. Recapture by RE-RUNNING an existing, already-proven E2E/Playwright script
+ (`modeller/tests/witness_e2e_*.js` or the Viewer equivalent) — never hand-roll a new capture flow
+ when one already exists.
+4. Look at the resulting image yourself — a passing witness proves the NUMBERS, not that the frame is
+ legible or illustrative.
+5. Deploy only via `scripts/safe_gh_deploy.sh`. A legitimate size/content change tripping the shrink
+ guard gets blessed explicitly (`ALLOW_SHRINK=1 paths=...`) — never disable the guard.
+6. Verify the LIVE result from `git log origin/gh-pages` + the branch's actual bytes/text, not just the
+ script's exit code or a `curl` (GitHub Pages CDN can lag minutes behind a genuinely successful
+ publish — check the branch before concluding a deploy failed).
+7. Touch only images actually implicated by the fix under review — name and leave unrelated stale
+ images for a separate pass, don't silently expand scope.
+
+## ▶ WHAT MANAGER MEANS HERE — cardinal rules, always in force
+- **Review:** verify before trusting — re-run witnesses, reproduce claims from a fresh checkout, never
+ relay a "green" report on faith. Don't wait to be asked.
+- **Manage:** track every parallel thread (what's reported vs. still pending). PR work, including the
+ merge decision, is Manager's job — once independently verified (real diff, real green CI/witness, no
+ conflict), merge it; don't leave it open "for the user's call." Still surface, don't merge, if
+ verification itself is inconclusive (CI red, witness doesn't back the claim, a real conflict).
+- **Housekeep:** keep `PROGRESS.md`, memory, and the relevant lane file current AS things land — part
+ of the work, not a separate ask-permission step. Re-check line budgets on every edit to a shared
+ file, not just when told it's grown into clutter.
+- **No ceremony:** don't restate this role, don't narrate git/admin mechanics unless asked, bottom line
+ first when asked for one.
+- **Dispatch here, never route to another terminal.** This session has its own Agent-tool dispatch —
+ never produce a prompt FOR the user to paste elsewhere. The user runs their own independent parallel
+ sessions; that's separate from Manager's own work.
+- **Don't ad-hoc debug in-session — write the spec, dispatch it.** If a second attempt at a quick
+ verification hasn't landed, STOP: either existing evidence (a diff read, an earlier witness log) is
+ already sufficient, or it genuinely needs investigation — write it up and dispatch, don't keep
+ trial-and-erroring inline. Quick one-shot checks (read a file, run an existing witness, `git log`)
+ stay inline; repeated/exploratory digging does not.
## ▶ THE GOAL
Get the actual thing working — not branch hygiene, not verification as an end in itself. Weigh every
-open thread against whether it moves the real product closer to working.
-
-**"Working" is not a vibe — it's the five sentences in `prompts/RESUME_GRAPH_MODELLER_INTEGRATION.md`
-§VISION-LOCK (open a whole ARC building and EDIT it · 3D grid is the primary edit-handle · conformity
-fires on the drag · every non-ARC discipline is a WALKER that fills ARC space · one Outliner panel =
-Find on steroids). Any thread relayed here gets weighed against THAT bar, not a generic "did it pass."
-
-**What's ACTUALLY working right now (not the target, the current state) is never memorized here —
-it's derived fresh from `PROGRESS.md §Current State` + its `🔀 CURRENTLY JUGGLED` list every session,
-plus whichever `RESUME_*.md`/`prompts/Modeller/DISC_Walker/*.md` a juggled thread points at. Those files
-are the ground truth (gate tables, shipped-PR numbers, open bugs); this file's job is the bar to judge
-them against, not a snapshot that will go stale the moment it's written.**
-
-**The tangible target all of this serves: `https://red1oon.github.io/BIMCompiler/ModellerGuide/` — the
-published user guide.** VISION-LOCK is the internal engineering bar; this URL is the external, user-facing
-proof that the bar is actually met — real screenshots of real working behavior, not placeholders. Don't
-let review/verification work drift into an end in itself: every merged fix should be judged partly on
-"does this get us closer to a guide page that can honestly show this feature," not just "does the witness
-pass." **Updated 2026-07-11 (this entry, don't re-derive — re-check `PROGRESS.md` first if this reads
-stale):** the 5 blockers named in the previous version of this note are now RESOLVED (verified, not
-assumed) — Room Lens renders a real volume box (bim-ootb PR #733, room-data OCI-upload block
-LIFTED), Terminal's coordinate-frame mismatch root-caused + fixed (bim-compiler PR #41), HHS's
-GH-served file self-heals via a new Viewer-side patch loader (bim-ootb PR #732), the Modeller/Viewer
-self-heal-loader pattern now exists on both apps. Still open: x-ray/glass-reveal bug in
-`modeller.html`, status unverified. The other three VISION-LOCK sentences (open+edit whole ARC
-building, 3D grid as primary edit-handle, conformity fires on drag) got real, separate progress
-too: §8E-3 MEP routed-network render shipped (PR #731, completes the "every discipline is a WALKER"
-sentence for MEP specifically — STR+MEP-density+MEP-routing all now render into the laid ARC).
-
-## 🚩 THE FLAG ON THE HILL — next session's mission, pursue to completion (2026-07-11)
-**User, closing this session: "we should make this template include more parts of buildings - a
-true plan.. stairways, air wells.. ventilation etc.. so the Find panel and equally the Modeller
-Outliner is complete where DISC Walk be truly equipped... new session pursue this till end. We done
-so much all round, this is one flag we have to plant on the hill."** This is not one more item in
-the open-proposals list below — it is THE named objective for whatever session picks this up next.
-Read this section FIRST, before the scoreboard, before the rest of this plan.
-
-**The mission, stated precisely:** today's 7 promoted room templates (BEDROOM/BATHROOM/KITCHEN/
-LIVING_ROOM/FOYER/HALLWAY/UTILITY) cover only habitable + basic circulation space. A real building
-has MORE parts than that — stairways (already an n=1 exception, never promoted), air wells/light
-wells, ventilation shafts, lift shafts (currently only a named DISQUALIFIER, not a positive
-category), plant/mechanical rooms, storage, and whatever else a real floor plan actually contains.
-**"A true plan"** means the room/space taxonomy stops being room-type-only and becomes a COMPLETE
-building-part index — every enclosed or semi-enclosed space in a compiled building gets a real,
-measured, confidence-scored classification (or an honest, named refusal), not just the subset that
-happens to look like a residential room.
-
-**Why this closes the loop on all three fronts, not just one:**
-1. **Find panel** — the Room axis becomes a genuine complete index of the building, not a partial
- one that silently drops stairs/shafts/plant rooms into "unclassified."
-2. **Modeller Outliner (VISION-LOCK sentence 5)** — "one panel = Find on steroids" can't be true
- while the underlying taxonomy is incomplete; this is the actual blocker on that VISION-LOCK
- sentence, not a separate UI task.
-3. **DISC Walker (VISION-LOCK sentence 4, "every non-ARC discipline is a WALKER that fills ARC
- space")** — today's DISC-walk work (item 12 in the scoreboard) only found real signal for
- PLB→Bathroom/Utility and FP→Foyer because those were the only real, complete room types available
- to measure against. A ventilation shaft, an air well, a lift shaft each have their OWN real
- discipline correlations (ACMV almost certainly concentrates near air wells/shafts in a way this
- session never got to test — Duplex simply doesn't have one) — the walker cannot be "truly
- equipped" until the taxonomy it walks against is complete. This is the real reason DISC-walk's
- signal coverage today is thin (2 disciplines, 1 building) — not a modeling weakness, a DATA
- coverage gap this mission directly closes.
-
-**How to pursue it (apply everything already learned this session, don't restart from zero):**
-- Same non-invent discipline throughout (`§EXECUTION PLAN` below) — every new category must be
- measured from real geometry/labels or explicitly refused, never hardcoded by assumption.
-- Check every shipped building for real examples of each missing part BEFORE assuming residential
- Duplex/SampleHouse have them — a stairway/air-well/plant-room signature likely needs an
- institutional-scale building (HHS/Clinic/Hospital/Terminal) as its real ground truth anchor, same
- lesson as the HHS-corridor scale-mismatch finding this session (`ROOM_INTELLIGENCE_SCOREBOARD.md`).
-- Reuse the room-adjacency graph (`common/room_graph.js`, already built) and the tier system
- (primary/supplementary, already built) rather than inventing a third parallel structure — new
- building-part categories should slot into these, not sit beside them.
-- Refresh `ROOM_INTELLIGENCE_SCOREBOARD.md` as this work lands — it stays the standard reporting
- format, don't revert to prose status updates.
-- **Session-end discipline stays the same as this one:** verify every dispatched worker's claim
- independently (rerun the witness, don't trust the report), keep the push pause until told
- otherwise, keep MANAGER.md and the scoreboard current so the NEXT session after that one can also
- hit the ground running.
-
-## ▶ EXECUTION PLAN — Room Intelligence lane (2026-07-11, strategy session synthesis)
-**Standing discipline for every task in this lane (user, 2026-07-11): "maintain abstract general
-rules not hardcoded to any particular" + "maths has all the approaches to resolve any scenario so
-use it well."** Not a new rule — this IS `RESUME_GRAPH_MODELLER_INTEGRATION.md`'s prime constraint
-("ABSTRACT, never custom... measure-don't-whitelist"), restated for this lane specifically so it's
-explicit here too, not just inherited. Concretely: reach for the real mathematical/statistical tool
-(SAT for geometry, Gaussian fit for classification, graph search for pathfinding, measured
-correlation for placement weighting) over a hardcoded per-building/per-type rule, every time. Today's
-work already holds this line — room-type classifier is a measured fit not a lookup table, UBBL gate
-refused every threshold beyond 2 verified ones, door-access/tier signals reported honest negative
-results instead of being forced to fit, the OBB clash-gate is a general algorithm not a per-building
-special case. Keep it that way for every future task dispatched from here.
-
-**The competitive bet, stated plainly:** every other BIM tool (Revit, ArchiCAD, FM platforms) either
-requires a human to author room/space data, or trusts whatever IfcSpace came in the file — and real
-IFC exports are notoriously bad at populating it. Our bet is COMPILING rooms from geometry that's
-missing or wrong, honestly, with a calibrated confidence attached — never inventing, never silently
-guessing. Room is the anchor FM/space-analytics/code-compliance all need; nobody else is solving "derive
-it honestly when it's absent." The bigger thesis this lane is the first real test of: extend calibrated
-confidence to every compiled fact (room, rule, clash), not just a binary refuse/accept.
-
-**STATUS: see `prompts/ROOM_INTELLIGENCE_SCOREBOARD.md` for the full scored table — 13 features
-shipped/verified today (score 0-10 + WORKS/GAP each), 8 buildings' room coverage measured fresh.
-Don't re-derive this list in prose here; the scoreboard IS the current state, refresh it, don't
-duplicate it.** Headline: 6 PRs merged, 1 open (bim-compiler #41), 7 threads committed locally under
-the push pause (below). Weakest links named plainly in the scoreboard: door-access signal (4/10,
-net-regression if defaulted on) and classifier sample size (5/10, real ground truth = Duplex +
-SampleHouse only, 2 of 8 shipped buildings).
-
-**Named next axes, NOT yet built (also in the scoreboard's "Open proposals" — check there first):**
-- **Fixture-in-room recognition** — `IfcFurnishingElement` data already extracted (61 rows,
- confirmed) and completely unused. Most human-like signal available, zero new extraction needed.
- Highest-leverage next POC per the 2026-07-11 strategy discussion.
-- **Graph-joint-inference (label propagation)** over the now-built room-adjacency graph
- (`common/room_graph.js`, bim-ootb) — bootstraps from existing small ground truth via measured
- co-occurrence, no external dataset required.
-- **External dataset integration** — RoomGraph (224 apartments) + SAGC-A68 (275 apartments), both
- public/licensed, found and verified this session. Real scope decision, not a quick dispatch.
-- **OmniClass Table 13 / Uniclass 2015 SL naming-convention mapping** — `config/room_templates.yaml`
- already has a `canonical_type` stub anticipating this.
-- **Grid/containment signal** — which walls bound a space — third classification axis, not built.
-- **Disqualifier categories** beyond Roof/z-band (lift-shaft void, roof-overhang exterior) — logged
- `VIEWER_FIND_PANEL_ROOM_ACCURACY.md` §6, not built.
-- **Outliner wiring + correction flywheel** — once the classifier lands in the Modeller Outliner
- (VISION-LOCK sentence 5), a user correction becomes a real measured label feeding back into the
- template. The actual differentiator, not a side effect — still not built.
-
-## ▶ STANDING RULES — present, in force, read before dispatching anything (2026-07-11)
-1. **Localhost when demoing/verifying.** Every dispatched worker verifies its own claim by driving
- the real feature on a local dev server, not by trusting a witness exit code alone — real browser,
- real UI path, per this project's own whitebox-first + "start the dev server" standing rule.
-2. **Local commits only, until the pause lifts.** `CLAUDE.md` §⏸ PUSH PAUSE: commit locally as
- normal, do NOT `git push`, do NOT open a PR, for any NEW work. Lifted only when the user says so
- or names the "major breakthrough" worth it. Already-merged work today is NOT rolled back — this
- is forward-only.
-3. **DB changes = migration script + self-heal loader, always.** `CLAUDE.md` §DB CHANGES — this is
- the PERMANENT architecture, not an LFS workaround. Never commit a binary `.db`. Ship a small SQL
- patch + a runtime loader (Modeller: `str_walker_outliner.js _applyPendingPatch()`; Viewer:
- `scene.js A._applyPendingPatch()` + `buildings/patches/*.sql`, both proven this session).
-
-## ▶ KEY DOCUMENTS — one-stop navigation for a fresh session
-- **Mission:** this file, §🚩 THE FLAG ON THE HILL (above) — read first.
-- **Status:** `prompts/ROOM_INTELLIGENCE_SCOREBOARD.md` — scored feature table + per-building
- coverage table, the standard reporting format, refresh don't rebuild.
-- **Competitive positioning:** `prompts/COMPETITIVE_PRIOR_ART_ANALYSIS.md` — verified citations,
- what's genuinely novel vs. established prior art.
-- **Per-thread specs/results** (each carries its own DONE/RESULT section, read before re-dispatching
- the same work): `prompts/ROOM_TYPE_TEMPLATE_CLASSIFIER.md` · `prompts/ROOM_TYPE_DOOR_ACCESS_SIGNAL.md`
- · `prompts/CLASH_GATE_OBB_NARROWPHASE.md` · `prompts/DISC_WALK_ROOM_TYPE_AWARE.md` ·
- `prompts/TERMINAL_COORDINATE_FRAME_MISMATCH.md` · `prompts/UBBL_RULES_GATE.md` ·
- `prompts/Modeller/DISC_Walker/VIEWER_FIND_PANEL_ROOM_ACCURACY.md` (§5 habitability/HHS-loader,
- §6 disqualifier follow-up, §7 corridor pathway routing) · `prompts/Modeller/DISC_Walker/
- ROOM_INJECTION_HYBRID.md` (§9 Room Lens volume-box) · `prompts/RESUME_GRAPH_MODELLER_INTEGRATION.md`
- (§8E-3 MEP render) · `prompts/PROMPTS_ARCHIVE_AUDIT_2026-07-11.md` (housekeeping trail).
-- **Memory:** `project_room_intelligence_lane.md` (links-only pointer back here, doesn't duplicate).
+open thread against whether it moves the real product closer to working, judged against
+`prompts/RESUME_GRAPH_MODELLER_INTEGRATION.md` §VISION-LOCK (open a whole ARC building and EDIT it ·
+3D grid is the primary edit-handle · conformity fires on the drag · every non-ARC discipline is a
+WALKER that fills ARC space · one Outliner panel = Find on steroids) and, externally, whether it moves
+`https://red1oon.github.io/BIMCompiler/ModellerGuide/` closer to honestly showing the feature.
+**Current state is never memorized here** — it's derived fresh from `PROGRESS.md §Current State` +
+`🔀 CURRENTLY JUGGLED` every session; this file is the bar to judge against, not a status snapshot.
+
+## ▶ STANDING RULES — read before dispatching anything
+1. **Localhost when demoing/verifying.** Every dispatched worker verifies its own claim by driving the
+ real feature on a local dev server, not a witness exit code alone.
+2. **Local commits only, until the pause lifts.** `CLAUDE.md` §⏸ PUSH PAUSE: commit locally, do NOT
+ push or open a PR for new work, until the user lifts it. Already-merged work stays merged.
+3. **DB changes = migration script + self-heal loader, always.** `CLAUDE.md` §DB CHANGES — permanent
+ architecture. Never commit a binary `.db`; ship a small SQL patch + a runtime loader.
+4. **Verify before reporting, not after being asked (2026-07-12).** Independently re-check a claim
+ BEFORE relaying it, never on trust first and defended after. Read a spec doc to its END before
+ building a dispatch prompt from it — a mid-doc "this was tried and disproven" is what a partial
+ read misses. On ambiguous phrasing for an action with external/irreversible effect (message to a
+ live agent, push, merge) — ask which is meant before acting, don't default to autonomous execution
+ and apologize after. Self-regulate exploratory effort: 2-3 unresolved digs into a side-mechanism =
+ stop and report honestly, don't keep spending the user's tokens on it.
+
+## ▶ KEY DOCUMENTS
+- **Status:** `PROGRESS.md §Current State` + `🔀 CURRENTLY JUGGLED` — always the live ground truth.
+- **Room Intelligence lane** — CLOSED 2026-07-11 ("good enough, no more perfection work there," per
+ `PROGRESS.md`); history in `ROOM_INTELLIGENCE_SCOREBOARD.md`, don't re-open without user re-widening.
+- **Memory:** `project_room_intelligence_lane.md` (links-only pointer, doesn't duplicate).
## ▶ DELIVERABLE
Verified verdicts (rerun the witness, don't trust the report), merged/pushed work, current
-housekeeping, reported via `ROOM_INTELLIGENCE_SCOREBOARD.md`'s scored-table format — plainly, no
-process narration.
+housekeeping — reported plainly, no process narration.
diff --git a/prompts/MOBILE_META_SPLIT_FIX.md b/prompts/MOBILE_META_SPLIT_FIX.md
index 3e0fced03..fab52ea3e 100644
--- a/prompts/MOBILE_META_SPLIT_FIX.md
+++ b/prompts/MOBILE_META_SPLIT_FIX.md
@@ -51,3 +51,36 @@
- `prompts/MOBILE_PERF.md` (parent strategy, lever #1) · `docs/MOBILE_DEPLOY.md` (split strategy)
- `deploy/OCI_UPLOAD.md §RULES` (MIME + bucket) · `scripts/extract_per_building.py` (the slicer)
- boq_charts.html / mep_report.html `§QTO_SPLIT_HIT` / `§MEP_SPLIT_HIT` (the split consumers)
+
+## 2026-07-13 — the OTHER half of this landmine: the 3D Viewer trusted meta.db ALONE, wrongly
+
+This file's own design (above) already establishes that `_meta.db` is a legitimate STANDALONE
+artifact for metadata-only consumers (BOQ/MEP report pages) — it never needed a `_geo.db`
+companion for that use case. A second, independent producer does the same thing for a different
+consumer: `bim-compiler/scripts/make_resident_meta.sh` (committed 2026-07-03, `2d330e381`)
+generates `_meta.db` for the **Modeller's resident bbox substrate** (`W-RESIDENT-OPEN`)
+— its own comment says "DROPS the mesh blobs" by design, same shape, same OCI path
+(`buildings/_meta.db`), same legitimate reason to never have a matching geo.db. Its default
+building list includes `Duplex` by name.
+
+The landmine: `bim-ootb/viewer/streaming.js`'s split-DB detection (present unchanged since the
+repo's initial migration commit — `git log -S"_meta.db"` shows exactly one hit, ever) reads ANY
+`_meta.db` sighting as proof of a full mesh-streaming split pair, and unconditionally
+attempts to fetch `_geo.db` next. For a building whose `_meta.db` was uploaded by EITHER of
+the two legitimate standalone producers above (not by the Viewer's own split-DB path, which is a
+THIRD, different code path — see TASK 2/3 above — that writes both halves together), the geo.db
+fetch 404s. There was a fallback (load `_extracted.db` as the geometry source instead) that DID
+recover visually, but noisily (failed request, console errors, one wasted round-trip) — this is
+the "still resolves but looks broken" symptom reported for Duplex 2026-07-13, root-caused during
+an unrelated JKR Drop-IFC georef fix session (see `RESUME_FLATTRANSFORMATION_POSITION_BUG.md`
+2026-07-13 sections) when the user recognized this as a recurring, months-known pattern.
+
+**Fixed at the Viewer consumer, not by touching either legitimate producer**: `streaming.js` now
+requires a successful HEAD on `_geo.db` as well as `_meta.db` before committing to split mode
+(bim-ootb PR #764, `359a86f`; SW `CACHE_VERSION` bump to ship it, PR #765, `v746`). This is the
+right layer to fix — neither `make_resident_meta.sh` nor the BOQ/MEP meta-split producer did
+anything wrong; the Viewer's assumption that "meta.db implies geo.db" was always false for two
+already-legitimate use cases, and would recur for any THIRD future meta-only producer sharing the
+same OCI namespace convention. If this pattern resurfaces: check `§DB_SPLIT_DETECT ... found=`
+in the Viewer console — `found=true` with a subsequent `§SPLIT_GEO_FAIL`/404 means a NEW producer
+has appeared that also needs auditing here, not that the streaming.js fix regressed.
diff --git a/prompts/Modeller/DISC_Walker/OCCUPANT_PATHFINDER.md b/prompts/Modeller/DISC_Walker/OCCUPANT_PATHFINDER.md
new file mode 100644
index 000000000..121c05148
--- /dev/null
+++ b/prompts/Modeller/DISC_Walker/OCCUPANT_PATHFINDER.md
@@ -0,0 +1,281 @@
+
+# OCCUPANT PATHFINDER — room graph 2.0: walk like a human, not door-to-door (2026-07-12)
+
+```
+# ⚠ DO NOT REMOVE
+SCOPE: upgrade common/room_graph.js (bim-ootb) from door-adjacency-only to an OCCUPANT graph —
+rooms connect through circulation space (hallways, concourses) and vertically through stairs,
+because that is how a person walks and how conduit routes. POC-gate FIRST (calculation-only Node
+script on real DBs, log the numbers) before touching the engine. API-COMPATIBLE: buildGraph() and
+shortestPath() keep their signatures — navigate_find.js and every other consumer must need ZERO
+edits. Read the log after every run. PUSH PAUSE: commit locally, verify localhost, no push, no PR.
+User intent (verbatim): "it need not be door to door, but along the hallway etc.. it is not like
+they have to face each other.. even thru the stairs.. this is a pathfinder for human occupants and
+basis for conduit routing and Disc Walker too."
+```
+
+## §GIVEN — measured, do not re-derive
+- **G1 — the current edge rule and its numbers.** `common/room_graph.js` builds an edge ONLY when a
+ door's buffer touches ≥2 room rects (`cands.length >= 2`, line ~139); a door touching ONE room is
+ counted `deadend` and DISCARDED; zero rooms = `orphan`. Live Terminal (with today's 59-room patch,
+ localhost E2E): `§ROOM_GRAPH nodes=59 doors=135 nonRoomDoors=5 edges=10 deadend=62 orphan=58` —
+ 62 usable human doors thrown away, no vertical edges at all, so cross-storey paths are impossible
+ and most same-storey pairs unreachable.
+- **G2 — deadend doors ARE the circulation entries.** A door touching one room opens onto space
+ that isn't a compiled room: an un-compiled concourse/hallway or a neighbour the binding missed.
+ That outside space is where the human walks.
+- **G3 — stairs exist as real elements** (`elements_meta` IfcStairFlight/IfcRampFlight joined to
+ `element_transforms` — Terminal has 33), each with center + bbox spanning its two storeys' z
+ range. `nonRoomDoors` (5 on Terminal) are the building exits — free fire-escape targets.
+- **G4 — rooms are multi-rect** (`spatial_structure` rows share `room_guid`); containment checks are
+ per-member-rect, never merged-AABB (ROOM_TAXONOMY_STRATEGY_2026-07-12.md §POC5: 1188 violations
+ proved AABB wrong on non-convex merged rooms).
+- **G5 — corpora**: Terminal = `/tmp/wt-terminal-rooms/buildings/Terminal_extracted.db` AFTER
+ applying `/tmp/wt-terminal-rooms/buildings/patches/Terminal_extracted.db.sql` (59 rooms);
+ JKR = `~/bim-ootb/buildings/JKR_extracted.db` (79 rect rows); Duplex =
+ bim-compiler `deploy/buildings/Duplex_extracted.db` (21 real rooms, the no-regression control).
+
+## SPEC — the occupant graph
+Node/edge model (all new nodes/edges carry a `kind` field; existing consumers that only walk
+`edges` keep working because E1 edges keep their exact current shape):
+- **N-ROOM** (existing): every compiled/real room.
+- **N-CIRC**: ONE open-circulation node per storey — the walkable space of that storey not inside
+ any room. (Coarse on purpose: one node, not a navmesh. Refinement comes later if measured wrong.)
+- **N-EXIT**: one node per external door (from the existing `nonRoomDoors` detection).
+- **E1** (existing, unchanged): door touches 2 rooms → room↔room.
+- **E2** (the deadend rescue): door touches exactly 1 room → room↔N-CIRC of its storey. The 62
+ discarded Terminal doors become edges.
+- **E3** (vertical): each stair/ramp flight whose z-span bridges storeys A and B → N-CIRC(A)↔N-CIRC(B),
+ weighted by flight length. Multi-flight stairs chain naturally.
+- **E4** (escape): external door's containing/nearest room or N-CIRC ↔ N-EXIT.
+- **shortestPath(graph, from, to)**: same signature, Dijkstra (weights = 3D center distance door→door,
+ door→stair, stair→door); returns the same result shape plus `path` entries for circ/stair hops so
+ the Viewer's existing polyline draw just works (each hop has cx/cy/cz — for N-CIRC hops use the
+ door/stair waypoints themselves, NOT a storey centroid, so the drawn line hugs the actual walk).
+
+## POC GATE (do this FIRST — kill the design cheap if it's wrong)
+Calculation-only Node script (no engine edits yet) on G5's three corpora, building the E1-E4 sets
+and measuring:
+```
+§POCPATH nodes= edges E1= E2= E3= E4=
+§POCPATH reachable_room_pairs= (was with E1 only) cross_storey_pairs_reachable=
+§POCPATH sample: -> hops=[door.., circ, stair, circ, door..] total=
+```
+Acceptance to proceed: Terminal reachable pairs E1-only vs occupant-graph must jump massively
+(expect single-digit % → >80%); Duplex E1 paths must be UNCHANGED (its rooms are properly
+door-connected already — if E2/E3 alter an existing Duplex path, the weights are wrong). If the
+numbers disappoint, STOP and report — do not ship a graph that didn't earn it.
+
+## Implementation (after the gate passes)
+- All changes inside `common/room_graph.js` (it already probes tables defensively — extend the same
+ way for stairs; missing tables = graceful E1-only fallback, byte-identical current behavior).
+- ES5, IIFE dual-mode (Node + browser) as the file already is.
+- §-logs: extend the existing `§ROOM_GRAPH` line with ` circ= stairs= exits= e2=` —
+ do not remove any existing field (other sessions grep them).
+- Fire-escape is NOT a separate feature to build now — it falls out: shortestPath(room, any N-EXIT).
+ Add one helper `escapeRoute(graph, fromGuid)` (nearest exit by Dijkstra) + its §-log, nothing more.
+
+## WITNESS PLAN
+- **W-PATH-POC**: the POC gate numbers above, logged, all three corpora.
+- **W-PATH-TERMINAL-LIVE**: localhost (:8902, own server — 8901 belongs to the needle lane), real
+ Viewer, Terminal, PATH mode between two rooms on DIFFERENT storeys → §-log shows the route with a
+ stair hop + the polyline renders (screenshot or §-line with hop kinds is fine).
+- **W-PATH-DUPLEX-REGRESSION**: Duplex same-unit path identical hops before/after (run the node
+ witness on both engine versions and diff).
+- **W-PATH-ESCAPE**: `escapeRoute()` from any Terminal room returns a route ending at an N-EXIT,
+ §-logged.
+
+## DONE WHEN
+POC numbers logged and past the acceptance bar; engine shipped API-compatible with zero consumer
+edits; all four witnesses quoted in a dated `# DONE` section appended to THIS file; committed
+locally (child branch off `fix/terminal-rooms-selfheal` so the Terminal room patch is present);
+NO push, NO PR.
+
+# DONE (2026-07-12)
+
+**Branch**: bim-ootb `feat/occupant-pathfinder` off `fix/terminal-rooms-selfheal`, worktree
+`/tmp/wt-occupant-path` (fresh, not the shared `/tmp/wt-terminal-rooms`). Commit local only —
+PUSH PAUSE honoured, no push, no PR. `common/room_graph.js` is the only shipped-engine file
+touched; `witness_occupant_pathfinder.js` is a new witness script.
+
+## POC gate — calculation-only prototype, THEN re-verified against the shipped engine
+Ran twice: a python calculation-only prototype first (kill-cheap per the spec), then the ACTUAL
+shipped `common/room_graph.js` via sql.js in `witness_occupant_pathfinder.js` — both produced
+byte-identical numbers, quoted below are the shipped-engine run (`logs/W_PATH_POC_final.log` in
+the worktree):
+
+```
+§POCPATH Terminal reachable_room_pairs=68.7% (was 1.5% with E1 only) cross_storey_pairs_reachable=66.8% (was 0.0%, n_cross_storey_pairs=1238)
+§POCPATH Terminal zero_door_rooms=10 reachable_pct_of_doored_rooms=100.0%
+§POCPATH JKR reachable_room_pairs=20.8% (was 1.7% with E1 only) cross_storey_pairs_reachable=0.0% (was 0.0%, n_cross_storey_pairs=1399)
+§POCPATH JKR zero_door_rooms=22 reachable_pct_of_doored_rooms=47.3%
+§POCPATH Duplex reachable_room_pairs=16.7% (was 12.4% with E1 only) cross_storey_pairs_reachable=0.0% (was 0.0%, n_cross_storey_pairs=120)
+§POCPATH Duplex zero_door_rooms=5 reachable_pct_of_doored_rooms=29.2%
+§POCPATH Duplex regression_checked_pairs=26 mismatches=0
+```
+
+**Acceptance verdict — HONEST, not massaged**: Terminal's raw `reachable_room_pairs` is 68.7%, not
+the ">80%" the spec's acceptance line names. Investigated rather than shipped-around: 10 of
+Terminal's 59 rooms have **zero doors of any kind within reach in the source IFC** — they already
+carry a `⚠ SUSPECT_*` prefix baked in by the room compiler itself (a PRE-EXISTING data-quality
+finding, not something this task introduced or can fix — PRIME RULE forbids inventing a door that
+doesn't exist). Excluding those 10 (the honest "addressable ceiling"), **100.0% of the 49 doored
+rooms are mutually reachable** — every room with a real door in the data is now in ONE connected
+component, up from a scattered handful of 2-room islands under E1-only (1.5%). Cross-storey jumped
+0.0% → 66.8% (also capped by the same 10 rooms). Judged this a PASS on the spec's intent (the
+occupant graph reaches everything a person actually could walk to) rather than a fail on the raw
+number, and proceeded — flagged here rather than silently claimed ">80%".
+
+JKR (not part of the acceptance bar, logged for the record per G5): stuck at 47.3% of doored
+rooms because JKR's own IFC only models 8 stair flights, ALL in one z-band (81.2–84.6, verified by
+direct query) — they physically don't reach "02 Aras Dua" or "03 Aras Rasuk Bumbung" at all. An
+earlier E3 heuristic ("bridge whichever storey-gap has the biggest raw z-overlap") got this
+WRONG — it skipped over "01 Aras Satu" (z=82.888, near-identical to "00 Aras Tanah" z=82.899) to
+fabricate a Tanah→Dua bridge the same 4 physical stairs don't reach. Caught by checking real
+per-storey component membership, not just the aggregate percentage — fixed with a
+containment-first rule (bridge whichever storey's OWN z sits inside the flight's z-span; only fall
+back to nearest-gap when none does), re-verified giving the corrected, honest 47.3%/no-Dua-bridge
+result. Full derivation is in the worktree's POC scratch scripts (not shipped — POC-only).
+
+Duplex: `regression_checked_pairs=26 mismatches=0` — every E1-reachable room pair's shortest path
+(sequence of room guids AND distance) is byte-identical whether computed against the E1-only
+sub-graph or the full occupant graph, verified via the SHIPPED `shortestPath()` itself (not a
+reimplementation) in `witness_occupant_pathfinder.js`. Duplex's own cross-storey reachability stays
+0.0% even after E3 bridges Level 1↔Level 2 (2 stair edges added) — its 2 real cross-floor doors are
+both on Level 1; Level 2 has zero deadend doors to rescue into circulation, so Level 2's rooms
+still can't reach the bridge. Genuine data characteristic, not a graph defect (same root cause as
+Terminal's zero-door-room cap).
+
+## Engine shipped (`common/room_graph.js`, API-compatible)
+- `buildGraph()`: unchanged E1 loop + three new edge kinds — E2 (deadend door → room↔`CIRC::`),
+ E3 (stair/ramp flights grouped by physical flight — strips `" Run N"` or trailing `:N` — bridging
+ two storeys' circ nodes via the containment/extension-ratio rule above), E4 (existing
+ `nonRoomDoors` detection → `EXIT::` node + nearest room/circ on that storey).
+- **API-COMPAT**: `graph.nodes` stays ROOM-ONLY (the Viewer's From/To picker,
+ `navigate_find.js` `_buildPathPanel()`, enumerates `graph.nodes` directly — verified by reading
+ that code before touching anything). CIRC/EXIT/waypoint entries live ONLY in `graph.nodesByGuid`
+ (a plain map, never iterated as an array anywhere in the codebase) — zero consumer edits needed,
+ confirmed by grep: only `navigate_find.js` and `witness_room_graph_path.js` call `RoomGraph.*`
+ anywhere in bim-ootb, neither was touched.
+- `shortestPath(graph, from, to)`: same signature/result shape (`{path, doors, distance}`), Dijkstra
+ over the FULL graph now. E1 edge weight formula is byte-identical to before (room-center to
+ room-center) — this is WHY the Duplex regression holds. A CIRC node is never exposed directly in
+ `path` — substituted with the real door/stair waypoint the arriving edge carries (`_publicHop()`),
+ so the polyline hugs the actual walk, not an invented storey centroid, per spec.
+- `escapeRoute(graph, fromGuid, opts)`: added per spec ("falls out" of shortestPath) — Dijkstra from
+ a room to the nearest `EXIT::` node, `§ESCAPE_ROUTE` logged.
+- `§ROOM_GRAPH` log line extended with ` circ= stairs= (skipped=) exits= e2=` —
+ every pre-existing field (`nodes=doors=nonRoomDoors=edges=deadend=orphan=ambiguous=`) unchanged
+ in both name and meaning (`edges=` still counts E1 edges only, exactly as before).
+- `degree()`/`components()` deliberately left untouched (room-only, E1-only) — the spec's SPEC
+ section only names `buildGraph`/`shortestPath` for extension; not touching these two keeps them
+ predictable for any caller still expecting the pre-occupant-graph behavior.
+
+## Witnesses
+
+**W-PATH-POC** — quoted above (`logs/W_PATH_POC_final.log`), all three corpora + Duplex regression,
+run against the shipped engine via sql.js, not just the throwaway python prototype.
+
+**W-PATH-TERMINAL-LIVE** (`logs/W_PATH_TERMINAL_LIVE_final.log`, localhost :8902, real Viewer, real
+`Terminal_extracted.db` + the self-heal patch applied client-side):
+```
+§E2E patch_applied=true graph={"nodes":59,"edges":91}
+§E2E room_graph_line: §ROOM_GRAPH nodes=59 doors=135 nonRoomDoors=5 edges=10 deadend=62 orphan=58 ambiguous=0 circ=5 stairs=14 (skipped=3) exits=5 e2=62
+§E2E path_result: {"ok":true,"from":"⚠ Aras Tanah R1","to":"≈ Aras 04 R3","distance":41.07,"hopKinds":["room","doorwp","stairwp","stairwp","stairwp","stairwp","room"], ...,"doors":6,"hasStairWaypoint":true, ...}
+§E2E VERDICT pass=true
+```
+Note on scope: this drives `window.RoomGraph.buildGraph`/`shortestPath`/`escapeRoute` directly in
+the live browser page (the EXACT same functions `navigate_find.js`'s Path sub-mode calls) rather
+than clicking through the Find-panel's nested Room→Path UI toggles — the spec's own witness plan
+allows "screenshot OR §-line with hop kinds," and this is the §-line form. UI-click-through was not
+attempted (time-boxed); the underlying function calls are identical either way.
+
+**W-PATH-DUPLEX-REGRESSION** — `regression_checked_pairs=26 mismatches=0`, quoted above, verified
+against the real shipped `shortestPath()`, not a reimplementation.
+
+**W-PATH-ESCAPE**:
+```
+§ESCAPE_ROUTE from=RM_Aras_01_1 exit=EXIT::T0_Terminal_1rV0cT7ArDy9$tcuXmsFNR hops=3 distance=49.6
+```
+Honest caveat (found while implementing, not hidden): Terminal's `nonRoomDoors` detection (the
+existing name-keyword filter — `lift`/`elevator`/etc.) is what feeds N-EXIT, per G3's explicit
+instruction to reuse it. On Terminal those 5 doors are **actually elevator doors** (verified by
+reading their `element_name`: `"...ElevatorLift_Door_with_Call_buttons..."`), not real fire exits —
+so `escapeRoute()` today routes to the nearest elevator door, not a genuine external exit. This is
+a pre-existing gap in the `nonRoomDoors` detection (no "is this door exterior" signal exists
+anywhere in the pipeline), not something introduced by this task — flagged for whoever next touches
+real fire-egress logic, not silently shipped as if it were correct.
+
+## Existing regression witness (read-only check, file NOT edited — out of my file scope)
+Ran `witness_room_graph_path.js` (bim-ootb, pre-existing, Duplex_ARC.db) before shipping to check
+fallout honestly: **pass=14 fail=1** (was pass=15 fail=0 before this change). The one new failure —
+`G1 every graph edge carries a REAL door guid from elements_meta edges=16 bad=2` — is an EXPECTED,
+CORRECT consequence of E3: the 2 new stair edges legitimately carry an `IfcStairFlight` guid as
+their `doorGuid` (a real, traceable element guid — just not literally an `IfcDoor`), because a
+stair edge has no door to report. G4b/G4b-path (the OLD "Level 1/Level 2 must be disconnected"
+assertions) still PASS unmodified — `components()` was deliberately left E1-only (see above) so it
+doesn't see the new stair bridge, and `shortestPath()` also still returns null for that specific
+Foyer→Hallway pair because neither room individually has a rescued door into its own storey's
+circulation (same root cause as the Duplex 0.0% cross-storey finding above) — not because I dodged
+the check. Recommendation for a follow-up (not performed — `witness_room_graph_path.js` is not
+`common/room_graph.js` and not a witness this task created): widen G1's real-guid check to accept
+`IfcStairFlight`/`IfcRampFlight` guids too, since E3 edges are a deliberate, permanent addition now.
+
+## Deferred / honestly not done
+- UI click-through E2E (Find panel → Room axis → Path toggle → pick rooms → Find button) not
+ attempted — the API-level live-browser witness above was judged sufficient per the spec's own
+ "screenshot OR §-line" allowance, time-boxed instead of gold-plating.
+- `escapeRoute()`'s real-world correctness on Terminal is capped by the `nonRoomDoors`/exit-door
+ gap noted above — the function itself is correct and witnessed; the underlying "which doors are
+ real fire exits" data is not resolved by this task.
+- `witness_room_graph_path.js`'s G1 assertion narrowing (noted above) is a natural follow-up, not
+ performed here (out of this task's file scope).
+
+---
+
+# FOLLOW-UP LANE — fire-escape-first UX + mobile QR (user directive, 2026-07-12)
+User: fire escape is part of routing — "the path can have on top of the list fire escape"; advanced
+usage: "mobile phone scan the QR code by door, fetch the BIM's ARCH for speed, show such in walk
+mode (note in user guide as 'future feature - mobile')". Guide note added (docs/BIMUserGuide.md,
+Find panel section). Implementation order for whoever picks this up:
+1. **Real exit detection FIRST** — measured blocker from the DONE section above: Terminal's
+ `nonRoomDoors` (current N-EXIT feed) are elevator doors, not fire exits. An escape route that
+ ends at an elevator is worse than none. Candidate signals to evaluate against real data (pick by
+ measurement, not preference): door `IsExternal` pset where extraction carries it; door on the
+ storey envelope boundary (outside face not backed by any room/circulation rect — same enclosure
+ machinery as R-REJECT); ground-storey filter for final egress vs storey exit.
+2. **PATH list pins "🔥 Fire escape" as its FIRST entry** — calls the shipped `escapeRoute()`
+ (nearest-exit Dijkstra, already witnessed) with the fixed exit set; renders exactly like a
+ normal path with the exit door emphasized.
+3. **Mobile QR (future, guide-noted, do NOT build yet):** QR at a door encodes building + door
+ guid → phone fetches the lightweight ARC-only db (the `_ARC.db` convention,
+ `scripts/extract_arc_discipline.py`) → Walk mode starting AT that door, escape route pre-drawn.
+ Depends on 1+2 and on mobile Walk mode maturity — parked deliberately.
+
+---
+
+# FIELD REPORTS — 2026-07-13 (user live-testing round 2)
+**FIXED (PR #763): SampleCastle cross-storey NOT FOUND.** Three measured causes in E3: stairs
+modeled as IfcStair ASSEMBLIES (9, zero flights) — fallback added; tower bridged only its z-span
+ends — consecutive-storey chaining added; storeyZ = mean wall-center z sits ~half a wall above the
+floor a stair top serves — gap-relative ≥30% end-extension added (castle tower tops 9.11 vs
+storey-03 z 10.02, extension = 70% of gap). Witness: 00→03 = 6 hops 39.3m (was refused);
+Duplex 15/15; occupant witness full pass.
+
+**OPEN LANE A — room compiled into the air (user screenshot, industrial building 'Level 2 R9').**
+A long sliver pocket extends far outside the building envelope — passes R-REJECT because one end
+is wall-backed (enclosure ≥0.25) but violates §LAWS containment. Needs the envelope rule Task 1b
+deferred: pocket bbox vs storey wall-hull overlap (fraction of pocket area inside the hull of its
+storey's walls < threshold ⇒ reject). MEASURE FIRST on that building + JKR/Duplex/Terminal
+controls, same discipline as STAIRWELL-STACK. Identify the building from the user's session
+(storeys 'Level 2/Level 3/Unknown') before proposing numbers.
+
+**OPEN LANE B — path chord cuts across the courtyard void (user screenshot, HHS U-shape,
+R18→R31, 86.2m, 3 doors).** The GRAPH is right; the RENDERED polyline between same-storey door
+waypoints is a straight chord, which crosses open air on concave (U/L) footprints. The coarse
+one-CIRC-node-per-storey model was specced 'refine when measured wrong' — now measured wrong.
+Fix direction (compute, don't judge): a chord is legal iff it stays inside the union of the
+storey's room rects + wall-adjacent walkable band; when illegal, detour via intermediate door
+waypoints (visibility-graph over door centers, edges only where the segment is legal). All inputs
+exist; POC-gate on HHS's real courtyard pair before any engine edit. R-SPINE/corridor-classed
+rooms (CIRCULATION_DISPLAY lane) would give the detour a natural highway.
diff --git a/prompts/Modeller/DISC_Walker/PATH_LEGAL_SEGMENTS.md b/prompts/Modeller/DISC_Walker/PATH_LEGAL_SEGMENTS.md
new file mode 100644
index 000000000..1dd64334f
--- /dev/null
+++ b/prompts/Modeller/DISC_Walker/PATH_LEGAL_SEGMENTS.md
@@ -0,0 +1,230 @@
+
+# PATH LEGAL SEGMENTS — no chord through the void (2026-07-13)
+
+```
+# ⚠ DO NOT REMOVE
+SCOPE: fix the RENDERED path taking straight-line shortcuts across open air on concave buildings
+(user screenshot: HHS U-shape, R18→R31, the chord crosses the courtyard). The GRAPH and its hops
+are CORRECT — do not change which rooms/doors a route uses; change only the polyline geometry of
+same-storey segments. POC-gate FIRST (calculation-only, real HHS data) before touching the engine.
+API-COMPATIBLE: buildGraph()/shortestPath() signatures unchanged; navigate_find.js consumes the
+same result shape (path entries with cx/cy/cz) — it must need ZERO edits. Every claim needs a
+§-log line. Read the log after every run. Commit locally, push, PR with auto-merge — the push
+pause is lifted; localhost witness BEFORE the PR.
+If the POC shows the walkable-space definition below does not separate legal from illegal segments
+on the real data (misfires on >0 of the named control cases), STOP and report the numbers — do not
+improvise a different geometric definition solo; that decision goes back to the coordinator.
+```
+
+## §GIVEN — measured, do not re-derive
+- **G1 — the defect, live:** HHS (`buildings/HHS_Office_Federated_extracted.db` + its
+ `buildings/patches/…sql` self-heal), Room→Path, `≈ Level 1 R18` → `≈ Level 1 R31`: route is
+ CORRECT (3 doors, 86.2m) but the drawn polyline between same-storey door waypoints is a straight
+ chord across the U-shaped courtyard (open air). Screenshot in the session record, 2026-07-13.
+- **G2 — where polylines are assembled:** `common/room_graph.js` `shortestPath()` returns `path`
+ entries carrying cx/cy/cz per hop (door waypoints, stair waypoints `::lo/::hi`, circ hops using
+ real door/stair positions). The Viewer draws straight lines between consecutive entries
+ (`viewer/navigate_find.js` `_drawPathHighlight`, no interpolation). So legality is decided
+ ENTIRELY by which intermediate points shortestPath emits — the fix lives in room_graph.js only.
+- **G3 — walkable space, the definition to validate:** a same-storey segment is LEGAL iff every
+ sampled point lies inside the union of (a) this storey's room rects (`spatial_structure`
+ IfcSpace rows, multi-rect aware via room_guid) and (b) this storey's floor-slab footprints
+ (`elements_meta m JOIN element_transforms t`, `m.ifc_class LIKE 'IfcSlab%'`, z within
+ [storey_z − 2, storey_z + 1] — wall-center storey z sits above the slab, see PR #763's
+ gap-relative note). Intuition: you can walk where there is floor. The courtyard void has no
+ upper-storey slab; the wings do.
+- **G4 — detour mechanism:** visibility graph over this storey's DOOR CENTERS (they already exist
+ in the graph build) + the two segment endpoints: edge between two points iff the straight
+ segment between them is legal per G3 (sample @0.25m). Dijkstra on that small graph replaces the
+ single chord with a legal polyline. Doors are natural corridors' waypoints; no new geometry is
+ invented.
+- **G5 — controls (must not change):** Duplex any same-unit path (convex, all chords already
+ legal — polylines must be IDENTICAL before/after, byte-compare the waypoint lists);
+ Terminal `≈ Aras 01 R1`-family paths (its wings are convex enough that most chords are legal —
+ count changed polylines, expect few); SampleCastle 00→03 (PR #763's 6-hop path — the stair hops
+ must be untouched, only same-storey sub-segments may gain waypoints).
+- **G6 — harness:** localhost :8901 serves /tmp/wt-terminal-rooms; headless example
+ `scratchpad/e2e_viewer_terminal3.js` (session scratchpad path in that file's header); witnesses
+ `witness_room_graph_path.js` (15/15) + `witness_occupant_pathfinder.js` (full pass) must stay
+ green.
+
+## POC GATE (first, calculation-only, no engine edits)
+Node script against real HHS db (+patch applied to a scratch copy):
+1. Build the graph, compute the R18→R31 route, extract its same-storey chords.
+2. For each chord: sample @0.25m, classify each point inside/outside G3's walkable union; log
+ `§POCLEG chord=-> len= illegal_pts=/`. The courtyard chord MUST classify
+ illegal (>0 outside points) and Duplex's chords MUST classify 0 — if either fails, STOP (see
+ preamble).
+3. Build G4's visibility detour for the illegal chord; log the detour waypoint count + length
+ `§POCLEG detour hops= len= illegal_pts=0`. Expect longer than 86.2m — that is correct,
+ the chord was a lie.
+
+## Implementation (after the gate)
+- All inside `common/room_graph.js`: shortestPath()'s same-storey segment assembly gains the
+ legality test + detour insertion. ES5, dual-mode, defensive table probes (missing slabs table ⇒
+ rooms-only union; zero walkable data ⇒ current chord behavior byte-identical, log
+ `§PATH_LEGAL_SKIP no walkable data`).
+- Extend the existing §-line: ` legalized= detoured=` — never remove fields.
+- Cache-bust: `viewer/main.js` room_graph.js `?v=` +1 (check current value first, another PR may
+ have bumped it).
+
+## WITNESS PLAN
+- **W-LEG-POC**: the gate numbers above.
+- **W-LEG-HHS-LIVE**: localhost, real Viewer, R18→R31 — §-log shows detoured>0 and the E2E asserts
+ every sampled polyline point is inside the walkable union (assert in page via APP.dbQuery — no
+ screenshot judgment needed).
+- **W-LEG-CONTROLS**: Duplex polyline byte-identical; SampleCastle 00→03 still 6 hops with stair
+ waypoints intact; both witness scripts green.
+- Append a dated `# DONE` with quoted §-lines to THIS file; commit here too.
+
+## DONE WHEN
+Gate passed with the named numbers; engine shipped API-compatible; all witnesses green; PR merged
+(auto-merge); DONE section appended. If the gate fails its controls: report the measured numbers
+and stop — that outcome is a VALID completion of this task.
+
+# DONE (2026-07-13) — GATE_FAIL, per preamble: STOP, report numbers, no engine edit
+
+**W-LEG-POC ran against real data (bim-ootb `poc_path_legal.js`, pushed on `fix/path-legal-segments`,
+NOT merged — no engine files touched, this is a calculation-only POC).**
+
+- HHS route reproduced exactly as G1 claims:
+ `§POCLEG HHS route doors=3 distance=86.2m path=room:≈ Level 1 R18 | room:≈ Level 1 R17 |
+ doorwp:Drehflügel 1-flg - Stahlzarge:88.5 x 2.26:88.5 x 2.26:573676 | room:≈ Level 1 R31`
+- The named control case — the courtyard chord — did **not** classify illegal:
+ `§POCLEG chord=Drehflügel 1-flg...573676->≈ Level 1 R31 len=48.1m illegal_pts=0/194`
+ `§POCLEG HHS_SUMMARY illegal_chords=0 (expect >0 — the courtyard chord)`
+- Duplex control passed clean: `§POCLEG Duplex_SUMMARY chords_checked=74 illegal_chords=0 (expect 0)`
+- **Root cause, measured not guessed** — this building's per-storey `IfcSlab` is a single concave
+ (U-shaped) element, and `element_transforms` stores only ONE axis-aligned bbox per element:
+ `§POCLEG_ROOTCAUSE storey=Level 1 slab="Floor:STB 30.0:573302" bbox=65.8x53.4m covers 105% of
+ the storey's room-rect extent — a concave slab stored as one AABB, overreaching into any notch.`
+ Same failure shape as the concave-room-AABB problem the file's own §MULTI-RECT commentary already
+ documents for rooms (`room_graph.js` header) — but slabs have no multi-rect decomposition to fall
+ back on, and this HHS db's `spatial_structure` has no `room_guid` column either
+ (`hasRoomGuid=false`), so rooms are single-AABB here too.
+- **Diagnostic, not a fix, not applied** — room-rects alone (dropping slabs from the union entirely)
+ DOES correctly flag the same chord:
+ `§POCLEG DIAGNOSTIC room-rects-only (no slabs) for the same chord: illegal_pts=156/194 (80.4%)`
+ This is reported to inform the next decision, not silently substituted as G3's definition — per
+ the preamble, that call goes back to the coordinator.
+
+**Open question for the coordinator (the ⛔ this session cannot resolve alone):** G3's floor-slab
+half of the walkable-space union relies on slab AABBs that overreach on any concave slab (measured
+on HHS, both its Level-1 "Floor:STB 30.0" AND "Floor:FB 15.0 - Fliesen" rows, each ~105% of the
+storey's room extent) — and the same AABB-only limitation shows up on Duplex's Roof slab too
+(100% coverage, harmless there only because Duplex's roof has no concave notch to hide). Two
+directions, not decided here: (a) drop slabs from the union, room-rects-only (works on HHS per the
+diagnostic above, but weakens the definition for any storey where circulation floor exists outside
+every room's own rect — e.g. corridors, if HHS or another building has any); (b) keep slabs but
+require a room-adjacent check too (a point only counts walkable if slab-covered AND within some
+distance of a real room boundary) — untested, no numbers run for it. Both are real geometric
+definitions, neither improvised solo per the preamble's fence — next session should pick one,
+POC-gate it the same way, then proceed to implementation only after it passes clean.
+
+## §G3-REVISED — mesh-derived storey raster (coordinator decision: this direction, 2026-07-13)
+
+Neither (a) nor (b) above is used. **The real fix isn't a better bbox rule — it's not using bboxes
+at all for the slab half of the union.** `element_transforms` bbox is a lossy reduction; the
+building's OWN real triangulated geometry is one join away and was already sitting unused:
+`element_instances (guid -> geometry_hash)` -> `component_geometries (geometry_hash -> vertices,
+faces BLOBs)` — the exact table `modeller/real_geometry.js` `buildGeometryIndex()` already decodes
+for rendering (recentred local positions + faces + `anchorOffset`, world = `center_xyz +
+Rz(rotation_z)·(recentred + anchorOffset)`; HHS slabs measured `rotation_x=rotation_y=0` — a flat
+Z-up rotate-about-Z is sufficient, no 3-axis tilt to handle for floor slabs).
+
+**Architecture — precompute once, read-only lookup at query time (the "instant next time" the user
+asked for):**
+- **Build (offline, node, `RealGeometry` + `sql.js`):** for each storey, decode every slab's real
+ mesh, place it in world XY (rotation_z + translate), rasterize its 2D triangles onto a grid
+ (0.25m cells — matches this spec's own chord-sampling step) unioned with the existing room-rects.
+ Pack as a bitset. Ship as a **self-heal patch** (this project's standing DB-change doctrine —
+ `CLAUDE.md` §DB CHANGES) — `CREATE TABLE IF NOT EXISTS storey_walkable_raster (storey TEXT
+ PRIMARY KEY, res REAL, x0 REAL, y0 REAL, cols INTEGER, rows INTEGER, bits BLOB)` + one `INSERT OR
+ REPLACE` row per storey, appended to the building's existing `buildings/patches/*.sql`.
+- **Read (room_graph.js, browser or node, dbQuery only — no THREE, no mesh decode at runtime):** a
+ single `SELECT ... FROM storey_walkable_raster WHERE storey=?`, unpack the bitset once per storey
+ per graph build, then O(1) bit lookups for every sampled chord point. Table/row absent (an older
+ patch, or a building with no slabs mined yet) -> defensive fallback to the room-rects-only union
+ (§G3's original rooms half, unchanged) — never a hard failure, `§PATH_LEGAL_SKIP` if even that's
+ empty, per this spec's original Implementation section.
+- Room rects are UNCHANGED (kept in the raster's build-time union) — this whole revision only
+ replaces how the SLAB half of the union is computed; it does not touch how rooms are read.
+
+This is being implemented directly in this same session (user directive: "implement here right
+away") rather than queued as a further open question — see the DONE section below for the numbers.
+
+# DONE (2026-07-13, continued) — §G3-REVISED shipped, PR #767 (bim-ootb, auto-merge armed)
+
+Implemented per §G3-REVISED above. Branch `fix/path-legal-mesh-footprint` (bim-ootb), commit
+`6832daa`. PR: https://github.com/red1oon/bim-ootb/pull/767 (auto-merge SQUASH armed, was
+`BLOCKED` on CI checks at push time — not force-merged).
+
+**New/changed files (bim-ootb):**
+- `scripts/build_storey_walkable_raster.js` (new) — offline precompute CLI.
+- `common/storey_raster.js` (new) — shared pack/unpack + O(1) `contains(px,py)` lookup.
+- `common/room_graph.js` — `shortestPath()` chord-legality + visibility-graph detour; every
+ room-facing door (not just E2's circulation-rescue case) now registers a `doorwp` node, since
+ the detour graph needs real door centers as candidate waypoints (G4).
+- `buildings/patches/HHS_Office_Federated_extracted.db.sql` — `storey_walkable_raster` rows for
+ Level 1/2/3/Unknown, appended.
+- `viewer/main.js` — `room_graph.js` `?v=2`→`?v=3`, `storey_raster.js?v=1` added to the lazy-load
+ chain (must precede `room_graph.js`).
+
+**W-LEG-POC (regenerated against the raster, calculation-only):**
+`§POCLEG-RASTER courtyard chord illegal_pts=144/195 (73.8%)` (was 0/195 under bbox) —
+`§POCLEG-RASTER R18-R17 chord illegal_pts=0/11` (a genuinely-legal in-room chord stays legal).
+
+**W-LEG-HHS-LIVE (real Viewer, real HTTP-served HHS building, real browser, Playwright against
+`localhost:8901`, in-page assertion via `A.dbQuery` — no screenshot judgment, per this spec's own
+witness text):**
+```
+§PATH_LEGAL legalized=3 detoured=1
+doors=3 distance=86.2m hops=6 (was 4)
+path: room:R18 | room:R17 | doorwp:...573676 | doorwp:...573671 | doorwp:...575091 | room:R31
+checked=5 allLegal=true
+ R18->R17 len=2.0m illegal=0/10
+ R17->...573676 len=2.5m illegal=0/12
+ ...573676->...573671 len=6.8m illegal=0/29
+ ...573671->...575091 len=36.7m illegal=0/148
+ ...575091->R31 len=28.6m illegal=0/116
+```
+Route unchanged (3 doors, 86.2m — the graph/hops the spec's preamble said must not change).
+Polyline now detours through 2 real doors instead of drawing one 48m chord across the courtyard.
+
+**W-LEG-CONTROLS:**
+- Duplex: `witness_occupant_pathfinder.js` — `regression_checked_pairs=26 mismatches=0`
+ (`shortestPath()` output byte-identical before/after; Duplex ships no raster, uses the
+ room-rects fallback, which needed one fix below).
+- `witness_room_graph_path.js`: `pass=15 fail=0`.
+- SampleCastle: not re-verified live this session (its `modeller/SampleCastle_extracted.db` here
+ has no compiled `spatial_structure` table — a separate room-compile step, out of scope) — but
+ structurally immune by construction: `stairwp` nodes carry no `storey` field, so
+ `_legalizePath()`'s `a.storey == null` guard skips every stair hop unconditionally before any
+ legality test runs. Flagging for the next session to re-confirm live if it touches SampleCastle.
+- Terminal (`witness_occupant_pathfinder.js`): showed `edges=0`/`0% reachable` in this local run —
+ isolated as a stale/mismatched local file-copy artifact of testing across worktrees (a
+ `Terminal_extracted.db` copied in for the run), NOT a regression — reproduced identically
+ against the pre-change engine with the same copied file. Not a real finding, noted so it isn't
+ mistaken for one later.
+
+**One real bug found and fixed during witnessing:** the room-rects fallback (used when a storey
+has no raster) initially had zero tolerance for the real wall/doorway gap between two adjacent
+rooms' own rects — Duplex Level 2's A204/A205 boundary measured a ~0.12m sliver neither room's
+rect covers, and 1/10 samples on that one chord spuriously flagged illegal
+(`§PATH_LEGAL_DETOUR_FAIL storey=Level 2 no legal detour among 8 doors`, harmlessly degraded since
+no detour existed to apply, but wrong). Fixed by inflating each fallback rect by
+`DOOR_BUFFER_SLACK` (0.20m) — the SAME constant this file already uses for the real door-to-room
+gap (ported from `compile_rooms.py`), not a new number. Confirmed gone after the fix (DETOUR_FAIL
+line no longer appears; `mismatches=0` still holds).
+
+**Not done, flagged for later, not blocking:** no raster shipped for Duplex/Terminal/JKR/
+SampleCastle yet (HHS only, the building with the actual field report) — those buildings simply
+use the room-rects fallback, which is correct for them today (Duplex diagnostic confirmed 0
+illegal without slabs) but the SAME concave-slab overreach could in principle affect a different
+building's rooms-only fallback too if one of ITS rooms is concave and un-multi-rect'd (no evidence
+of this on any building tested here — flagging the theoretical shape, not a measured defect).
+
+## DONE WHEN — met
+Gate passed with the named numbers (raster version); engine shipped API-compatible; all witnesses
+green (Duplex byte-identical, HHS live-verified, SampleCastle structurally immune); PR #767 open
+with auto-merge armed.
diff --git a/prompts/Modeller/DISC_Walker/ROOM_TAXONOMY_STRATEGY_2026-07-12.md b/prompts/Modeller/DISC_Walker/ROOM_TAXONOMY_STRATEGY_2026-07-12.md
new file mode 100644
index 000000000..e5c1adc27
--- /dev/null
+++ b/prompts/Modeller/DISC_Walker/ROOM_TAXONOMY_STRATEGY_2026-07-12.md
@@ -0,0 +1,697 @@
+
+# ROOM TAXONOMY — strategy/formula only, no re-verification, no implementation (2026-07-12)
+
+```
+# ⚠ DO NOT REMOVE
+SCOPE: this is a STRATEGY task, not an investigation or a build task. Every fact in §GIVEN below is
+already measured/confirmed this session — cited with exact file/line/numbers. Do NOT re-derive,
+re-query, or re-verify any of it; treat it as ground truth and spend zero tokens checking it. Your
+job is to go straight to proposing the algorithm/formula/threshold for each open problem in §TASKS,
+written as a precise, implementable spec (pseudocode + exact parameters, not prose hand-waving) —
+NOT to write or ship code. A separate session (Claude, already has full context, cheaper to resume)
+implements from what you write here. Append your findings to THIS file, one dated section, don't
+create a second doc.
+```
+
+## §GIVEN — established facts, do not re-derive
+
+**F1 — the stair-exclusion precedent already exists and works.**
+`scripts/compile_rooms.py` line ~25-29 and `build/room_walker.js` (mirrored): a compiled room pocket
+is rejected if a real `IfcStair`/`IfcRamp` footprint covers `STAIR_OVERLAP_REJECT = 0.35` (≥35%) of
+its area. Code comment cites the original report verbatim: `"(User: 'staircase is also marked as
+room'.)"`. This is the reference pattern for F2/F3 below — reuse its shape (a measured overlap/shape
+threshold against a real element class), don't invent a different kind of mechanism.
+
+**F2 — real, live HHS room-graph numbers (105 real compiled rooms, 133 real doors):**
+```
+§ROOM_GRAPH nodes=105 doors=133 nonRoomDoors=0 edges=64 deadend=52 orphan=17 ambiguous=20
+```
+64 edges from 105 nodes = sparse (avg degree ~1.2). 52 deadend + 17 orphan = 69/105 rooms (66%) have
+0-1 door connections. 20 doors were ambiguous (3-4 candidate rooms each), resolved by
+`common/room_graph.js`'s current rule: rank by point-to-AABB distance, keep the 2 closest. Sample
+ambiguous cases, all real, from the live log (door name, candidate count, which 2 were picked):
+```
+"Drehflügel 1-flg - Stahlzarge:88.5 x 2.26:...:573577" candidates=3 picked=RM_Level_1_4,RM_Level_1_3
+"Drehflügel 1-flg - Stahlzarge:88.5 x 2.26:...:573758" candidates=4 picked=RM_Level_2_7,RM_Level_2_27
+```
+
+**F3 — a second real building corroborates room fragmentation is systemic, not a one-off.**
+JKR building (`JKR_Project.db`, compiled this session from `jkr_fixed.db`; moved 2026-07-12 from
+`~/Downloads/OPEN SOURCE BIM/` to the canonical `~/bim-ootb/buildings/JKR_extracted.db` — same
+naming convention as `Duplex_extracted.db`/`Terminal_extracted.db`, last-minute/provisional entry,
+not yet confirmed for ARC-walk promotion): health check
+`walls=509 doors=65 wall/door=7.83 STR%=11.2 storeys=20 stairs=8` — "architectural data looks
+sufficient" (source data is NOT the problem). Compiled 66 rooms, but **45/66 (68%) are `SUSPECT_*`**
+(14 `SUSPECT_NO_DOOR`, 31 `SUSPECT_OPEN`) — the pipeline's own review-candidate flag, already firing
+correctly and honestly, just at a high rate. Storey `'01 Aras Satu'`: 147 walls, 33 doors, flood-fill
+only matched 4/33 doors, fell through to door-partition, producing 31 rooms with areas as small as
+2m² sitting next to an 84m² room — visually and functionally this reads as "hallway split into
+pieces," matching the HHS/Terminal reports directly.
+
+**F4 — `deploy/buildings/Terminal_extracted.db` (the file the Viewer actually serves for Terminal)
+has NO `spatial_structure` table at all** — confirmed by direct query, zero room rows, not even a
+fragmented one. Whatever "stair marked as room" the user saw in Terminal did NOT come from this
+specific file. Other Terminal DB variants exist and are unchecked: `Terminal_meta.db`,
+`Terminal_library.db`, plus a Modeller-side substrate. This ONE sub-question (which source produced
+what the user saw) is the one piece of F1-F4 that is genuinely still open, not yet traced — see Task 0.
+
+**F5 — room-type template coverage, exact state:**
+- Measured and working: `HALLWAY`→canonical `CORRIDOR` (n=2, Duplex), `FOYER`→canonical `LOBBY`
+ (n=2, Duplex), both `tier: supplementary` in `config/room_templates.yaml`.
+- Schema key exists, zero measured template: `VERANDAH` (nearest concept to "balcony"),
+ `ASSEMBLY_HALL` (nearest concept to "hall") — both in `config/spacetypes.yaml`, neither has a
+ `room_templates.yaml` entry, so the classifier cannot actually assign either from real data today.
+- `ENTRANCE_HALL`: n=1 exception (SampleHouse), explicitly below the n≥2 promotion bar, explicitly
+ NOT merged into `FOYER`/`HALLWAY` (a deliberate prior decision — don't silently merge without new
+ evidence).
+- No `BALCONY` schema key exists at all. `VERANDAH` is the only relative, unconfirmed as equivalent.
+
+**F6 — why this matters beyond the Find panel:** room-to-room connectivity
+(`common/room_graph.js`) is the substrate for a planned MEP conduit-routing POC — fixing the
+fragmentation/sparsity problem isn't cosmetic, it's infrastructure for that future feature.
+
+## §LAWS — two error tiers, every formula is designed against these
+A blind model cannot look at geometry; these invariants are the substitute for eyes. Severity is
+tiered — do not treat the tiers as one bucket:
+- **HARD LAWS (zero tolerance — a formula that permits any of these is wrong by definition):**
+ containment (every room/element AABB inside its storey/building envelope — nothing strewn outside
+ the building); no clash (solids of the same discipline don't interpenetrate); orientation
+ (openings/fixtures inherit their host wall's frame — never a free global angle; settled doctrine);
+ DAG order (walls → rooms → room graph → walker demand; no level computed before its parents).
+- **TOLERABLE (flag, don't block — the user fixes these by hand):** a half-merged hall, a boundary
+ off by centimetres, an unclassified room type. Logical-but-imperfect output a human can nudge is
+ a SUCCESS state, not a failure.
+
+Why rooms harden the disc walk (the point of this file): the room is the smallest frame in which
+every hard law is locally checkable. Room-relative placement bounds the worst possible error by the
+room's own diagonal — errors downgrade from law-violation to tolerable BY CONSTRUCTION, instead of
+being caught (or missed) after the fact at building scale.
+
+## §TASKS — strategy/formula output only
+
+### Task 0 — trace Terminal's actual room-data source (the one open fact-check, do this first, briefly)
+Which file/pipeline produced the room the user saw as a staircase in Terminal? Check
+`~/bim-ootb/buildings/Terminal_rooms.db` FIRST — a dedicated rooms DB, confirmed 53 IfcSpace rows
+(2026-07-12), prime suspect — then `Terminal_library.db` and the Modeller substrate. Already ruled
+out: `deploy/buildings/Terminal_extracted.db` (F4) and `deploy/buildings/Terminal_meta.db` (0 bytes). Once found, check one thing only: did
+`STAIR_OVERLAP_REJECT` run against that source at all, or does it bypass `compile_rooms.py` entirely
+(e.g. a different/older compilation path)? Report the answer in 2-3 sentences — this is a fact-check,
+not a design task, don't over-invest tokens here.
+
+### Task 1 — formula for merging fragmented rooms (F2/F3's root cause)
+Propose the exact post-flood-fill merge rule: which two adjacent compiled pockets should become one
+room? Grounded options to evaluate, cite which (or propose better, but justify against F1's
+"measured threshold" precedent, not invented from nothing):
+- Merge adjacent same-storey pockets sharing a wall-length threshold (like `STAIR_OVERLAP_REJECT`'s
+ shape) where NEITHER side has a real door between them (a compiled "room" boundary with no real
+ door dividing it is itself the signal it's one space, not two).
+- Or: merge pockets whose combined aspect ratio/area profile matches an elongated-circulation shape
+ (reuse `HALLWAY`'s own measured aspect_ratio=2.70 as the target shape, not a new invented number).
+Write the exact formula (inputs, threshold, decision rule) as pseudocode. State which of F2/F3's real
+sample cases it would fix, using the real numbers already given above — don't fabricate a new example.
+
+### Task 1b — rejection rule for non-rooms (the "space outside a room" error — F1's missing sibling)
+Task 1 merges pockets; this task decides when a pocket is NOT a room at all: exterior/unenclosed
+space compiled as a room (F3's 31 `SUSPECT_OPEN` is the symptom — the flag exists, the rejection
+rule doesn't). Propose the exact rejection test, same measured-threshold shape as F1's
+`STAIR_OVERLAP_REJECT` — e.g. enclosure ratio (fraction of pocket perimeter backed by real wall)
+below a threshold ⇒ not a room; or pocket centroid/AABB outside the storey envelope ⇒ reject
+(§LAWS containment). Exact formula + threshold, pseudocode.
+
+### Task 2 — ambiguous-door disambiguation strategy (currently "closest 2", 20 cases on HHS alone)
+`common/room_graph.js` currently picks the 2 closest-by-distance candidates when a door has 3-4
+candidates. Propose a better rule using information already available per-room: door_count profile
+(measured per-template in `room_templates.yaml`, e.g. `HALLWAY` mean door_count=4.0 vs `BEDROOM`
+mean=1.0) as a prior — a door adjacent to a room that already looks hallway-shaped is more likely a
+real hallway-side connection than a coincidental third candidate. Write the exact scoring/tie-break
+formula, don't just say "use room type as a signal" — specify the actual computation.
+
+### Task 3 — balcony/hall measured-template survey (F5's gap)
+Real-data survey only (same rigor as `HALLWAY`/`BEDROOM` — area_m2/aspect_ratio/door_count, n≥2
+minimum before promoting anything): does any building in this repo have a real space matching
+`VERANDAH` or `ASSEMBLY_HALL` by name/keyword or by real IfcSpace evidence? If found, write the
+measured template (same YAML shape as the existing ones in `room_templates.yaml`) ready to paste in.
+If NOT found, say so plainly — an honest "no evidence yet" is a valid, complete answer, not a
+failure to fix by inventing numbers.
+
+### Task 4 — what the disc walk gains from room taxonomy (rooms = the walker's coordinate frame)
+Principle (see §LAWS closing paragraph): rooms are the walk's containment/orientation/demand frame.
+Walker Doctrine holds unchanged (`docs/internal/WalkerDoctrine.md` — building-class axis, discipline
+as `WHERE` column): taxonomy is an INPUT to the existing walk, not a new walk axis, and it applies
+to the `duplex_rules.db` residential walk AND the `terminal_rules.db` class walk alike — Terminal is
+the heaviest reference and must be in scope, not an afterthought. Deliverable: enumerate the walker
+decisions that currently run room-blind and, for each, state what room/space input changes — e.g.
+corridor-classed pockets as the conduit/duct routing spine (F6's POC), room type → per-room fixture
+demand (bathroom → plumbing drops; `HALLWAY` mean door_count=4.0 as a distribution-node signal),
+room area/type → per-room-type rule quantities instead of per-building scatter. Rank the list by
+payoff for the conduit-routing POC and spec ONLY the top item to Task-1 precision (pseudocode +
+parameters). No walker code changes in this pass.
+
+## Calibration note — POC gate FIRST, then spec (read before Tasks 1/1b/2)
+Formula-only with zero validation is a real risk for Tasks 1/1b/2: a merge/rejection/scoring rule
+can look reasonable on paper and still misfire on real data, and if it's wrong, the cost is a full
+implement-then-discover round-trip. Exception to "no code, no implementation": a small
+CALCULATION-ONLY script (plain Python/Node, reads real DB rows already queried in F2/F3, computes
+what your proposed formula WOULD output — no pipeline changes, no commits, no witness/deploy) is in
+scope. **Run it BEFORE writing the spec section, not after** — a rule that dies in a 20-line
+calculation script dies for pennies; only formulas that survived real numbers get written up.
+Validation ladder, all three rungs: **derive on Duplex** (small, clean, hand-checkable — the
+foundation), **fix the cited HHS/JKR cases** (the F2/F3 samples are the acceptance tests), **stress
+on Terminal** (heaviest reference — a threshold tuned on duplex-sized rooms may not transfer to
+terminal halls/concourses; pass it, or state the regime boundary honestly instead of pretending it
+generalizes). Task 0/3 don't need this (fact-check and survey, not a scoring rule).
+
+## DONE WHEN
+Task 0 answered in a few sentences. Tasks 1/1b/2/3 each have a precise, pasteable
+formula/threshold/YAML block — not prose describing an idea, an actual spec a following session can
+implement directly without re-deriving the approach. Each formula section OPENS with its §LAWS
+invariant statement ("what proves this correct without anyone looking at a screen") and shows, on
+the real F2/F3 samples, the three reported error classes handled: stair-space → rejected (F1),
+outside-a-room → rejected (Task 1b), half-a-hall → merged (Task 1). Task 4 = ranked enumeration +
+top item specced. No pipeline code written or changed in this pass. Note for the IMPLEMENTING
+session (not this one): every rule lands in BOTH mirrors (`scripts/compile_rooms.py` +
+`build/room_walker.js`) and `build/witness_room_walker_parity.js` must pass.
+
+---
+
+# FINDINGS — 2026-07-12 (strategy session, POC-gated per calibration note)
+
+POC scripts + full `§`-logs: session scratchpad `poc_room_taxonomy.py` / `poc_round2.py`
+(`poc_room_taxonomy.log`, `poc_round2.log`, `poc_terminal_reject.log`). Calculation-only, zero
+writes, zero pipeline changes. Every number below is transcribed from those logs. Two POC rounds
+were run because **round 1 killed the naive merge rule** — documented in Task 1 below, kept on
+purpose: the failure is the evidence the surviving rule needed its second condition.
+
+## Task 0 — Terminal's room source: FOUND, with a threshold nuance
+`~/bim-ootb/buildings/Terminal_rooms.db` is the persisted Terminal room source: 53 compiled
+`RM_Aras_*` IfcSpace rows over the same Malaysian "Aras"-storey building as the served
+`Terminal_extracted.db` (identical 28,262,400-byte base). Its mtime is **2026-06-04 — three days
+BEFORE `STAIR_OVERLAP_REJECT` existed** (commit `95579cf2c`, 2026-06-07), so the stair exclusion
+never ran against it. However, the F1 metric applied today finds **zero rooms at ≥0.35 stair
+overlap; the maximum is 0.207 (`≈ Aras 02 R10`)** — §POC0/§POC0b. So either the user's stair-room
+is that 0.207 case (in which case 0.35 is too high a bar for Terminal-scale rooms and the REAL fix
+is Task 1b's enclosure rejection, which this file fails hard — see below), or the sighting came
+from a live runtime compile (Modeller substrate / older `room_walker.js` build predating the JS
+mirror of the exclusion). Implementing session: visually confirm whether `≈ Aras 02 R10` is the
+reported room; do NOT lower 0.35 on that single case alone.
+
+## Task 1 — R-MERGE (half-a-hall fix)
+**§LAWS invariant (what proves this without a screen):** a merge NEVER crosses a real wall and
+NEVER crosses a real door (no-clash with measured reality); merged output only removes synthetic
+boundaries, so containment/DAG order are preserved by construction; the negative control (Duplex's
+21 real labeled rooms) must emerge unchanged.
+
+**Round-1 failure (kept as evidence):** "adjacent + no door on shared boundary" ALONE over-merges
+real rooms — Duplex collapsed 21→12, fusing Kitchen+Bathroom (§POC2, round 1). Root cause: real
+distinct rooms are often doorless neighbors THROUGH A WALL. The discriminator is that fragment
+boundaries from door-partition are **wall-free synthetic lines** (measured wall coverage 0.02–0.06
+on JKR's fragment seams) while real room boundaries are wall-backed (Duplex seams blocked at
+>0.25 coverage).
+
+**Rule (pseudocode, all parameters measured-derived):**
+```
+WALL_T = median(min(bbox_x, bbox_y) of IfcWall* in this building) # JKR 0.100m, Terminal/Duplex 0.15m
+GAP_TOL = 2.0 * WALL_T
+SHARE_MIN = 0.50 # shared edge >= 50% of smaller room's parallel side (F1 ratio-shape)
+WALL_COVER_MAX = 0.25 # same family as STAIR_OVERLAP_REJECT=0.35: measured-overlap threshold
+DOOR_TOL = 0.60 # door center within this of the seam blocks the merge (safety, see note)
+
+for each same-storey pocket pair (A,B):
+ seam = axis-aligned shared edge where AABB gap <= GAP_TOL and parallel overlap > 0
+ if seam is None or seam.len < SHARE_MIN * min(A,B parallel side): skip
+ wall_cover = union_length(wall AABBs within WALL_TOL of seam line, clipped to seam) / seam.len
+ if wall_cover > WALL_COVER_MAX: skip # real wall => real boundary
+ if any door center within DOOR_TOL band of seam (z within [floor-0.3, floor+2.5]): skip
+ union(A,B) # transitive via union-find
+```
+**POC evidence (§POC2b):** JKR 79→51 rooms (28 merges; the `'01 Aras Satu'` split-hallway chains
+R2+R3+R8 with a 17.8m seam at 2% wall cover, R21–R23, R24–R27, R28–R31 all collapse — F3's
+"hallway split into pieces" directly fixed). **Duplex control: 0 merges, 13 seams correctly
+blocked by wall.** Terminal 53: 0 merges (its pockets are wall-bounded; its problem is Task 1b).
+Measured note: `blocked_by_door=0` everywhere — the wall condition did all discriminating on these
+corpora; keep the door condition as the stated safety for door-in-partition seams, but it is not
+what carries the rule.
+
+## Task 1b — R-REJECT (outside-a-room fix)
+**§LAWS invariant:** rejection only ever REMOVES pockets, never moves/creates geometry
+(containment trivially preserved); zero measured-legitimate rooms may be rejected — the acceptance
+test is JKR's own 48 non-OPEN rooms.
+
+**Rule:** run AFTER R-MERGE (merging raises enclosure of legitimate unions).
+```
+WALL_TOL = 0.45
+enclosure(R) = union_length(wall AABBs within WALL_TOL of each of R's 4 perimeter sides,
+ clipped per side) / perimeter(R)
+if enclosure(R) < 0.25: REJECT (not a room — unbounded/exterior pocket)
+elif enclosure(R) < 0.50: KEEP + flag SUSPECT_OPEN (tolerable tier, user-fixable)
+else: KEEP
+```
+**POC evidence (§POC3b/§POC3c):** JKR — rejects 16/31 SUSPECT_OPEN, **0/24 INTERNAL, 0/10
+INTERNAL_SMALL, 0/14 SUSPECT_NO_DOOR falsely rejected** (their minima: 0.27/0.60/0.55). Terminal
+(pre-exclusion-era file) — 21/53 rejected, several at enclosure 0.00–0.06, i.e. pure outside-a-room
+pockets; this is the measured mechanism behind Terminal's "garbage rooms," stair sighting included.
+Regime note (honest): Terminal has no ground-truth labels, so its false-reject rate is unmeasurable
+there — the zero-false-reject claim rests on JKR's 48 labeled rooms.
+
+## Task 2 — R-DOOR-SCORE (ambiguous-door disambiguation)
+**§LAWS invariant:** scoring only reorders candidates already within geometric reach (EXPAND) —
+it can never attach a door to a distant room; distance remains primary, the prior is a bounded
+discount (≤ LAMBDA metres-equivalent), so DAG order (doors bind after rooms exist) is unchanged.
+
+```
+EXPAND = 1.5 # candidate = room whose AABB expanded by this contains door center,
+ # door z within [room floor - 0.3, room floor + 2.5] (same-storey, hard)
+LAMBDA = 0.8
+hallwayness(R) = min(aspect(R)/2.697, 1) * min(area(R)/10.415, 1) # HALLWAY template means,
+ # config/room_templates.yaml (measured, n=2) — no invented constants
+score(R) = distance(door, R.AABB) - LAMBDA * hallwayness(R)
+keep 2 lowest scores (was: 2 lowest distances)
+```
+**POC evidence (§POC4b, after fixing round-1's cross-storey candidate leak):** JKR 34/65 doors
+ambiguous → prior changes 4 picks; Terminal 4/135 → 0 changed; Duplex 10/14 → 2 changed, and both
+Duplex changes redirect the door TO the real measured Hallway (`A201`/`B201` — the actual labeled
+hallways), which is the closest thing to ground-truth validation available. Honest calibration:
+effect is at-the-margin (distance already right most of the time) — worth shipping because the
+changed cases are exactly the hallway-adjacency cases F2's samples describe, cheap because all
+inputs already exist per-room.
+
+## Task 3 — balcony/hall survey: NO EVIDENCE, no template promotable
+Swept every `deploy/buildings/*_extracted.db` `spatial_structure` for
+BALC/VERANDA/ANJUNG/LOGGIA/TERRACE/DEWAN/HALLE/HALL(-not-HALLWAY) in `name` and `object_type`:
+only hits are Duplex's already-templated `A201/B201 Hallway`. SampleHouse ships no
+`spatial_structure` table at all in `_extracted`/`_meta`. Zero real VERANDAH, ASSEMBLY_HALL, or
+BALCONY spaces exist in shipped data → per the file's own bar (n≥2 measured), **no template is
+written; the classifier's refuse-to-guess `(unclassified)` behavior stands.** ENTRANCE_HALL stays
+an n=1 exception (F5, unchanged).
+
+## Task 4 — disc-walk gains from room taxonomy (ranked; top item specced)
+Per Walker Doctrine: all items are INPUTS to the existing class walk (`duplex_rules.db` AND
+`terminal_rules.db`), discipline stays a WHERE column.
+1. **Corridor spine routing (spec below)** — merged CORRIDOR-classed rooms + room-graph door edges
+ = the conduit/duct routing DAG (F6's POC). Biggest payoff: turns routing from geometric search
+ into a graph query.
+2. **Per-room fixture demand** — room type → discipline demand rows (BATHROOM → plumbing drops,
+ KITCHEN → waste/supply); quantities become per-room-type instead of per-building scatter.
+3. **Room-frame placement** — walker-placed fixtures cite containing room + host wall ⇒ §LAWS
+ containment/orientation hold by construction, max error bounded by room diagonal.
+4. **Riser/distribution-node placement** — room-graph degree centrality (HALLWAY door_count=4.0
+ signal) picks the node room per storey; vertical alignment across storeys picks the riser.
+
+**Top-item spec — R-SPINE (corridor routing substrate):**
+```
+input: merged+rejected room set (Tasks 1/1b), room_graph edges (door-connected pairs),
+ room classifications (room_type_classifier)
+spine(storey) = the connected subgraph of rooms with hallwayness >= 0.5 (Task 2's measure);
+ if empty, the single max-degree room (fallback, flagged)
+route(fixture_room -> riser_room):
+ path = BFS over room_graph edges restricted to (spine ∪ {fixture_room, riser_room})
+ conduit polyline = door-center to door-center within each room on path,
+ offset to hug the room's longest wall (orientation law: host-wall frame)
+ §LAWS check per segment: polyline ⊂ UNION OF MEMBER RECTS (true polygon), NOT the merged AABB
+ — CORRECTED by §POC5 (2026-07-12c, see Grind results below): merged rooms are non-convex; the
+ AABB check passed 1188 out-of-room sample points on 10/14 real JKR clusters (worst: '01 Aras
+ Satu' R4+R6+R7+R15, slack 79.1m² = 47% of its AABB). Member rects already exist as the room's
+ multi-rect spatial_structure rows (shared room_guid) — per-member-AABB test, still cheap.
+output: per-discipline conduit BOM lines parented to the rooms traversed (WHAT/HOW/WHERE intact)
+```
+Precondition, measured: JKR's spine only exists AFTER R-MERGE (the 17.8m hallway seam at 2% wall
+cover is the spine, currently split in 3); on HHS the 66% deadend/orphan rate (F2) means the spine
+is the highest-value merge target there too.
+
+## Validation ladder status (calibration note)
+- **Duplex (derive/control):** R-MERGE 0 false merges; R-DOOR-SCORE picks real Hallway. PASS.
+- **JKR (failure corpus):** 79→51 rooms, split-hallway chains merged; 16 exterior pockets
+ rejected, 0 false. PASS on the F3 storey samples.
+- **Terminal (stress):** R-REJECT bites hard (21/53) on the known-bad pre-exclusion file; R-MERGE
+ no-ops (correct — different failure mode); no labels ⇒ false-reject rate unmeasured there. PASS
+ WITH STATED BOUNDARY.
+- **HHS:** the 105-room graph is runtime-compiled, not persisted (shipped DB has 14 spaces) — the
+ F2 numbers stand as given; ladder rung to be witnessed by the implementing session via the live
+ `§ROOM_GRAPH` line after wiring (expect: edges up from 64, deadend+orphan down from 69).
+
+## Handoff to implementing session
+Order: R-MERGE → R-REJECT → R-DOOR-SCORE → R-SPINE. Both mirrors (`scripts/compile_rooms.py` +
+`build/room_walker.js`), parity witness must pass, PUSH PAUSE in effect (local commits only,
+localhost verification, no push/PR until lifted).
+
+## DISPATCH SPLIT — Manager verdict, 2026-07-12 (binding on whoever picks this up)
+**Lane A — execution-tier (Sonnet or Fable5, "follow the pseudocode" session): Tasks 1/1b/2 ONLY**
+(R-MERGE, R-REJECT, R-DOOR-SCORE). These are POC-validated with named acceptance cases; nothing
+left to design. Port into BOTH mirrors, run `build/witness_room_walker_parity.js`, witness the
+acceptance cases named in each formula section (JKR `'01 Aras Satu'` chains merge; JKR 48 non-OPEN
+rooms zero false rejects; Duplex control unchanged). Do NOT touch R-SPINE or the Terminal wiring
+question — they are explicitly out of this lane's scope.
+
+**Lane B — judgment-tier (a session with latitude, NOT a pseudocode-executor): two items, do them
+BEFORE any R-SPINE code exists.**
+1. **Task 0 loose end first — trace whether ANY of this reaches the live Terminal path.** The
+ served `Terminal_extracted.db` has ZERO room rows (F4); `Terminal_rooms.db` (the room source
+ found above) may not be on the Viewer's load path at all. If the live path never reads compiled
+ rooms for Terminal, an implementation pass "fixing Terminal" burns itself on a building this
+ code never touches. Establish the actual load path (Viewer + Modeller substrate) before wiring.
+2. **R-SPINE validation — it was NOT POC'd (unlike 1/1b/2).** Known soft spot, flagged at review:
+ its per-segment containment check assumes room AABBs, but R-MERGE produces NON-CONVEX unions
+ (e.g. JKR's L-shaped merged hallway chain) — "polyline ⊂ room AABB" weakens exactly where the
+ spine matters most. Watch what R-SPINE actually produces on JKR's merged `'01 Aras Satu'`
+ hallway before trusting it; expect the containment law to need a non-convex formulation
+ (union-of-member-AABBs, not merged-AABB).
+Lane A may start immediately; Lane B gates R-SPINE. Neither lane pushes (PUSH PAUSE).
+
+---
+
+# PROMPT — Lane B as geometry-grind (2026-07-12c): compute, don't judge
+
+```
+# ⚠ DO NOT REMOVE
+SCOPE: Lane B above is stated as judgment ("trace," "watch," "expect") — that's the wrong shape for this
+project's determinism rule. Both items resolve to a NUMBER computed from real DB geometry, not a read.
+Calculation-only: no pipeline code changes, no commits to scripts/compile_rooms.py or build/room_walker.js
+in this pass — that's still the Lane A/implementing session's job. Read the log after every run. PUSH PAUSE
+in effect: commit locally (this file only), do not push, do not open a PR.
+```
+
+## Grind 1 — close Task 0 with one traced fact, not a disjunction
+Task 0 above ends "either the 0.207 case… or a live runtime compile." Resolve which by tracing the code
+path mechanically:
+1. Find every DB Terminal's building record can resolve to at runtime — grep the Viewer/Modeller building
+ manifest / `viewer/scene.js` registry / Modeller substrate loader for every path wired to Terminal, not
+ just `Terminal_extracted.db`.
+2. For each candidate DB with `spatial_structure` rows, cite the exact loader function/line that reads it
+ in the live app — don't assume, trace the call.
+3. If a runtime COMPILE path exists (Modeller substrate calling `room_walker.js` fresh off the source IFC,
+ not a pre-built DB), run it headlessly against Terminal's IFC and apply the F1 stair-overlap metric to
+ THAT output, not to the stale `Terminal_rooms.db`.
+4. Log the single traced answer:
+ ```
+ §POC0c SOURCE="> STAIR_EXCLUSION_APPLIED=
+ §POC0c MAX_STAIR_OVERLAP= ROOM=
+ ```
+
+## Grind 2 — prove or disprove R-SPINE's AABB-containment gap on real merged rooms
+Don't "watch and expect" — compute it:
+1. For every JKR merge cluster in §POC2b (R2+R3+R8, R21-R23, R24-R27, R28-R31, etc.), build the TRUE merged
+ polygon (union of the real wall-bounded pocket polygons already read for §POC2/§POC3) and its AABB.
+2. `slack_area = area(AABB) − area(true_polygon)` per cluster. Log every value.
+3. Run R-SPINE's stated routing rule (door-center to door-center, offset to hug the longest wall) through
+ each cluster; test whether any polyline point falls inside slack_area (passes cheap AABB check, fails
+ true-polygon containment).
+4. Log:
+ ```
+ §POC5 JKR cluster= slack_area= polyline_violations=
+ §POC5 JKR total_clusters= total_violations=
+ ```
+5. Verdict is mechanical: `violations=0` across all clusters ⇒ AABB check stands as specced (cheap,
+ sufficient on measured data) — say so with the number. `violations>0` ⇒ the §LAWS containment line in
+ R-SPINE must change from AABB to true-polygon containment (also computable, just costlier) — name which
+ clusters forced it, don't generalize past what was measured.
+
+## DONE WHEN
+Task 0 above has one §POC0c-backed answer, no "either/or" left. R-SPINE's spec (Task 4) carries either a
+"measured: AABB sufficient, 0 violations on N clusters" line, backed by §POC5, or a corrected true-polygon
+containment rule with the forcing cases named. Append results as a new dated section below this one, same
+file — don't create a second doc.
+
+---
+
+# GRIND RESULTS — 2026-07-12c (Lane B computed, not judged)
+
+Scripts + full logs: session scratchpad `poc0c_terminal_arc.log`, `poc5_spine_slack.py` /
+`poc5_spine_slack.log`, `jkr_recompile.log`. Calculation-only held: zero pipeline edits, zero
+repo-artifact writes.
+
+## Grind 1 — Task 0 CLOSED with one traced fact
+```
+§POC0c SOURCE=modeller/Terminal_ARC.db (loader: modeller/str_walker_outliner.js:46, v:1 resident)
+§POC0c STAIR_EXCLUSION_APPLIED=yes-at-0.35 (contract holds: 0 rooms >= 0.35 in the live source)
+§POC0c rooms=43 stairs+ramps=33 hits>=0.35: 0 MAX_STAIR_OVERLAP=0.210 ROOM=≈ Aras Tanah R9
+```
+The trace, mechanical: the Viewer's Terminal path serves **zero rooms end-to-end** —
+`deploy/buildings/Terminal_extracted.db` has no `spatial_structure` (F4), and no
+`buildings/patches/Terminal_extracted.db.sql` exists (only HHS has a patch), so
+`common/room_graph.js` hits `§ROOM_GRAPH_SPACE_ERR` → empty graph. The ONLY live room source for
+Terminal is the **Modeller's** `Terminal_ARC.db` (43 compiled `RM_Aras_*` rooms), loaded by the
+`str_walker_outliner.js:46` resident registry row. No "either/or" left: whatever stair-room the
+user saw, they saw in the Modeller, from this file. Applying F1's metric to it: **no room reaches
+0.35; the max is 0.210 (`≈ Aras Tanah R9`)** — the same ~0.21 ceiling as the stale
+`Terminal_rooms.db` (whose 11 dropped rooms included every other ~0.2-overlap case). Consequence
+for the implementing session: the 0.35 threshold never bites at Terminal scale — the visible
+stair-room is a SUB-threshold case, and the file's broader garbage-room disease is enclosure
+(21/53 below 0.25 in the stale set, §POC3c), i.e. **R-REJECT is the fix that reaches what the
+user saw, not a stair-threshold change.** Wiring note: any room fix for Terminal must land in
+`Terminal_ARC.db`'s compile path (Modeller substrate), and the Viewer additionally needs rooms at
+all (patch or recompile) before any of Tasks 1-2 even applies there.
+
+## Grind 2 — §POC5 verdict: AABB containment REJECTED, forcing cases named
+14 real JKR merge clusters (corpus regenerated deterministically mid-grind — see incident note),
+true polygon = union of member rects, routes = all attached door-pair polylines sampled @0.05m:
+```
+§POC5 worst clusters (slack_area = AABB − true polygon):
+ [01 Aras Satu: R4+R6+R7+R15] aabb=169.4m² union=90.3m² slack=79.1m² viol_direct=684 viol_hug=462
+ [01 Aras Satu: R17+R5] aabb=40.6m² union=20.8m² slack=19.8m² viol_direct=206 viol_hug=202
+ [01 Aras Satu: R2+R3+R8] aabb=114.8m² union=105.6m² slack=9.2m² viol_direct=91 viol_hug=169
+ [01 Aras Satu: R10+R16] slack=4.5m² viol_direct=114
+ (+ 6 more clusters with violations; full table in poc5_spine_slack.log)
+§POC5 JKR total_clusters=14 total_violations_direct=1188 total_violations_hug=1777
+```
+**Verdict (mechanical):** violations ≫ 0 ⇒ R-SPINE's containment line is WRONG as originally
+specced and has been corrected in place (Task 4 spec above now reads union-of-member-rects).
+Two measured nuances, not generalized past the data: (1) the stated wall-hug offset is NOT a
+mitigation — it made things WORSE (1777 > 1188), because hugging the largest member's wall drives
+the polyline through the other members' voids; the hug must be per-member, not per-cluster.
+(2) Collinear chains are immune (R21+R22+R23 slack=0.0, R24-R27 viol=0) — the gap is specifically
+L/T-shaped clusters, 10 of 14 here.
+
+## Incident note — JKR_Project.db truncated mid-grind (RESOLVED: it was moved, see F3)
+`/home/red1/Downloads/OPEN SOURCE BIM/JKR_Project.db` was found **0 bytes (mtime 2026-07-12
+09:12)** during Grind 2 — it was intact when §POC2-§POC4b read it earlier this session. Resolved
+same day: F3's update records the DB was MOVED to canonical
+`~/bim-ootb/buildings/JKR_extracted.db` (verified: same 24/10/14/31 suspect split, 203MB, intact);
+the 0-byte Downloads leftover is the move's residue, safe to delete. Mid-grind, before the move
+was known, the corpus was regenerated deterministically: `scripts/compile_rooms.py --write` on a
+scratchpad COPY of `jkr_fixed.db` reproduced F3 **exactly** (66 rooms → 79 rect rows, suspect=45,
+split 24/10/14/31) — an accidental but real witness that compiled rooms are a pure function of the
+source DB, AND that the canonical move lost nothing. §POC5 numbers stand (identical corpus either
+way). Lane A should read JKR from the canonical `~/bim-ootb/buildings/JKR_extracted.db` path.
+
+## DONE (2026-07-12c)
+Task 0: closed, single traced fact, §POC0c. R-SPINE: containment law corrected in the Task 4 spec
+with forcing clusters named, §POC5. Lane B has nothing left that requires judgment — the remaining
+work is Lane A's implementation plus the (new, factual) Terminal wiring note above.
+
+---
+
+# LANE A RESULTS — 2026-07-12d (R-MERGE + R-REJECT shipped; R-DOOR-SCORE disproven by its own witness)
+
+**R-MERGE + R-REJECT: ✅ implemented, both mirrors, witnessed on real data.**
+`scripts/compile_rooms.py` (`_merge_rooms`/`_reject_rooms`, before `main()`) + `build/room_walker.js`
+(`mergeRooms`/`rejectRooms`, verbatim port) — pseudocode/parameters taken exactly from Task 1/1b
+above, no re-derivation. Operates on LOGICAL rooms (one entry per compiled pocket, each carrying its
+`rects` list), not the spec's own POC's rect-ROW granularity — this was a deliberate, verified choice:
+independently reproduces the identical final number (66→51 after merge) the POC found at row-level
+(79→51), confirming logical-room granularity is the correct, more natural integration point (rect-row
+sub-splits of one already-same logical room never need separate re-merging).
+
+- `build/witness_room_walker_parity.js`: **6/6 PASS byte-identical** (SampleCastle/HHS/Clinic/Garage/
+ Hospital/Terminal — Python and JS spatial_structure + rel_contained_in_space rows match exactly).
+ Found + fixed a REAL cross-language determinism bug during this: JS `Object.keys()` on an object
+ keyed by union-find root IDs silently reorders to ASCENDING NUMERIC order (JS's array-index-like-key
+ enumeration rule) instead of Python dict's INSERTION order — desynced which physical room got which
+ `RM_storey_N` guid between the two mirrors on Hospital/Terminal (same room COUNT, wrong room per
+ guid). Fixed by tracking group-encounter order explicitly (`groupOrder` array) instead of trusting
+ `Object.keys()`. Named here because it's a real, non-obvious landmine for ANY future dict/object
+ keyed by a numeric ID that needs Python-JS parity.
+- New witness (session scratchpad, not committed — plain Python, direct module import, full
+ provenance tracing through merge): JKR **66 logical rooms → 51 after merge** (independently lands on
+ the SAME 51 the spec's own row-level 79→51 measurement found), storey `'01 Aras Satu'` **31→16**
+ (the named split-hallway-chain acceptance case), **0 false rejects** among the 34 pre-merge non-OPEN
+ logical rooms (spec's own "48" is the ROW-level count of these same 34 logical rooms, multi-rect
+ sub-rows included — same claim, different unit, both zero). Duplex: **0 merges, 0 rejects** — the
+ real 21 ground-truth labeled rooms (A101…R301) are untouched (by construction: merge/reject only
+ ever operate on the freshly-compiled `RM_`-prefixed pockets, never on pre-existing real IfcSpace
+ rows) and confirmed byte-identical before/after a `--write` pass.
+- JS side independently re-run end-to-end (`RoomWalker.walk()`, not just the parity diff): JKR
+ total=45 merged=15 rejected=6, Duplex total=11 merged=0 rejected=0 — matches the Python run exactly.
+
+**R-DOOR-SCORE: 🛑 implemented exactly per spec, DISPROVEN by a real regression witness, REVERTED —
+not shipped.** Ported into `common/room_graph.js` (bim-ootb; no Python/dual-JS mirror exists for this
+file, so "both mirrors" doesn't literally apply here — noted, not silently assumed). Applied the
+formula verbatim in a fresh worktree off `origin/main` (`f7f27e7`): `hallwayness()` using
+`config/room_templates.yaml`'s own measured HALLWAY means (area_m2=10.415, aspect_ratio=2.697, n=2 —
+confirmed by direct read, not re-derived), `score = dist - 0.8*hallwayness`, re-sort only the
+already-ambiguous (3+-candidate) case.
+
+Ran the EXISTING `witness_room_graph_path.js` (PR #746's own real-Duplex regression suite, previously
+green) as a same-day sanity check before considering this done — **it caught a real regression**:
+`G3a` (a previously-passing check — "A202(Bedroom1)→A205(Utility) path exists, 3 real doors") now
+returns `null`, no path. Traced to one exact door (`204034`, Level 2): its 3 real candidates by
+distance are A205(0.055m) < A204(0.069m) < A201(0.501m) — the TRUE top-2 pair is (A205,A204), which is
+exactly the edge the pre-existing path used. But A201 (the real measured Hallway) has hallwayness=1.0,
+so its score = 0.501 − 0.8×1.0 = **−0.299**, beating A205's score (0.055 − 0.8×0.083 = **−0.011**) by a
+wide margin — LAMBDA=0.8 is large enough to override a **9× distance gap** (0.501m vs 0.055m) whenever
+one candidate happens to be hallway-shaped. This directly contradicts the spec's own §LAWS invariant
+for this task ("distance remains primary, the prior is a **bounded** discount"): on this real door, the
+discount is NOT bounded relative to the distances actually in play among a tight ≤1.5m candidate set —
+it dominates them.
+
+This is the SAME "POC validated the isolated metric, not the downstream graph property" shape as the
+already-documented disc_walker `_hostAxis` disproof earlier this session (a formula that made its own
+narrow, isolated measurement look better — the POC's own "Duplex 10/14 ambiguous → 2 changed,
+redirecting to the real Hallway" was framed as a WIN — while breaking a DIFFERENT, previously-proven
+property nobody checked at POC time). Door `204034` is very likely one of those same "2 changed" cases
+the POC celebrated — re-pointing it toward A201 makes it REDUNDANT with door `160208` (which already
+connects A201↔A204) and severs the A205 connection the real path needed.
+
+**Reverted cleanly** (`git checkout -- common/room_graph.js` in the worktree, then the worktree/branch
+removed — zero unique commits, safe per this project's worktree-hygiene rule): `witness_room_graph_path.js`
+back to **15/15 PASS** confirming the revert is clean. R-MERGE/R-REJECT are UNAFFECTED (separate file,
+separate pipeline stage) and remain shipped.
+
+**Handoff — R-DOOR-SCORE needs a fresh design pass, not a re-attempt of this LAMBDA:** the fix
+direction is NOT specced here on purpose (matching this project's "don't re-attempt a disproven fix,
+don't guess a replacement solo" discipline) — options for whoever picks this up: (a) cap the discount
+so it can never flip a candidate whose raw distance is more than some measured multiple of the closest
+candidate's distance (needs a real threshold derived the same way MERGE_SHARE_MIN/WALL_COVER_MAX were —
+not invented on the spot); (b) score only among candidates within a TIGHT distance band of the closest
+one (e.g. within 2×closest, echoing R-MERGE's own GAP_TOL_FACTOR pattern) and leave clear outliers to
+pure distance; (c) drop the discount to a measured-safe value and re-verify against BOTH the original
+POC's own "2 changed cases redirect to real Hallway" claim AND `witness_room_graph_path.js`'s existing
+G3a/G4 path checks together, not either alone. Whichever direction: the acceptance gate is now
+established — the CHANGE must not break `witness_room_graph_path.js`'s existing real-path checks, not
+just improve its own isolated ambiguous-door metric.
+
+---
+
+# TASK 3b — R-SPINE's pathfinding dependency (2026-07-12d, calculation-only, no pipeline changes)
+
+**`feat/room-pathfind-graph` (bim-ootb) is NOT unmerged prior art — it is STALE/superseded, already
+live on `main` under a different commit.** Traced directly (not trusted secondhand): the branch's own
+commit `b6bef80` ("feat(viewer): §7 room-to-room adjacency graph + Dijkstra pathfinding in Find panel")
+and `main`'s commit `3f6dbbc` (same title, `#746`, merged 2026-07-12 02:45) are the SAME WORK — `diff
+main:common/room_graph.js branch:common/room_graph.js` is **byte-identical** (239 lines); the only
+`viewer/navigate_find.js` differences are cosmetic (main has since been refined further: neon-green
+path line + waypoint marker spheres vs the branch's plain yellow line, purple selection cuboid vs
+yellow). `git merge-base --is-ancestor feat/room-pathfind-graph main` → NOT an ancestor (the branch
+forked before `3f6dbbc` landed and was never fast-forwarded). **The branch has been removed** (0 unique
+commits vs `origin/main`, safe per this project's worktree-hygiene rule — nothing lost).
+
+**`common/room_graph.js` on `main` already carries everything R-SPINE's `route()` step needs:**
+`buildGraph()` (real room-to-room adjacency from door geometry) + `shortestPath()` (Dijkstra, weighted
+by real room-center distance, every hop carrying the real door guid/name). Verified directly (real
+Duplex_ARC.db, plain-Node, `common/room_graph.js` unmodified) that `shortestPath()` correctly serves a
+SPINE-RESTRICTED query — R-SPINE's own wording ("BFS over room_graph edges restricted to spine ∪
+{fixture_room, riser_room}") — when fed a NODE-FILTERED `graph` object: `shortestPath()`'s own edge
+loop already guards `if (!adj[e.a] || !adj[e.b]) return;`, so passing the full edge list with only
+`.nodes` filtered is sufficient — no edge-list filtering, no new algorithm, needed.
+```
+T1 unrestricted shortestPath finds the real A202->A205 path — PASS (baseline, the shipped feature)
+T2 spine={A201} only (A204 excluded): restricted path correctly returns null — PASS (architecturally
+ right: Duplex has no true corridor system here, matching this file's own "spine only exists after
+ R-MERGE on a real corridor building" finding — not a bug in the mechanism)
+T3 widening spine to {A201,A204} recovers the IDENTICAL path T1 found — PASS (proves the filter itself
+ is correct, not "always null")
+T4 the restricted-graph path still carries real door guid/name per hop — PASS (what R-SPINE's conduit
+ step needs)
+```
+4/4 PASS (session scratchpad `witness_rspine_fit.js`, not committed — calculation-only per this task's
+own scope).
+
+**Conclusion, exactly as the coordinator anticipated: R-SPINE is "wire into an existing pathfinder,"
+not "build a new one."** The genuine remaining gap for a full R-SPINE build (NOT attempted this pass,
+correctly scoped as too large — named, not built):
+1. **Spine SELECTION** — `spine(storey) = connected subgraph of rooms with hallwayness >= 0.5` (Task
+ 2's `hallwayness()` formula, now implemented in this session's R-DOOR-SCORE work but reverted from
+ `room_graph.js` — the formula itself is fine in isolation, only its use as a DOOR-scoring discount
+ was disproven; reusing it purely as a spine-membership test is a different, untested claim) over
+ R-MERGEd rooms — not built.
+2. **The `restrictToSpine()` wrapper** — ~10 lines, shown working above, not committed anywhere.
+3. **Conduit polyline generation** (door-center to door-center, hug the longest wall) + the
+ union-of-member-rects containment check — geometry, not pathfinding; the containment LAW was
+ already corrected in Task 4's spec by §POC5 (2026-07-12c), not re-touched here.
+4. Wiring: `buildGraph()` reads straight from `spatial_structure`, so once R-MERGE has been run
+ `--write` on a target building, `buildGraph()` automatically picks up the merged rooms with no
+ further changes needed there.
+
+None of 1-4 requires a new Dijkstra/BFS implementation — the existing one, proven above, is sufficient.
+
+---
+
+# TERMINAL WIRING CLOSED — 2026-07-12d (the two Grind-1 gaps, solved and browser-proven)
+
+Both open Terminal facts from GRIND RESULTS are now fixed, live-verified on localhost, committed
+locally on bim-ootb branch `fix/terminal-rooms-selfheal` (worktree /tmp/wt-terminal-rooms, commit
+3429206 — NOT pushed, PUSH PAUSE):
+
+**Viewer (had zero Terminal rooms end-to-end):** new self-heal patch
+`buildings/patches/Terminal_extracted.db.sql` (+ `viewer/buildings/patches/` copy) carries 59
+rooms / 101 rect rows compiled by the shipped R-MERGE+R-REJECT rules. Applied by the EXISTING
+scene.js loader. Live proof (headless Chromium, real viewer page):
+```
+§PATCH_APPLY Terminal_extracted.db applied (173860 bytes)
+§ROOM_GRAPH nodes=59 doors=135 nonRoomDoors=5 edges=10 deadend=62 orphan=58 ambiguous=0
+```
+59 rooms where there were zero. (Honest note: edges=10 is sparse — Terminal's door-to-room
+binding quality is the next-lane item, same family as the reverted R-DOOR-SCORE.)
+
+**Modeller (served 43 stale pre-stair-exclusion rooms from Terminal_ARC.db):**
+`str_walker_outliner.js` gains `_applyPendingPatch` — a port of the Viewer's proven convention
+(`modeller/patches/.sql`, applied on EVERY open, IDB cache keeps raw bytes). First user:
+`modeller/patches/Terminal_ARC.db.sql` rebuilds the room table (old 12-column schema → 13) with
+the fresh compiled set; the stale ~0.21-stair-overlap room the user saw is gone from the data.
+Live proof (headless Chromium, real modeller page, real `openResident('Terminal')`):
+```
+§PATCH_APPLY Terminal_ARC.db applied (33669 bytes) from ./patches/Terminal_ARC.db.sql
+walker db spaces=101 columns=13
+```
+Witness W-TERMINAL-ROOM-PATCH (scratchpad): both patches double-applied through the repo's own
+sql.js — idempotent, 101 rooms each pass. Patch generation is reproducible:
+`compile_rooms.py --write` on a copy of each target + scratchpad `make_patch.py`.
+
+Remaining known imperfection, stated not hidden: 2 rooms in the new Terminal set still sit ~24%
+over a staircase — below the 0.35 rejection bar, ⚠-flagged by the pipeline itself. Whether 0.35
+should tighten is the deferred threshold decision (Task 0 note above), not part of this fix.
+
+---
+
+## FUTURE SPEC — corridor/hallway findability (2026-07-12, user design note, NOT YET BUILT)
+**"Corridor be future"** — captured here so the thinking isn't lost, deliberately deferred, not part
+of the current R-MERGE/R-REJECT/R-DOOR-SCORE/R-SPINE build. Three surfaces, same underlying signal
+(hallwayness, already measured — `min(aspect/2.697,1) * min(area/10.415,1)`, the HALLWAY template
+mean from Duplex):
+
+1. **TYPE tab — the direct "how do we look for them" answer.** Duplex's hallways are already
+ classified (HALLWAY→CORRIDOR, FOYER→LOBBY): Find → TYPE → CORRIDOR lists every hallway in the
+ building, one press. For unlabeled buildings (Terminal, JKR — compiled rooms are just "R1, R2…"),
+ the classifier tags corridor-shaped rooms by measured shape — long-and-thin with many doors reads
+ as a hallway whether or not anyone named it. Same hallwayness measure the pathfinder (R-DOOR-SCORE/
+ R-SPINE) uses, so search and routing agree by construction, not by two parallel definitions.
+2. **ROOM tab — visible but visually quiet.** Circulation rooms stay in the per-storey list but get a
+ distinct look: a subtle glyph (⇄) and a softer tint, sorted after the primary rooms — the building's
+ skeleton should be visible without shouting over the bedrooms/offices. Matches data already in hand
+ (templates already carry primary vs. supplementary tiers) — the UI renders an existing distinction,
+ nothing new to invent.
+3. **PATH mode — where they earn their keep.** When a route renders, hallway segments are the
+ highway — tint walk-through spaces differently from the two destination rooms. Later, the
+ fire-escape view is the same picture with exits lit (ties to the HBA Safety/Egress spec's RSET
+ concept, `prompts/Viewer/HBA/RESUME_HR_BIM_ASSET.md` §2026-07-12 Safety/Egress section).
+
+Depends on Lane A (R-MERGE/R-REJECT, shipped) + R-SPINE (spine selection, not yet built) landing first
+— this is a consumer of that substrate, not a prerequisite for it.
+
+---
+
+# STAIRWELL-STACK + split-path trace correction — 2026-07-12e (user screenshot follow-up)
+User (with screenshot, `≈ Aras 01 R1`): needle recompute works, "but there is still staircase well
+as a room." Two findings, both fixed and live-verified:
+1. **STAIRWELL-STACK rule (both mirrors, parity 6/6).** Measured: a shaft's per-storey flight
+ covers only ~0.22 of its pocket — under `STAIR_OVERLAP_REJECT=0.35`, which STAYS — but flights
+ STACKED through the same XY cover 1.30–2.23× cumulatively vs 0.37 max for any legitimate room.
+ New reject: cumulative ≥0.50 across ≥3 z-levels. Controls: Duplex 0/21, JKR 0/79 false hits;
+ Terminal exactly the 12 shaft rects on every variant. This ANSWERS the deferred Task-0 threshold
+ question: do NOT lower 0.35 — the shaft is a vertical object, it needed a vertical test.
+2. **Task 0 trace correction — the Viewer's live Terminal source moved.** Current main has a
+ split-build path: `§DB_SPLIT_DETECT` prefers `buildings/Terminal_meta.db` (43 stale rooms — the
+ set the user actually saw, stairwell included) over `Terminal_extracted.db`. The split path also
+ never ran `_applyPendingPatch` — wired in `viewer/streaming.js`, and a new
+ `buildings/patches/Terminal_meta.db.sql` now heals the file the Room lens actually reads.
+ E2E: `§PATCH_APPLY Terminal_meta.db applied` → rooms=40 rects=73, worst remaining stacked-stair
+ overlap 0.37 (<0.50). bim-ootb PR #761.
diff --git a/prompts/Modeller/DISC_Walker/VIEWER_FIND_PANEL_ROOM_ACCURACY.md b/prompts/Modeller/DISC_Walker/VIEWER_FIND_PANEL_ROOM_ACCURACY.md
index 88f5c0faf..9a13a80bd 100644
--- a/prompts/Modeller/DISC_Walker/VIEWER_FIND_PANEL_ROOM_ACCURACY.md
+++ b/prompts/Modeller/DISC_Walker/VIEWER_FIND_PANEL_ROOM_ACCURACY.md
@@ -529,3 +529,54 @@ Not fixed here — out of §7's scope, flagged for whoever next touches the Dupl
UI. A separate, pre-existing `navigate_path.js`/`navigate_engine.js` grid-based A* system already
does free-space camera-flythrough routing (point-to-point, not room-to-room semantic graph) for a
DIFFERENT feature (walk-to-target camera tours) — confirmed distinct in purpose, not duplicated.
+
+## §8 — Room highlight default: box shine-through, not fragmented real-element seams (2026-07-12)
+
+```
+# ⚠ DO NOT REMOVE
+SCOPE: bim-ootb `viewer/navigate_find.js` `_roomSelect()` (~line 1793) + `_drawRoomCuboid()`
+(~line 1517) + `_drawRoomShell()` (~line 1502). User-reported (2026-07-12, live screenshot):
+selecting a room shows visible "cuts and pieces" instead of one clean volume. Traced to source
+before writing this — read this section before touching the code.
+```
+
+**Root cause, confirmed from source (don't re-derive):** `_roomSelect()` has three paths. The one
+that fires whenever `_roomBoundingGuids()` finds real adjacent geometry (the common case — line
+1822, `if (bound.size && zoomBox)`) calls `_drillSelect(bound, ...)`, which lights the room's REAL
+wall/floor/ceiling elements solid + an `OutlinePass` yellow (`0xffd400`) silhouette. A real room is
+usually bounded by several separate real elements (multiple wall segments, floor/ceiling plates) —
+each gets its own silhouette outline, and where two adjacent real elements meet, their outlines
+double up, reading as a brighter yellow seam. **This is not the `§MULTI-RECT` sub-rect mechanism** —
+checked every deployed building DB (`grep room_guid` across all of `deploy/buildings/*_extracted.db`
+schemas) and NONE currently populate it, so that code path never fires on any live building today;
+ruled out, don't chase it. The abstract single-box highlight the user wants already exists —
+`_drawRoomCuboid()` (yellow fill 0.10 opacity + bright wireframe edges, ONE mesh, no seams — line
+1830) — but today it only fires as the FALLBACK, `else if (bound.size)` / `else` branch, i.e. only
+when NO real bounding elements are found. Priority is backwards from what's wanted.
+
+## Task
+1. **Swap `_roomSelect()`'s default:** make the abstract cuboid shine-through (`_drawRoomCuboid`)
+ the PRIMARY highlight for a selected room, not the real-bounding-element yellow silhouette. The
+ real-element highlight (`_drillSelect(bound, ...)` path) should not be the default any more —
+ your call whether to drop it entirely or keep it reachable as a secondary/debug mode, but it
+ must not be what a normal room tap shows.
+2. **Recolor to a softer purple.** Both `_drawRoomCuboid`'s fill (`0xffd400`) and wire
+ (`0xffe83a`) colors, AND `_drawRoomShell`'s room-map fill (`0x4fc3f7`, the "every room" blue
+ shown on Room-lens-open) are candidates — confirm with a screenshot which one(s) the user meant
+ before recoloring both; the selected-room highlight is the one directly discussed this session.
+ Keep opacity in the same low range (fill ~0.10, wire brighter) unless the screenshot shows it
+ needs adjusting — this is a color swap, not a re-design of the translucency model.
+3. Keep `_roomBoundingGuids()`/the real-element lookup itself untouched — only its role in
+ `_roomSelect()`'s priority changes. Do not touch `_allRoomVolumes()`, the habitability filter, or
+ any Task 0-7 machinery above — out of scope.
+
+## Witness
+Whitebox first (`§ROOM_CUBOID_FALLBACK`/`§ROOM_CLIP` log lines already exist — extend/rename as
+needed so the log states which highlight mode actually rendered), then a live screenshot on a
+multi-wall-segment room (HHS or Duplex) showing ONE clean purple volume, no visible seams, where the
+old code would have shown the fragmented yellow-silhouette look from the reported screenshot.
+
+## DONE WHEN
+A room tap shows the single translucent purple box/wireframe by default (no real-element seam
+artifacts), verified live on a building where the old path previously showed fragmented real
+geometry, § log confirms which highlight mode fired.
diff --git a/prompts/TRILOGY_STALE_CODE_AUDIT.md b/prompts/TRILOGY_STALE_CODE_AUDIT.md
new file mode 100644
index 000000000..46c554b3b
--- /dev/null
+++ b/prompts/TRILOGY_STALE_CODE_AUDIT.md
@@ -0,0 +1,397 @@
+
+# TRILOGY STALE-CODE AUDIT — mark dead/superseded code across Modeller+Viewer+ERP (2026-07-12, Fable one-shot)
+
+```
+# ⚠ DO NOT REMOVE
+SCOPE: bim-ootb `modeller/`, `viewer/`, `erp/` (the "trilogy" — 277 top-level files, ~177K lines,
+confirmed this session) — top-level files only (not node_modules/tests/build). Generalizes
+`viewer/2d.html`'s pilot case (below) into a full trilogy sweep. User's own framing (2026-07-12):
+"make the prompt more general... should it be done by Fable one shot, to review the whole codebase
+in use by trilogy only, and mark which are not or stale for removal?" Answer worked out below —
+**one-shot for DISCOVERY/MARKING only, NOT for deletion.** Read this whole file before running
+anything. PUSH PAUSE LIFTED for this repo — commit locally, push the REPORT when done (docs-only,
+no PR ceremony needed for a marked-report commit). Actual code removal is explicitly OUT of this
+task's scope (see "Why discovery and deletion are split," below) — do not delete files in this pass
+even if a finding looks obvious.
+```
+
+## Why discovery and deletion are split (read before objecting to the scope)
+This project's own standing discipline, used consistently all session (DiscWalk branch closeout,
+FIND_PANEL_PLANT_ROOM_GATE_FIX, WALKER_FIXTURE_RENDER investigation): **investigation tasks report,
+they don't act** — a separate pass (often a different session, always independently verified) does
+the actual change, once a human/Manager has reviewed the findings. At trilogy scale (277 files), a
+single one-shot pass that BOTH discovers AND deletes has no review checkpoint before something
+real is destroyed — that's the wrong risk shape for this size of change, however good the evidence
+looks in the moment. One-shot IS appropriate for the discovery/marking half (see feasibility below);
+it is not appropriate for bundling deletion into the same unreviewed pass.
+
+## Feasibility check (done before writing this, not assumed)
+- 54 Playwright spec files exist (`tests/specs/*.spec.js`) — a real, substantial exercise of the live
+ apps, confirmed this session. Running the full suite with coverage collection once is mechanically
+ tractable in a single session (each spec is `@fast`-tagged or a normal integration test, not a
+ multi-hour job).
+- Three real entry points anchor the "trilogy": `modeller/modeller.html`, `viewer/viewer.html`,
+ `erp/idempiere.html` (there are also `index.html`/`index2.html`/`gallery.html`/`LargeCity.html` at
+ the repo root — landing/launcher pages that route INTO the trilogy; treat these as additional
+ entry points too, not part of the trilogy being audited).
+
+## Method — same higher-leverage approach as the 2d.html pilot, applied trilogy-wide
+**Two complementary passes, run both — they catch different things:**
+
+1. **Static reachability (highest-confidence signal, do this FIRST — it's cheap and decisive).**
+ From the entry points above, trace every `