Part of the ADR-0004 vanilla-shell investment track (see docs/ADR-0004-ui-shell.md); scoped from the 2026-08-03 architecture review.
Goal
Once #586 and #587 land, add mechanical checks to build/check-boundaries.mjs that lock in what they establish — one overlay-lifecycle implementation and a registry-driven panel model — so the six-copy-pasted-overlays problem #586 fixes can't silently regrow the same way it grew the first time.
Context
The repo's layering (src/core/ pure logic, src/net/ injected fetch, src/application/ no UI/editor imports, model/layouts <- application <- UI in src/dashboard/, editor seams, etc.) holds because build/check-boundaries.mjs enforces it mechanically, not because reviewers remember the rules — that's the whole reason the check exists.
Nothing today stops the pattern #586 is fixing from regrowing: #586's own evidence is that six independent overlay lifecycles (cell drawer, rows viewer, doc pane, detached-view, dialog-shell, popover) accumulated over time with five different Escape policies and five different focus-restore patterns, with no gate that would have caught a seventh copy being added. Once #586 collapses those into one SurfaceLifecycle + a docked inspectorHost slot, and #587 makes side panels registry-driven, a new feature that copies the old pattern instead of using the new primitive would pass npm test and tsc --noEmit today with nothing flagging it as a regression.
Deliverables (after #586 and #587 merge)
Add checks to build/check-boundaries.mjs, in its existing deterministic, no-dependency style:
- Forbid new panel mounts outside
SurfaceLifecycle. Grep-level check that document.body.append/document.body.appendChild (panel-shaped mounts, not arbitrary DOM utility use) appears only inside the SurfaceLifecycle module itself, with an explicit allowlist for surfaces #586 deliberately keeps separate (toast; dialog-shell.ts/popover.ts if they remain distinct implementations for their own documented reasons per #586's body).
- Forbid new
position: fixed panel CSS in src/styles.css outside an explicit allowlist. #586 deletes the .cd-backdrop overlay CSS as part of its own work; this check stops a new rule with the same shape from being added later.
- Assert exactly one overlay-lifecycle implementation. Grep-level check that no new capture-phase
Escape/keydown handler implementing panel-close semantics exists outside SurfaceLifecycle (and dialog-shell.ts/popover.ts if the allowlist in (1) keeps them separate) — mirroring #586's finding of five independent Escape policies before this issue's checks existed.
Each check should follow check-boundaries.mjs's existing conventions: deterministic (no external deps, no network), fails with a clear file:line message pointing at the offending site and the allowed alternative, and is fast enough to run in the existing pretest/check:arch step.
Tests
Acceptance criteria
build/check-boundaries.mjs fails the build if a new panel is mounted via document.body.append/appendChild outside SurfaceLifecycle and the documented allowlist.
build/check-boundaries.mjs fails the build if new position: fixed panel CSS is added to src/styles.css outside the documented allowlist.
build/check-boundaries.mjs fails the build if a new capture-phase Escape/close handler is added outside SurfaceLifecycle (or the allowlisted exceptions).
- All three checks run as part of the existing architecture-check step with no new runtime dependency.
npm test (coverage gate) and tsc --noEmit pass.
Non-goals
Inherited from #586 (phase 1 of #593) — an unguarded JS/CSS constant duplication
Surfaced while reviewing PR #596 and verified. This is the guardrail-shaped part of a finding
whose behavioural half is filed as #597, and it belongs here because mechanically forbidding this
class of regrowth is this issue's whole purpose.
#586's dock-aware width ceiling reserves space for everything beside the inspector:
reservedPx: state.sidebarPx + HANDLE_PX * 2 // src/ui/app-shell.ts:174
const HANDLE_PX = 7; // src/ui/app-shell.ts:109
The real handle width is declared independently in CSS:
.col-resize, .inspector-resize { width: 7px; cursor: col-resize; } /* src/styles.css:901 */
Nothing links the two. A CSS-only edit to the handle width leaves reservedPx wrong, which
silently narrows the centre surface below the minimum CENTRE_MIN_PX is supposed to guarantee —
and no test fails, because happy-dom evaluates no CSS layout and the e2e assertions are
inequalities rather than exact geometry.
This repo already treats JS↔CSS drift as worth a mechanical gate: FONT_BYTE_BUDGET and the whole
type ramp are asserted by tests/unit/typography-contract.test.js precisely so a CSS edit cannot
silently change a JS-side assumption.
Extra acceptance for this inherited item
Related
Part of the ADR-0004 vanilla-shell investment track (see
docs/ADR-0004-ui-shell.md); scoped from the 2026-08-03 architecture review.Goal
Once #586 and #587 land, add mechanical checks to
build/check-boundaries.mjsthat lock in what they establish — one overlay-lifecycle implementation and a registry-driven panel model — so the six-copy-pasted-overlays problem #586 fixes can't silently regrow the same way it grew the first time.Context
The repo's layering (
src/core/pure logic,src/net/injected fetch,src/application/no UI/editor imports,model/layouts <- application <- UIinsrc/dashboard/, editor seams, etc.) holds becausebuild/check-boundaries.mjsenforces it mechanically, not because reviewers remember the rules — that's the whole reason the check exists.Nothing today stops the pattern #586 is fixing from regrowing:
#586's own evidence is that six independent overlay lifecycles (cell drawer, rows viewer, doc pane, detached-view, dialog-shell, popover) accumulated over time with five different Escape policies and five different focus-restore patterns, with no gate that would have caught a seventh copy being added. Once #586 collapses those into oneSurfaceLifecycle+ a dockedinspectorHostslot, and #587 makes side panels registry-driven, a new feature that copies the old pattern instead of using the new primitive would passnpm testandtsc --noEmittoday with nothing flagging it as a regression.Deliverables (after #586 and #587 merge)
Add checks to
build/check-boundaries.mjs, in its existing deterministic, no-dependency style:SurfaceLifecycle. Grep-level check thatdocument.body.append/document.body.appendChild(panel-shaped mounts, not arbitrary DOM utility use) appears only inside theSurfaceLifecyclemodule itself, with an explicit allowlist for surfaces#586deliberately keeps separate (toast;dialog-shell.ts/popover.tsif they remain distinct implementations for their own documented reasons per#586's body).position: fixedpanel CSS insrc/styles.cssoutside an explicit allowlist.#586deletes the.cd-backdropoverlay CSS as part of its own work; this check stops a new rule with the same shape from being added later.Escape/keydownhandler implementing panel-close semantics exists outsideSurfaceLifecycle(anddialog-shell.ts/popover.tsif the allowlist in (1) keeps them separate) — mirroring#586's finding of five independent Escape policies before this issue's checks existed.Each check should follow
check-boundaries.mjs's existing conventions: deterministic (no external deps, no network), fails with a clear file:line message pointing at the offending site and the allowed alternative, and is fast enough to run in the existingpretest/check:archstep.Tests
check-boundaries.mjs's existing checks are tested — e.g. a scratch file that intentionally violates each new rule) proves each of the three new checks fails on a violation and passes on the current, post-refactor(shell): one SurfaceLifecycle primitive and a docked right-inspector slot #586/refactor(shell): a side-panel registry so adding a navigation panel is a one-file change #587 tree.npm test(coverage gate, ifcheck-boundaries.mjs's existing checks have unit coverage) andtsc --noEmitpass.npm run check:arch(or wherevercheck-boundaries.mjsis wired) runs clean onmainafter refactor(shell): one SurfaceLifecycle primitive and a docked right-inspector slot #586/refactor(shell): a side-panel registry so adding a navigation panel is a one-file change #587 land, before this issue's new rules are added — establishing the pre-check baseline the new rules must not immediately break.Acceptance criteria
build/check-boundaries.mjsfails the build if a new panel is mounted viadocument.body.append/appendChildoutsideSurfaceLifecycleand the documented allowlist.build/check-boundaries.mjsfails the build if newposition: fixedpanel CSS is added tosrc/styles.cssoutside the documented allowlist.build/check-boundaries.mjsfails the build if a new capture-phase Escape/close handler is added outsideSurfaceLifecycle(or the allowlisted exceptions).npm test(coverage gate) andtsc --noEmitpass.Non-goals
SurfaceLifecycle, the registry, or panel behavior itself — this issue only adds enforcement after refactor(shell): one SurfaceLifecycle primitive and a docked right-inspector slot #586/refactor(shell): a side-panel registry so adding a navigation panel is a one-file change #587 establish the pattern.check-boundaries.mjsstyle, consistent with its other checks.Inherited from #586 (phase 1 of #593) — an unguarded JS/CSS constant duplication
Surfaced while reviewing PR #596 and verified. This is the guardrail-shaped part of a finding
whose behavioural half is filed as #597, and it belongs here because mechanically forbidding this
class of regrowth is this issue's whole purpose.
#586's dock-aware width ceiling reserves space for everything beside the inspector:
The real handle width is declared independently in CSS:
Nothing links the two. A CSS-only edit to the handle width leaves
reservedPxwrong, whichsilently narrows the centre surface below the minimum
CENTRE_MIN_PXis supposed to guarantee —and no test fails, because happy-dom evaluates no CSS layout and the e2e assertions are
inequalities rather than exact geometry.
This repo already treats JS↔CSS drift as worth a mechanical gate:
FONT_BYTE_BUDGETand the wholetype ramp are asserted by
tests/unit/typography-contract.test.jsprecisely so a CSS edit cannotsilently change a JS-side assumption.
Extra acceptance for this inherited item
unnoticed — either single-source it (a CSS custom property or generated token JS reads, or a
JS constant the build injects into CSS) or add a check that fails when the two disagree.
HANDLE_PX/.col-resize/.inspector-resizeas its first case.Related
SurfaceLifecycleprimitive and docked right-inspector slot; this issue depends on it merging first.docs/ADR-0004-ui-shell.md)