build(#592): lock in the #586/#587 shell primitive guardrails - #672
Conversation
Add three mechanical check:arch guards so the six-copy-pasted-overlays problem #586 fixed cannot silently regrow: - shell-body-mount: a new Document.body.append/.appendChild call outside an exact, reviewed baseline of sanctioned lifecycle/primitive scopes. - shell-fixed-position: a new position: fixed CSS declaration in src/styles.css outside the current selector/at-rule snapshot, via a focused CSS lexical scanner (no CSS parser dependency). - shell-capture-escape: a new global capture-phase Escape keydown lifecycle outside SurfaceLifecycle and its exact documented exceptions/non-panel gesture exclusions. The two source-level rules share one real-TypeScript-parser batch (findShellGuardrailSourceContractViolations, build/lib/check-legacy- owners.mjs), reusing this repo's existing withParsedSources/walkTree/ SyntaxKind idiom -- no new parser dependency. Every fingerprint in the frozen policy tables was generated by running the analyzers over the live tree and reviewing each occurrence, including two the issue's own attachment underestimated (dashboard-chart-interaction.ts's beginSelection chart-selection Escape cancellation, and app.ts's export-progress/download- anchor body mounts). Also closes the inherited #586/#593-phase-1 finding: an independent tests/unit/resize-handle-thickness-contract.test.js proves app-shell.ts's HANDLE_PX and styles.css's .col-resize/.inspector-resize width cannot drift unnoticed. Enforcement-only -- no runtime UI/DOM/CSS behavior changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 1Reviewed head: I reviewed the complete six-file PR against #592 and the supplied acceptance/invariant map. The production wiring is present: all three new architecture checks are called from P1 — Resolve aliases/handlers lexically; the current file-global maps can silently miss violations
This creates direct false-negative shapes. For example, a rogue Please resolve bindings at each use site through the lexical ancestor chain (or an equivalent TypeScript binding mechanism), and fail closed when the visible binding is ambiguous/unresolvable. Add sabotage fixtures with same-named parameters/locals in sibling and nested scopes for the Document target, capture-options alias, and handler alias. P1 — The frozen snapshots are only upper-bound/membership allowlists, not exact snapshots
That violates the supplied invariant requiring exact Please compare the discovered baseline and policy as exact multisets in both directions. For CSS, include a count per P1 — The HANDLE_PX drift test misses individual CSS overrides
For example, this still passes the shipped contract while making the actual inspector handle 8px in normal CSS cascade order: .col-resize, .inspector-resize { width: 7px; }
.inspector-resize { width: 8px; }The same hole exists for a media-query or more-specific override. That means the inherited acceptance criterion — JS reserved thickness and CSS-declared handle thickness cannot drift unnoticed — is not mechanically guaranteed. Please scan every width declaration capable of targeting VERDICT: REVISE |
- buildGlobalAliasMap/buildFunctionDeclMap/buildCaptureAliasMap now key every
binding by its own declaring scope (a new scopeOwnerOf/scopeChain/
lookupInScopeChain lexical-resolution layer) instead of one flat file-wide
Map<name, value>. A later sibling `doc: Window` no longer overwrites an
earlier `doc: Document`, a sibling scope's `opts = false` no longer erases
a real scope's `{ capture: true }` alias, and a same-named nested helper no
longer resolves in place of the real addEventListener handler.
- Fixing the scoping exposed a real (previously accidental) detection gap:
`openInDetachedTab`'s `mount(({ doc, ... }: MountCtx) => ...)` destructures
`doc: Document` from a named interface type, a shape none of the alias
rules modeled directly — it only worked before via an unrelated same-name
binding's file-wide leak. Added an explicit, narrowly-scoped MountCtx
recognition rule so explain-graph.ts's two real capture-Escape listeners
resolve on their own merits.
- shellBodyMountViolations/shellCaptureEscapeViolations now also compare the
frozen policy against the tree in the missing direction: any approved
scope whose occurrence count dropped below its baseline is flagged, not
just excess occurrences. Gated on declaredScopeKeys (does this scope even
exist in what was scanned) so this suite's many single-scope synthetic
fixtures for multi-entry files (popover.ts, app.ts, detached-view.ts,
explain-graph.ts, dashboard-tile-gestures.ts) aren't misread as "missing".
- findShellFixedPositionViolations is now count-based (a duplicate of an
approved selector/at-rule is flagged, not just a brand-new one). The
reverse direction (an approved fingerprint that vanished from the CSS
entirely) is a new, separate export, findShellFixedPositionMissingBaseline
Violations — kept separate because CSS has no structural way to tell a
partial test fixture from the real, complete stylesheet; wired into
check:arch against the real src/styles.css.
- extractSharedResizeWidthPx now also catches a standalone or media-scoped
single-class override of .col-resize/.inspector-resize, not just the
shared grouped rule.
- Added same-file scope-shadowing sabotage fixtures (doc alias, capture
options, handler), missing-baseline-entry sabotage for both TS guards,
duplicate-approved-CSS and missing-baseline-CSS sabotage (against the real
styles.css), and CSS-override sabotage for the resize-handle contract.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 2Previously reviewed head: Reviewed head: I compared the new head directly with the pass-1 SHA and re-reviewed the complete six-file PR against the pass-2 delivery contract. The fix commit is one commit ahead of the old review head. Canonical CI for this exact new head is green: Pass-1 reassessment
P1 — The new “lexical” resolver still ignores block scope, so valid
|
Fix four ChatGPT PR-review findings on the #592 shell guardrails: - scopeOwnerOf/innermostScopeNode collapsed same-function block-local let/const shadows (if/loop/bare-block bindings) into one flat per-function bucket, letting a block-local shadow of a Document/Window alias, capture-options alias, or named handler silently hide (or be hidden by) a real occurrence elsewhere in the same function. Added innermostLexicalScopeNode/isBlockScopeNode for real block-scoped resolution in scopeOwnerOf (used by every alias/handler/capture table), while innermostScopeNode stays function-only for the SurfaceLifecycle-composition and scope-PATH callers that need it. bodyMountCandidates' own bodyAliasNames Set was also file-global and is now a scoped scope->Map<name,true> resolved the same way. - resolveObjectCaptureLiteral only recognized an identifier-named `capture` PropertyAssignment, so `{'capture': true}`, `{['capture']: true}`, and shorthand `{ capture }` fell through to the no-key branch and resolved provably false instead of failing closed. Added staticPropertyKeyName to resolve string/computed-string-literal/ shorthand keys and recurse through resolveCaptureFlag for the shorthand value reference; any other unresolvable key now fails closed to null. - The CSS fixed-position scanner recorded only the nearest enclosing at-rule, so wrapping an already-approved rule in an additional outer at-rule produced the identical fingerprint. scanFixedPositionDeclarations now records the full chain of enclosing at-rules, outermost first, joined with ' > '. Tests added for nested block-local shadowing (body-mount and capture-escape, both directions), the three capture-key shapes, and the nested-at-rule-chain fingerprint (forward + missing-baseline). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 3Previously reviewed head: Reviewed head: I compared the new head directly with the pass-2 SHA and re-reviewed the complete six-file PR against the pass-3 contract/invariant map. The new head is exactly one commit ahead of the prior review head. Canonical CI for this exact SHA is green (test, build, e2e, bundle, Docker smoke, CI gate). The PR still changes only Pass-2 reassessment
P1 — The new lexical resolver still models scope by AST block, not by declaration semantics
function rogue(doc: Document) {
{ var d = doc; }
d.body.appendChild(panel);
}
function rogue(doc: Document) {
for (const doc = window; false; ) {}
doc.body.appendChild(panel);
}The analyzer leaves the loop-local Please make binding ownership declaration-aware: P1 — Capture options still fail open when a known
|
- Made every #592 scope-resolution table (buildGlobalAliasMap, the handler- alias table, buildCaptureAliasMap, and bodyMountCandidates' body-alias table) declaration-kind-aware via new declarationScopeOwnerOf/ varDeclarationScope helpers: a `var` bound inside a nested block (or a for-loop header) is now correctly function-scoped instead of vanishing from analysis outside that block, and a `let`/`const` bound in a for/for-in/for-of HEADER now gets its own loop-construct scope (isBlockScopeNode now recognizes those three statement kinds) instead of clobbering a same-named outer binding in the same enclosing scope map. - resolveObjectCaptureLiteral now respects real object-literal property evaluation order: it tracks only the LAST capture-affecting event across node.properties, so a spread or unresolvable key that comes AFTER an explicit `capture` property correctly makes the result unresolvable (`{ capture: false, ...{ capture: true } }` no longer resolves to the earlier `false` and silently escapes the capture-Escape guard). - bodyMountCandidates now also recognizes a destructuring alias of Document.body (`const { body } = document`, and the renamed `const { body: host } = document`) as a direct body mount, matching the existing plain-identifier alias handling. - scanFixedPositionDeclarations now decodes real CSS identifier escapes (decodeCssEscapes) before comparing property/value text, so spec-legal escaped spellings like `\70osition: fixed;` or `position: \66ixed;` are recognized exactly like the literal `position: fixed` they decode to. - resize-handle-thickness-contract.test.js's extractSharedResizeWidthPx now recognizes the .col-resize/.inspector-resize classes inside compound (`.inspector-resize.dragging`) and descendant (`.shell .inspector-resize`) selectors, not just an exact selector-list membership match — while still excluding pseudo-element selectors (`::before`/`::after`), which style an unrelated generated box. - Added sabotage/positive fixtures for every case above across shell-guardrails-arch.test.ts and resize-handle-thickness-contract.test.js. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…eal TypeScript checker Root-cause circuit breaker (per-issue-cycle.md): three formal ChatGPT code-review passes (570e089, ad4f71f, e76c8a7) each found and fixed a real defect in build/lib/check-legacy-owners.mjs's hand-rolled, Map-based scope-resolution layer (scopeOwnerOf/scopeChain/declarationScopeOwnerOf/ buildGlobalAliasMap/buildFunctionDeclMap/buildCaptureAliasMap/a body-alias scope map) backing the shell-body-mount and shell-capture-escape guards — flat file-wide maps (pass 1), same-function block shadowing not modeled (pass 2), var/for-loop-header declaration-kind-unaware scoping plus object-literal evaluation order (pass 3). All three were variants of ONE root cause: re-deriving JavaScript/TypeScript binding semantics by hand instead of asking the real TypeScript binder. withParsedSources's underlying Project (already constructed for every batch, previously discarded down to just its SourceFile) exposes a real checker: Checker with genuine binder symbol resolution (checker.getSymbolAtLocation). withParsedSources/withParsedSource now hand that checker to their callback alongside the SourceFile, and every place that answered "what does this identifier resolve to" now resolves it through the checker against the identifier's real declaration instead: - resolveGlobalKind (Document/Window classification) + a new classifyGlobalDeclaration inspect the resolved declaration's own type annotation, covering bare document/window (which resolve to their own lib.dom.d.ts ambient declarations — no bare-identifier special case needed), typed parameters/variables, destructuring renames, and the narrow MountCtx exception. - resolveHandlerNode resolves an addEventListener handler identifier to its real FunctionDeclaration/arrow-or-function-expression-initializer declaration. - resolveCaptureFlag resolves a capture-options identifier through its real VariableDeclaration initializer, including the ShorthandPropertyAssignment special case (getShorthandAssignmentValueSymbol) the checker itself requires. - resolvesToDocumentBody replaces the body-alias map entirely, resolving a receiver on demand instead of pre-walking the whole file. Deleted outright: scopeOwnerOf, scopeChain, declarationScopeOwnerOf, varDeclarationScope, owningDeclarationList, isVarDeclarationList, innermostLexicalScopeNode, isBlockScopeNode, lookupInScopeChain, buildGlobalAliasMap, buildFunctionDeclMap, buildCaptureAliasMap. Candidate discovery (the .appendChild/.append/addEventListener/Escape-comparison AST shapes) and the capture-options evaluator's own real object-literal evaluation-order logic are unchanged; the CSS shell-fixed-position scanner is untouched. No policy fingerprint, diagnostic shape, or public export changed. Added resolver-level tests mirroring the pre-implementation spike (same- function block shadow, for-loop-header shadow, sibling-scope non-pollution, correct reversion after a shadow's scope ends) for both the Document/Window and handler/capture-alias questions. All existing sabotage fixtures from the three prior review-fix commits still pass, now for the real structural reason. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 1Reviewed head: The checker-based binding refactor is a material improvement: it removes the hand-rolled lexical scope tables that caused the earlier sibling/block/ P1 — the TypeScript “exact snapshot” reverse check still skips whole-scope and whole-file deletionBoth const declaredScopes = declaredScopeKeys(sourceFile);
...
if (!declaredScopes.has(key)) continue;That only detects a count drop while the approved function/scope still exists. If The shipped “loses its ONLY occurrence” tests do not cover this: Action: add a complete-tree reverse comparison at batch level (or an explicit P1 — binding identity is now correct, but alias values are still resolved from stale declaration initializers
const onKey = (e) => { if (e.key === 'Escape') close(); };
let opts = { capture: false };
opts = { capture: true };
document.addEventListener('keydown', onKey, opts);is a real capture-phase Escape listener, but the guard resolves Action: never treat a mutable alias as provably clean from its declaration initializer alone. For capture options, the simplest sound behavior is to allow only immutable/provable forms (e.g. inline object literals and primitive P1 — Document/Window classification still depends on the type/property spelling, so ordinary aliases bypass both source guardsThe new checker is used to resolve the binding, but type Doc = Document;
function rogue(doc: Doc) {
doc.body.appendChild(panel);
}resolves There is also a narrower destructuring spelling gap: Action: resolve type aliases through the checker (or recursively inspect their resolved type-alias declaration) instead of keying only on the literal annotation text; reuse a static-property-name helper for binding-element property names so identifier/string/static-computed spellings are equivalent. Add both sabotage forms. P1 — the Escape-semantic filter has a direct constant-alias bypass
const ESC = 'Escape';
const onKey = (e) => { if (e.key === ESC) close(); };
document.addEventListener('keydown', onKey, true);It is nevertheless exactly a global capture-phase Escape lifecycle. A loose Action: at minimum resolve local constant string aliases and the equality operators JavaScript permits. More robustly, make the “clean non-Escape” classification conservative: if key/code-dependent behavior cannot be proven non-Escape, fail closed rather than classifying it clean. Add sabotage tests for a local P1 — the HANDLE_PX drift test silently ignores valid width overrides it cannot parse
/\bwidth\s*:\s*(-?\d+(?:\.\d+)?)px\s*;/gSo this override is ignored entirely: .inspector-resize { width: 8px !important; }The canonical Action: detect every P2 — fixed-position fingerprints ignore enclosing nested style-rule contextThe CSS scanner now records the full enclosing at-rule chain, but for style rules it records only .wrapper {
.auth-host { position: fixed; }
}the stored fingerprint is still selector Action: include enclosing style-rule context in the fingerprint, or conservatively reject nested style-rule Verified positives
VERDICT: REVISE |
Five accepted ChatGPT PR-review findings against the #592 shell-guardrail checker-refactor commit: - Add findShellGuardrailMissingBaselineViolations, a complete-tree strict reverse pass for shell-body-mount/shell-capture-escape that (unlike the softened forward-check reverse pass) catches a whole approved function deleted outright or a whole approved file missing from the batch, wired into check-boundaries.mjs alongside the existing forward check. - resolveCaptureFlag/resolveHandlerNode now require a genuine const VariableDeclaration before trusting its initializer, so a let/var alias reassigned after declaration (capture flag or handler function) fails closed to "uncheckable" instead of resolving to its stale initial value. - classifyGlobalDeclaration resolves a local type-alias chain (type Doc = Document) through the real checker instead of only literal type-reference names, and both classifyGlobalDeclaration and resolvesToDocumentBody now recognize a quoted destructuring rename key (const { 'body': host } = document) via a shared bindingElementSourceKeyName helper. - The resize-handle CSS width extractor now counts every width: declaration targeting the resize classes instead of silently skipping a !important suffix or a non-literal (calc()/var()) value — an unconvertible value becomes a NaN sentinel that can never falsely satisfy the exact-equality contract. - scanFixedPositionDeclarations now marks a position: fixed declaration nested inside another plain style rule (real CSS nesting) as nested: true; findShellFixedPositionViolations unconditionally flags it instead of fingerprint-matching it against the approved baseline, and the missing- baseline reverse pass no longer treats it as "still present". Tests added alongside each fix in shell-guardrails-arch.test.ts and resize-handle-thickness-contract.test.js. Full local gate green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 2Previously reviewed head: Reviewed head: Base: I compared the new head directly with the previous review SHA and re-reviewed the complete six-file PR against the supplied pass-2 contract. The new revision is one commit ahead. Canonical CI for this exact head is green: the The complete PR still changes only Pass-1 reassessment
P1 — Value-flow is still unsound for
|
Close five accepted ChatGPT PR #672 review-pass-2 findings against the shell-guardrail architecture checks: - resolveCaptureFlag now fails closed when a resolved const capture-options object literal is later mutated via a `.capture`/`['capture']` property write anywhere in the file (hasCapturePropertyMutation), not just on a whole-binding let/var reassignment. - resolvesToDocumentBody now also resolves a let/var binding's later whole-binding reassignment (laterAssignmentResolvesToDocumentBody), so a real `let body = ...; body = document.body; body.appendChild(x)` mount is no longer invisible to the shell-body-mount guard. - containsEscapeSemantics now resolves a const identifier alias of the 'Escape' literal (resolveStringLiteralValue/classifyEscapeComparison) in both `===`/`!==` comparisons and `switch` case values, and fails closed (treats as a possible Escape listener) on any unresolvable comparison value instead of silently dropping it as 'clean'. - scanFixedPositionDeclarations' processDeclaration now searches outward through the frame stack for the nearest enclosing style-rule frame instead of bailing on a single innermost at-rule frame, so a bare `position: fixed` declaration nested inside an at-rule nested inside a rule is no longer invisible to the shell-fixed-position guard. - extractSharedResizeWidthPx's value regex now treats end-of-rule-body as a valid declaration terminator alongside `;`, so a `width` declaration with no trailing semicolon before the closing brace is no longer silently skipped. - findShellGuardrailSourceContractViolations gains a `completeTree` option that folds the complete-tree reverse-baseline check into its own shared parser batch; production wiring in check-boundaries.mjs now uses that option instead of a second, separate call to findShellGuardrailMissingBaselineViolations, so check:arch no longer opens two native TypeScript-parser batches for one rule. Adds sabotage/characterization tests for every fix, plus a completeTree equivalence test proving the folded batch matches the union of running the forward and reverse checks separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 3Previously reviewed head: Reviewed head: I compared the new head directly with the pass-2 SHA and re-reviewed the complete six-file PR against the pass-3 contract. The new head is one commit ahead. Canonical CI run 1531 is green for this exact head: Pass-2 reassessmentThe exact pass-2 repros are materially fixed: direct P1 —
|
Close five confirmed shell-guardrail escapes: resolveGlobalKind now tracks a let/var Document/Window receiver alias's later reassignment (not just its initializer), hasCapturePropertyMutation now recognizes compound assignment operators and alias-mediated writes on the same capture-options object, shell-capture-escape candidate discovery now recognizes a bracket-spelled addEventListener callee and a const alias of the 'keydown' event-name literal, the independent resize-handle width/property CSS extractor now decodes CSS identifier escapes, and resolvesToDocumentBody/laterAssignmentResolvesToDocumentBody now carry a cycle-safe visited-binding set so an alias-reassignment cycle can no longer crash the architecture guard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
|
Coordinator note — merging without a formal Two formal ChatGPT
Current head |
When a code-review session's final pass under its 3-pass cap completes without certifying (`fixed-await-push`, `no-accepted-findings`, or `session-cap-exhausted` at that final pass), the coordinator now automatically asks ChatGPT one holistic consultation in the same conversation before the merge gate: is the underlying approach sound, does the plan need to change, and does it still have any concern at all. A clean `VERDICT: SHIP` from that round is itself sufficient certification (same SHA/CI/branch-protection checks, no human step); `VERDICT: REVISE` or an unparseable answer proceeds to the existing FULL STOP. Observed live on /ship 592 (PR #672): this ad hoc consultation, improvised by hand twice, was the signal that let the human make a good call both times a formal review session exhausted its cap without converging. Also fixes three now-stale "drive the tab manually" prescriptions in review-loops.md (the pass-cap continuation, stalled-generation recovery, and the general ad hoc consultation) — the chatgpt-review skill's own current rule forbids manual DOM driving, and both loops' runner agents already retry stalled/incomplete generations automatically via `--session`, so no manual recovery step was ever actually needed. Replaced with the same `issue`-mode + `--seed-from-session` mechanism the new consultation uses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
What & why
Part of the ADR-0004 vanilla-shell investment track. Closes #592.
Once #586 (
SurfaceLifecycle+ dockedinspectorHost) and #587 (side-panel registry)landed, nothing mechanically stopped the six-copy-pasted-overlays problem #586 fixed
from regrowing. This PR adds three deterministic
check:archguards that lock in whatthose two issues established:
shell-body-mount— a newDocument.body.append/.appendChildcall fails thebuild unless it matches an exact, reviewed baseline snapshot of sanctioned
lifecycle/primitive scopes (11 scopes: the
SurfaceLifecycle-backed cell-detailoverlay in
results.ts,dialog-shell.ts, bothpopover.tsprimitive families,toast.ts, bothdetached-view.tsmount paths,menu.ts,shortcuts.ts's modal,and
app.ts's export-progress surface + download anchor). Theresults.tsexceptionadditionally requires its
openSurfaceLifecycle(...)composition to stay in scope —losing that call while keeping the body mount fails.
shell-fixed-position— a newposition: fixedselector insrc/styles.cssfails unless it matches the exact current selector/at-rule snapshot (14 declarations),
via a hand-written comment/string/escape/brace-aware CSS lexical scanner (no CSS
parser dependency).
shell-capture-escape— a new global capture-phase Escapekeydownlifecyclefails unless it matches the canonical
SurfaceLifecyclescope, one of 5 distinctpre-existing panel exceptions (
dialog-shell.ts,popover.ts×2,results.ts's DataPane,
explain-graph.ts×2,menu.ts), or one of 3 non-panel gesture-cancellationexclusions (
dashboard-tile-gestures.ts×2,dashboard-chart-interaction.ts'sbeginSelection— Escape there cancels a chart-range selection, not a panel close).Non-Escape capture-phase keydown listeners (e.g.
dashboard.ts's activity/nav-highlight trackers) are excluded structurally before any policy lookup, not via
an exception fingerprint.
Rules 1 and 3 share one real-TypeScript-parser batch
(
findShellGuardrailSourceContractViolations,build/lib/check-legacy-owners.mjs),reusing this repo's existing
withParsedSources/walkTree/SyntaxKindidiom — no newparser dependency, and no unsound textual prefilter gating it.
Also closes the "Inherited from #586" item: an independent
tests/unit/resize-handle-thickness-contract.test.jsprovessrc/ui/app-shell.ts'sHANDLE_PXandsrc/styles.css's.col-resize/.inspector-resizewidth cannot driftapart unnoticed (chosen over a CSS-custom-property refactor, to keep this unit
enforcement-only).
Enforcement-only. No
SurfaceLifecycle, side-panel registry,inspectorHost,Escape ordering, gesture-cancellation behavior, or runtime UI/CSS composition changed —
verified via
git diff --stat: onlybuild/check-boundaries.mjs,build/lib/check-legacy-owners.mjs/.d.mts, two new test files, andCHANGELOG.mdchanged.
Delivery contract, invariant map, and decision rationale (why
results.ts/explain-graph.ts/menu.ts's existing Escape handlers stay allowlisted rather thanmigrated, why the chart-selection exclusion is a separate category from panel-close
exceptions) were established through a ChatGPT-authored / Fable-reviewed plan (3 review
passes, all findings incorporated) — see the ship-log comment on #592 for the full
handoff.
Sabotage verification
Five real, temporary mutations against production files, each confirmed to fail
check:arch(or the new focused test) and then restored from exact original bytes:toast.ts→shell-body-mountviolation.position: fixedselector appended tostyles.css→shell-fixed-positionviolation.dialog-shell.ts→shell-capture-escapeviolation.dashboard-chart-interaction.ts'sbeginSelectionexclusion(proving the non-panel exclusion is non-transitive) → violation.
HANDLE_PXchanged to 8 while CSS stayed at 7px → the drift contract test failed.Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated