Uh oh!
There was an error while loading. Please reload this page.
feat(desktop): add the Astryx migration contract harness - #1653
Merged
Conversation
The real-window smoke launches the app with MAKA_E2E_FIXTURE, which makes main.ts compute startHidden and main-window.ts keep the window hidden for its whole lifecycle. The gate therefore had nothing to look at: on a clean main its own programmatic-window-visible check reports visible=false and exits 1, and every human check it prompts for — dragging window edges and corners to resize, dragging the titlebar to move, Tab/Shift+Tab traversal inside the modal — is unrunnable against an invisible window. Set MAKA_E2E_SHOW_WINDOW on the launch env, which is exactly the escape hatch main.ts already provides for runs where there is no focus to steal. Capture runs and CI e2e do not set it and keep their hidden-window behavior unchanged.
Hiding the dock icon makes the run an accessory app. Contrary to what the migration tracker assumed, such a window is fully clickable — measured on darwin: with MAKA_E2E_SHOW_WINDOW set, the fixture window reports isVisible/isFocused true and takes clicks, drags, and keyboard input normally. What it does not have is a Dock tile or a Cmd+Tab entry, so a reviewer doing manual visual comparison who switches to another app has no way back to it. Key the dock icon off the same startHidden that decides whether the window shows at all, instead of re-deriving the condition from MAKA_E2E_FIXTURE. The two can no longer drift, and MAKA_E2E_SHOW_WINDOW now opts out of both halves of staying out of sight. Capture runs and CI e2e leave the variable unset, so they keep hiding the window and the dock exactly as before. Lock it with a dock diagnostic and a programmatic-dock-visible check in the real-window smoke, which reported dockVisible=false before the change and true after.
A migration that must stay visually neutral needs to look at the running app constantly, but the only way to boot a route with fixture data was the smoke gate, which insists on walking a reviewer through twelve checklist prompts and filing a report every time. Add --manual: same launch path, then stop. The window stays up until it is closed or interrupted. Reusing the gate's launcher keeps the window a reviewer inspects identical to the window the gate checks, and leaves one place that knows how to boot a fixture instance. Expose it as `launch:fixture`, and lift the build chain the smoke scripts repeated verbatim into `build:with-deps` rather than copying it a third time.
#1565 PR 0. The mega-branch produced regressions on screens nobody edited, and slicing alone does not localise them: the signal has to come from a baseline that says what the app looked like before. This captures one. check:visual-contract walks the five representative fixture routes in light and dark, records every visible element's box, paint, and alignment properties, and diffs the result against committed JSON. The alignment set (align-items/justify-content/place-items/gap/grid-area) is there because flex and grid misalignment is exactly the class a paint-only property set cannot see. It gates no-change, not correctness: a pre-existing bug on main stays out of scope, and zero diff means the migration did not move this element, never that the element is right. The boot sequence audit-alignment.mjs already worked out — spawn Electron with MAKA_E2E_FIXTURE, wait for a CDP page target, evaluate in the renderer — moves to fixture-cdp.mjs so both scripts share one launcher instead of growing a second. It also gains what a contract harness needs and an audit could do without: animations and webfont reflow frozen before capture, an evaluate deadline so a wedged renderer fails loudly instead of hanging, and process-group teardown so Electron's helpers do not pile up and starve later windows. Also carries #1565's third PR 0 item, salvaging the mega-branch's late regression fixes. Six of the eight land on properties this snapshot already records — a wrapped titlebar row, a timestamp stacking above its title, a missing gap, a column painting over the settings rail all move a rect or a recorded property, so the diff catches them without bespoke rules. What bespoke rules do add is noticing when the harness stops watching, so each one is pinned to an anchor that must keep appearing in the baseline. The remaining two fix files the mega-branch itself created, which do not exist on main; they are recorded in the PR body instead. Determinism was verified by capturing the same build twice. The first attempt was not clean, and both causes are excluded rather than tuned around: element labels fell back to el.id, which Base UI regenerates every render, and OverlayScrollbars' chrome fades with pointer activity, so its visibility was a coin flip. Baselines stay at 1.5MB rather than 4MB because properties equal to their initial value, or inherited unchanged from the parent, are omitted and read back as unchanged. Migration-only: deliberately outside CI, following check:chat-visual, and removed in PR 14 with the maka.legacy layer.
#1565 PR 0. The mega-branch's most expensive regressions were controls that render perfectly and refuse clicks. A computed-style snapshot cannot see that, so it gets a contract of its own: every visible interactive element on the five routes must be reachable at its centre, must not sit under a pointer-events or visibility trap, must not be transparent through an ancestor, and must not fall inside an -webkit-app-region: drag area. Three parts of #1565's proposal did not survive contact with a clean main, and each is replaced by something answering the same question: The drag-region probe was specified as a real click plus a side effect. Fixture windows are hidden, and an unmapped window does not route synthesized input through the browser's hit-testing path, so every target read as swallowed. Making the window visible to fix that turned the probe into something that fires real product actions — one opened a native file dialog and hung the run. Reading -webkit-app-region off the stylesheet rules instead has no side effects: the property is missing from getComputedStyle, but not from the rules that declare it. All five probe points were specified as required. On a clean main that is unreachable: sibling chrome legitimately covers a couple of edge pixels, so a workbar resize handle would indict a control nobody struggles to click. The centre is now required and corners corroborate — three lost corners means something covers the control. The overlay guard keyed on [role=dialog][open], which a React-rendered div[role=dialog] never has, so the settings surface went undetected and every control behind it was reported unhittable. It now keys on the product's own data-modal="true". Geometry probes do need a visible window — elementFromPoint disagrees with getBoundingClientRect against a hidden one — so this check steals focus for a few seconds per route. Memoising style and rect lookups took the densest route from a 60s timeout to 3s, and routes are spaced so one window is fully gone before the next boots. All five routes are clean on ae43cb2.
Astro-Han
marked this pull request as draft
July 30, 2026 09:45
Review found the harness could report `ok` for a window it never launched, could not see the class of change the migration actually makes, and shipped its only product change with no automated coverage. Launch through the shared E2E seam. The harness rolled its own Electron launcher — fixed debug ports, a /json/list poll, a raw WebSocket, a process-group kill, `...process.env` — all of it already solved next door in `apps/desktop/e2e/fixtures.ts`, and each hand-rolled version worse. It never verified the CDP target belonged to the child it spawned, and the port allocator restarted at 14600 in every process, so a leftover Electron on that port got captured instead. Reproduced: two concurrent runs requesting different scenarios both read the same window. When the leftover is the same route from an older build — the shape you get by re-running one route after an edit — the contract reports `ok` for a stale window. `detached: true` with no signal handler supplied the leftovers; this machine had 270 stale user-data dirs, none ever removed. `buildE2eEnv` moves to `scripts/fixture-env.mjs`, shared by both callers; its own comment already described the bug the harness had, since inheriting the environment leaks `VITE_DEV_SERVER_URL` and loads the dev server instead of the build under test. Launch, readiness and teardown now go through Playwright's Electron support: fixed sleeps become per-route readiness selectors, and teardown removes its temp dir. 235 lines become 139, and the hit-test retry goes with them, per the sibling harness's rule that flakes should fail loudly. Stop committing baselines. They encode the capturing host — font metrics and the macOS traffic-light inset — so one machine's baseline reads as thousands of diffs on another. Capture on the branch you compare against, apply the slice, compare: the two captures that matter always come from the same host. The timezone is pinned through the fixture's IANA override for the same reason. This removes 70k lines from review and makes the orphan-baseline check moot. Record what a cascade flip actually changes. `boxShadow`, `textAlign`, `flexDirection`, `flexWrap` and `outline` were missing, so a utility that starts beating a product rule could change elevation or text placement with an identical rect and be reported clean. Three `INITIAL_VALUES` entries guessed at values Chromium does not serialise and never matched once, leaving constants on all 3,964 records; `findDeadOmissionRules` now fails the run when a rule stops matching — it caught a fourth such entry immediately. Inherited values were compared against the parent rather than the nearest recorded ancestor, so a zero-box wrapper's color could change, repaint every visible child, and leave the capture byte-identical: 129 masked values on the chat route alone. Delete the second drag-region resolver. It parsed stylesheet rules in source order, ignoring specificity, `!important` and `@layer` — which PR 1 and PR 2 introduce — on the premise that the property is absent from computed style. It is not: `getComputedStyle(el).webkitAppRegion` returns `drag`, and `e2e/window-titlebar.spec.ts` has read it that way in CI all along, against rendered geometry and document order. The hit-test keeps what that spec does not cover, and now reports how many elements it actually probed rather than how many the selector matched. Pin the product change. `resolveDockPresentation` is a pure function with tests; the branch it replaced could only be exercised by launching Electron, which is why it had none. The programmatic smoke checks are now pinned by the contract test that exists to pin checks — including the new dock one, which could previously be deleted with CI green — and `e2e` shares `build:with-deps` instead of repeating the chain verbatim.
…budget buildFixtureEnv read process.env.CI inline, so the fixture-env test asserting a hidden run stays hidden passed on every laptop and failed on the Linux CI runner. The ambient read now lives in isCiLinuxDisplay for callers to compose explicitly. audit-alignment gets its 2500ms settle back (the shared-launcher refactor had silently dropped it to 1000ms against static-markup readiness) plus a real ready selector per fixture, and its header now states the deliberate environment change from the raw-spawn launcher it replaced.
withFixtureWindow tore down with a bare app.close(), which has no deadline — one wedged launch turned a capture run (or the CI alignment audit, which has no job timeout) into an infinite hang, and the temp userData dir leaked because rm was sequenced after the close. The bounded close the E2E suite already owned moves to scripts/electron-lifecycle.mjs — the same shared-home precedent as fixture-env.mjs, so a bare-node harness and the Playwright suite stop splitting one launch concern across two directories. Renderer evaluate gets a deadline too; teardown then reclaims the process either way.
Two measurement-semantics gaps in the visual capture, both instances of a class the harness had already been bitten by: - textAlign was in PROPERTIES but not INHERITED_PROPERTIES, so 79-85% of records re-recorded an ancestor's value and one container change printed as N descendant lines. It moves to the inherited omission (and out of INITIAL_VALUES — absent must mean exactly one thing). A static table test now pins every CSS-inherited property to that route, which is the guard findDeadOmissionRules could never provide. - Descendants of an opacity:0 element report opacity 1 and a non-zero rect, so the collapsed session panel put 105 invisible records into the chat capture; a change under it would diff on pixels that never paint. The walk now prunes zero-opacity subtrees. The prune exposed that two salvaged anchors were matching those phantom records: the chat fixture never shows the session rows at all. They now point at mcp-hub, whose fixture opens the sidebar for real.
Replaces the two-step gitignored-baseline workflow. The checker now builds the base ref in a cached temp worktree (node_modules shared; the compare refuses to run across a package-lock change), builds the working tree, captures every route x theme x platform from both builds, and diffs in memory. Every step of the old workflow a human could skip - rebuild after switching, recapture after rebasing, stash on a committed slice - produced a zero-diff pass that had measured the same binary twice; now the instrument owns the whole measurement and there is no baseline file to trust or to stale. The matrix gains the win32 column #1565 asked for: the fixture platform override drives the production app:info -> data-os path, so the per-OS cascade is captured on any host - no Windows runner involved.
desktop-real-window-smoke was the last launcher still spreading process.env wholesale: a developer with npm run dev open smoked the dev server instead of the build the script just made, and the run touched the real $HOME (#1517's skills-deletion surface). It now launches through buildFixtureEnv with an explicit visible window. The dock contract drops its duplicated fourth case and pins the wiring instead - the regression this module exists for was the call site re-deriving startHidden, which no pure-function test can observe. The reveal contract follows fixture-env's pure shape (the ambient CI read now lives in isCiLinuxDisplay, composed at each call site).
…entity Round-3 review found the compare's base was not a base: the borrowed node_modules resolved @maka/* through workspace links straight back to the working tree, so the base bundled and loaded the candidate's own packages — a slice changing @maka/core would have compared itself against itself. The base now runs its own npm ci against its own lockfile in the cached worktree (atomic lock against concurrent builds; the marker name encodes the scheme so old symlink-era caches rebuild). The lockfile-parity gate and the symlink list existed only to justify the sharing; both are deleted, and a slice may now legitimately change dependencies. The same round showed the instrument asserting nothing about what it measured, so now it proves it: route/theme/platform are closed sets; every capture waits for the requested data-os and theme on the live document before reading a style; painted ::before/::after get paint signatures (47 pseudo rules paint in this renderer, led by the body::after film grain, and a cascade flip on any of them was invisible); mixBlendMode joins the property set; and dead omission rules are counted at the raw sample where the border/outline gates cannot shadow them.
The smoke gate still had two unbounded exits: --manual waited forever on 'exit' after one SIGTERM, and the report path fired SIGTERM and called process.exit without waiting or escalating — plus a failed report write skipped the kill entirely. All paths now stop through the shared bounded close (SIGTERM as the graceful phase, 5s grace, SIGKILL the tree), inside a finally. electron-lifecycle's import of @maka/runtime now fails with an error that says what to build; on a fresh clone the raw ERR_MODULE_NOT_FOUND surfaced from four files deep in the import chain.
The shared teardown's force path kills by process group. The smoke gate spawned Electron without detached, so the group signal hit ESRCH, the terminator read that as "tree already gone", and a visible window that had ignored SIGTERM survived its own SIGKILL stage. Spawn the smoke child as a group leader like every runtime consumer does, and back the group kill with a direct kill on the root so a future non-leader child still dies inside the 2s exit deadline.
filter, backdrop-filter, clip-path and transform move pixels without moving the rect — grayscale() on disabled provider rows, the glass theme's backdrop blur, clip-path as visually-hidden, rotate() on square chevrons — so losing one was invisible to every property the contract sampled. Capture all four (plus transform-origin, gated on a transform actually painting), and stop the renderer's clock first: a running spinner serialises a different matrix at every phase, which would diff time rather than the cascade. Verified with a negative control (an injected filter rule fails the compare) and a clean 20/20 run. Also correct the header, which still described the rejected node_modules-sharing base build, and compare the direct-run guard through pathToFileURL so a percent-encoding checkout path cannot turn the instrument into a silent exit-0 no-op.
--route accepted any value and only failed when nothing matched, so `--route chat --route chatt` dropped the typo and exited 0 as "1 route(s) clean" — silently shrinking the requested coverage. Validate each value against the closed route set (as the visual contract already does), guard the entry point through pathToFileURL, and pin the CLI's exit codes with subprocess tests.
The ESRCH-swallowing group-kill path was only proven against a live window; the fake now exercises it too — a terminator that returns without killing must be followed by a direct SIGKILL on the root.
Astro-Han
marked this pull request as ready for review
July 30, 2026 13:04
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refs #1565. PR 0 of the Astryx renderer migration: the measurement instrument every later slice depends on. The mega-branch produced regressions on screens nobody edited, and slicing alone cannot localise them — the bisect signal has to come from a record of what the app looked like before.
check:visual-contract— one command compares two builds: it checks out the base ref into a cached temp worktree with its ownnpm ci, builds both sides, captures five representative fixture routes × light/dark × darwin/win32 from each, and diffs every visible element's box, paint, and alignment properties — plus a paint signature for every painted::before/::after— in memory. Gates no-change, not correctness: a pre-existing bug on the base stays out of scope, and zero diff means "the migration did not move this element", never "this element is right".check:hit-test— interactive elements must be reachable at their centre, free of pointer-events/visibility traps, and opaque through their ancestry; every route reports what it probed and what it skipped (invisible, out-of-scope, disabled, transparent, clipped) so the coverage is a number, not an assumption.launch:fixture— a real, clickable fixture window for manual visual comparison, which a visually-neutral migration cannot do without.Both harnesses live in
scripts/as npm scripts and are deliberately outside CI, following thecheck:chat-visualprecedent and #1565's own scoping — a two-build compare costs minutes per run, which is a per-slice gate, not a per-push one. They are migration-only and get deleted in PR 14 with themaka.legacylayer.scripts/fixture-env.mjs,scripts/fixture-window.mjs, andscripts/electron-lifecycle.mjsare the exception:audit-alignment.mjsruns in CI and the E2E suite shares them, so they outlive the harness.How to use it
There is no baseline file and no capture step to remember. An earlier version had one, captured by hand on the base branch and compared by hand after switching; every step a human could skip — rebuild after switching, recapture after rebasing — produced a convincing zero-diff pass that had measured the same binary twice. Now the instrument owns the whole measurement: both sides are checked out, installed, built, and captured in the same run, on the same host (font metrics and the macOS traffic-light inset are host facts, so cross-host comparison was never sound anyway).
The base gets its own dependency closure: a fresh worktree of the ref with its own
npm ciagainst its own lockfile, cached keyed by the resolved commit, so iterating on a slice rebuilds only the working tree. Sharing this checkout'snode_moduleswas tried and rejected — the workspace links inside it resolve@maka/*back to the working tree's packages, so the base would have bundled and loaded the candidate's own code. A side effect of the honest install: a slice may legitimately change dependencies. Route/theme/platform arguments are closed sets, and every capture asserts the renderer actually reached the requesteddata-osand theme before reading a single style — a capture must prove it measured the cascade it claims.The win32 column runs on any host:
MAKA_E2E_FIXTURE_PLATFORMdrives the productionapp:info → data-ospath, which is what keys the per-OS CSS. It covers the cascade, not native chrome.Route mapping
#1565 names product routes; these are the
MAKA_E2E_FIXTUREscenarios that actually open them onmain. There is no scenario literally called "chat" or "settings-providers".turn-narrative.maka-session-workbarsettings-general.settingsRowsprovider-workspace.providersPanelmodule-mcp.maka-module-main-headerfirst-run.maka-onboarding-surfaceKnown blind spots, stated in the tool's header rather than papered over:
display: nonesubtrees (closed menus, unmounted dialogs) never enter the capture; pseudo-elements are paint signatures without a rect; native top-layer content (dialog.showModal, popover) would escape the opacity-prune, and nothing in the app uses it today.Where this departs from #1565, and why
Each departure is a measurement, not a preference. The issue body has been corrected where it was factually wrong.
The fixture window was never unclickable.#1565 stated that
app.dock.hide()makes fixture runs reject all real clicks. Measured onae43cb291/darwin, that does not hold:MAKA_E2E_FIXTUREalone means the window is never shown at all (startHidden→keepHiddenForE2eFixture,main-window.ts:114), and addingMAKA_E2E_SHOW_WINDOW=1givesisVisible: true,isFocused: true, and fully working clicks — withdock.hide()still running.scripts/desktop-real-window-smoke.mjswas already broken onmain. It boots a real fixture window but never setMAKA_E2E_SHOW_WINDOW, so its ownprogrammatic-window-visiblecheck fails and its twelve human checks are unrunnable against an invisible window. Fixed here, extended with--manualrather than adding a second launcher, and now launched through the shared environment builder — it was the last launcher still inheritingprocess.envwholesale, which meant a developer withnpm run devopen smoked the dev server instead of the build the script had just made.The one product-code change is about reachability. The dock rule now keys off the same
startHiddenthat decides whether the window shows, instead of re-deriving it fromMAKA_E2E_FIXTURE— a re-derivation that had already drifted, since it missed theMAKA_E2E_SHOW_WINDOWescape hatch. An accessory app has no Dock tile and no Cmd+Tab entry, so a reviewer who switches away mid-comparison cannot switch back. Capture runs and CI e2e leave the variable unset and keep their accessory, no-focus-steal behaviour unchanged.The window-drag question is not asked here.#1565 proposed probing it with a real click; that was tried and abandoned (an unmapped window does not route synthesized input, and a visible one fired real product actions — one opened a native file dialog and hung the run). It is also already answered:
e2e/window-titlebar.spec.tsmeasures it in CI against rendered geometry and the document order Chromium composes drag rects in, and.maka-window-titlebaris the only element allowed to declaredrag. The hit-test covers what that spec does not — whether a control anywhere in the window can be reached at all.Centre is required; corners corroborate. All five probe points cannot pass on a clean
main— sibling chrome legitimately covers a couple of edge pixels. Three lost corners means something is actually covering the control; one means the border has a neighbour.Item 3: salvaged regressions
Six of the eight late "fix cascade / fix click" commits land on properties the snapshot already records. Bespoke rules would be redundant; what they do add is noticing when the harness stops watching, so each is pinned to an anchor that must keep appearing in a route where the element actually paints. That last clause is load-bearing: the session-row anchors originally pointed at the chat route, where the fixture keeps the panel collapsed under
opacity: 0— they "matched" records with no visible pixels until the capture started pruning invisible subtrees, at which point the check failed and exposed its own false assurance. They now point atmcp-hub, whose fixture opens the sidebar.be5e69584maka-shell-topbar-railfd38a37cemaka-composer-inner(resting state only)9aad59740maka-list-row-meta3dbd68ca7maka-list-row9d20a9396maka-session-workbar-countbe1406705settingsSidebarTwo are not transcribable:
3e57d1951(stat tile leading) and400e8f4a9(turn marker measure) fix files the mega-branch itself created, which do not exist onmain. They are recorded here as fragile spots for whichever slice introduces those components.What review changed
Four review rounds (Codex plus an independent fresh-context agent each time), every finding reproduced before being fixed. The branch is meaningfully different as a result.
Round 1
It could report
okfor a window it never launched. The harness rolled its own launcher — fixed debug ports, a/json/listpoll, a raw WebSocket, a process-group kill,...process.env. It never verified the CDP target belonged to the child it spawned, so a leftover Electron on the port was captured instead; reproduced with two concurrent runs both reading the same window, and 270 stale user-data dirs on the dev machine. Now launched through Playwright's Electron support, with the environment builder extracted toscripts/fixture-env.mjsand shared with the E2E suite.It could not see the class of change the migration makes.
boxShadow,textAlign,flexDirection,flexWrapandoutlinewere not captured — a utility that starts beating a product rule after the@layermove can change elevation or text placement with an identical rect. ThreeINITIAL_VALUESentries guessed at values Chromium does not serialise and never matched once.Inherited values were compared against the wrong ancestor. A zero-box or
display: contentswrapper is skipped, so nothing in the capture carried its values — yet visible children omitted properties that matched it. Now compared against the nearest recorded ancestor, so every omission is recoverable by a reader of the file.The drag-region resolver was built on a false premise. It parsed stylesheet rules in source order because "the property is absent from
getComputedStyle". It is not:getComputedStyle(el).webkitAppRegionreturnsdrag, andwindow-titlebar.spec.tshas read it that way in CI all along. Deleted, ~50 lines.The product change had no automated coverage.
resolveDockPresentationis now a pure function with tests, and the programmatic smoke layer is pinned by the source contract.Round 2
The two-step baseline workflow could pass silently when a step was skipped. The checker never rebuilt the candidate, the baseline was a bare array with no provenance, and the gitignored file could not travel between worktrees — comparing a build to itself read as a clean slice. Replaced by the one-command
--againstcompare; the baseline file, the--updateflag, and the.gitignoreentry are gone with it.Teardown was unbounded, and CI inherits this launcher.
app.close()has no deadline; one wedged launch turned a capture run — or the CI alignment audit, whose job has no timeout — into an infinite hang, reproduced once at ≥600s. The bounded close the E2E suite already owned (grace, then SIGKILL the tree) moved toscripts/electron-lifecycle.mjsand both consumers share it; rendererevaluategained a deadline; the temp profile is removed even when close fails.The shared-launcher refactor had silently changed the CI gate's conditions.
audit-alignment's settle budget had dropped 2500ms → 1000ms against static-markup readiness, andbuildFixtureEnvreadprocess.env.CIinline — which made the "hidden run stays hidden" test pass on every laptop and fail on the Linux CI runner. The settle budget is restored with real per-fixture ready selectors, the window-visibility decision is a pure function of the builder's arguments, and the one ambient read lives inisCiLinuxDisplayfor callers to compose.Two capture-semantics gaps.
textAlignwas inPROPERTIESbut notINHERITED_PROPERTIES, so 79–85% of records re-recorded an ancestor's value; and descendants of anopacity: 0element (own opacity1, non-zero rect) were captured as visible — 105 phantom records under the collapsed session panel. Fixed, plus a static table test pinning every CSS-inherited property to the inherited route in both directions.The darwin-only matrix contradicted the issue, and its stated justification was wrong. No Windows runner is needed: the fixture platform override flips
data-osthrough the production path on any host. The matrix now includes win32.Round 3
The base was not isolated. The base worktree borrowed this checkout's
node_modules, and the workspace links inside it resolved@maka/*straight back to the working tree — the base bundled and loaded the candidate's own packages, so a slice changing@maka/corewould have compared itself against itself. Both reviewers converged on this independently; reproduced byte-for-byte withrequire.resolve. The base now runs its ownnpm ciagainst its own lockfile (cached per commit, guarded by an atomic build lock), and the lockfile-parity gate plus the symlink list that existed to justify the sharing are deleted.Pseudo-elements were invisible to the contract. 47
::before/::afterrules paint in this renderer — including the full-screenbody::afterfilm-grain overlay — and a cascade flip on any of them changed pixels while every element record stayed byte-identical. Painted pseudo-elements are now captured as paint signatures that inherit from their host.The instrument now proves its own measurement identity. CLI arguments are closed sets; every capture waits for and asserts the requested
data-osand theme on the live document before reading styles — the win32 column can no longer silently degrade into a second darwin column if the override seam regresses. Dead omission rules are counted at the raw sample inside the capture (not inferred from the compressed output), so a rule shadowed by the border/outline paint gates still proves itself.Remaining teardown edges. The smoke gate's SIGTERM paths now escalate through the shared bounded close, and a failed report write no longer leaves a live Electron behind. Verifying that fix surfaced one more: the shared force path kills by process group, the smoke gate's child was not spawned
detached, so the group signal hit ESRCH, the terminator read "tree already gone", and a visible window that ignores SIGTERM survived its own SIGKILL stage. The smoke child is now a group leader like every runtime consumer, and the bounded close backs the group kill with a direct kill on the root.Round 4
The contract could not see the paint-only channels.
filter,backdrop-filter,clip-pathandtransformmove pixels without moving the rect —grayscale()on disabled provider rows, the glass theme's backdrop blur,clip-pathas visually-hidden,rotate()on square chevrons — so a migration losing one diffed zero. All four are now sampled (plustransform-origin, gated on a transform actually painting), and the capture freezes animations and transitions before reading a single style: a running spinner serialises a different matrix at every phase, which would diff time rather than the cascade. Verified with a negative control — an injectedfilterrule fails the compare withfilter: ∅ → sepia(0.1)— and a clean 20/20 run.The hit-test's
--routewas an open set. It accepted any value and failed only when nothing matched, so--route chat --route chattdropped the typo and exited 0 as "1 route(s) clean" — silent coverage shrink on a tool whose whole point is that coverage is a number. Each value is now validated against the closed route set (as the visual contract already does), and the CLI's exit codes are pinned by subprocess tests.Two smaller honesty fixes. Both direct-run guards compare through
pathToFileURL— a checkout path with a space or non-ASCII segment percent-encodes inimport.meta.url, and the string-concatenation compare would have made the instrument exit 0 having measured nothing. The tool header still described the rejectednode_modules-sharing base build; it now states the actual isolation model. The direct-kill backstop from round 3 gained a fake-based test alongside its live-window proof.Verification
Run on
main(ae43cb291) + this branch, macOS arm64.