feat(components): close the geometry flywheel — executable ledger, fixed status, source-pointed repairs - #353
Draft
wibus-wee wants to merge 4 commits into
Conversation
A ledger baseline was a number the report PRINTED and nothing executed: `pnpm test:geometry` only checked that the two promoted contracts still compiled and held, so an `accepted-debt` finding could drift arbitrarily far from its reviewed offset, and a brand new finding could appear, with every check green. The ledger's 74 debt entries constrained nothing. Extract the capture walk the report already performs into `tests/e2e/support/geometry-capture-plan.ts`: `GEOMETRY_CAPTURE_PLAN` is now the ONE list of captures, `showGeometryCaptureStory` the one way to open and settle a story, and `buildGeometryCapture` the one way to turn discovery into a capture record. The report walks it to shoot its screenshots; the gate walks it to measure and nothing else. Neither can name a story, a viewport, a device scale or an aggregate scope the other does not. Add the ratchet to the gate: rerun `capture -> observation -> findings` over that plan with no screenshots, then fail when - a finding's |offset| passes |baseline| plus one device pixel (1/DPR of the coarsest capture it merged), or - a finding has no ledger entry at all, pointing at `geometry:triage` the way a lockfile check points at an install. `ignored` opts out and `promoted` is left to the contract check that already gates it exactly. `checkGeometryLedgerRatchet` and `geometryFindingDevicePixel` are pure and unit-tested; the failure message carries key, label, status, baseline and current offset. A finding offset is a mean over the whole plan, so a baseline belongs to the platform that recorded it. These were recorded on macOS and the gate runs on Linux, so 12 of the 27 findings this plan reproduces here are re-baselined to the Linux rendering in this commit; the 49 entries that no longer reproduce keep their reviews untouched. Verified: `tsgo --noEmit`, unit tests (106 passed), `pnpm test:geometry` (10 passed, 7.2m; the ratchet adds ~4.6m over the previous 2.6m gate). Model: claude-opus-5[1m]
… status `accepted-debt` carried two different decisions under one word — "this is wrong and we will fix it later" and "this is not a defect" — so any queue built from it mixed work with non-work, and 74 of the ledger's 76 entries sat in it. And there was no way to record the other end of the flywheel at all: a finding whose offset had been driven back to its line stayed `accepted-debt` at its old, loose baseline, so the fix it just received was not protected by anything. - `GeometryLedgerStatus` becomes `new | debt | wont-fix | fixed | ignored | promoted`. All 74 existing entries migrate to `debt`; the distinction between debt and wont-fix is a review, and guessing it here would record decisions nobody made. `triage` records `debt` for the same reason. - `fixed` is baselined at what was measured, which makes it the strictest entry in the file: the ratchet then treats a reopened fix as the regression it is. - Only `promoted` still compiles into a contract; `debt`, `wont-fix`, `fixed` and `ignored` compile into nothing, now stated in code and in `tests/e2e/AGENTS.md`. - Add `pnpm geometry:verify-fix <dir> <findingKey...>`. It reruns the ratchet gate itself — same capture plan, no screenshots — so "did this fix land" and "did it cost something elsewhere" are one measurement. A finding measured back inside one device pixel, or gone from findings entirely, becomes `fixed` at its new baseline; anything else prints why and leaves the ledger alone. The decision is made in TypeScript (`verifyGeometryFixes`) beside the ratchet, and the script only applies the file the gate writes, exactly as triage does. - A `promoted` finding is refused: it is gated EXACTLY by its contract, and moving it to `fixed` would stop compiling that contract and quietly drop the tightest rule in the file. Retiring a contract stays a deliberate step. The report's status chips and default filter follow: the work queue is new + changed + css-defect + promoted, minus `wont-fix` (a decision) and `fixed` (already done). Verified: `tsgo --noEmit`, unit tests (111 passed), lint, format. Model: claude-opus-5[1m]
`GeometryRepairTerm.element` was a rendered DOM description — `div[role=button][data-slot=…]` — which a designer recognises and an agent cannot open. Nothing in a finding said which component wrote that node, so the repair text told a reader the size of the problem and nothing about where to make the edit. Collect two more fields for every box-model node during observation: the `class` attribute verbatim, and the nearest function component above it in the React fiber tree. `geometry-react-fiber.ts` walks `fiber.return`, unwrapping the `memo` and `forwardRef` objects React puts in `type`'s place and preferring an explicit `displayName`; it is closure-free like every other function capture serializes into the page, and it introduces no dependency. React 19 dropped `_debugSource`, so a component NAME is the whole pointer — there is no file or line to read, and guessing one would be worse than none. Both fields are optional and both are LABELS: they ride the repair term and the proposal, never a finding key, for the same reason an accessible name may not — renaming a component is not a new finding. A node React never rendered simply contributes none and nothing fails. A report run resolves a component for 3389 of 3429 captured nodes and a class list for 3247. The report's repair text becomes one executable sentence per term, built by the pure `formatGeometryRepairProposal`: SidebarRowShared 里 div[role=button] 的 padding-inline-end 多 2px(class: pe-3) `geometryRepairCssProperty` names the property that actually exists on each edge: `padding-inline-end`, `border-inline-start-width`, and `column-gap`/`row-gap` for a gap, which belongs to an axis rather than to one of its two edges. A real class list runs to hundreds of characters, so only the card body carries it; the collapsed one-line summary omits it rather than truncate it, because half a class list greps for nothing. Verified: `tsgo --noEmit`, unit tests (120 passed), lint, format. Model: claude-opus-5[1m]
The report deduplicated by LABEL: `groupLabel.split(' · ')[0]` plus a
member-offset signature, which collapses two instances of one named
marker group and nothing else. Ten discovered rows that share one wrong
padding still produced ten cards, so the queue's length measured how
many elements a defect touched rather than how many defects there were.
Give a `css-defect` finding a `repairGroup` in the FINDINGS stage — the
component that owns the edit (or, where React rendered no component, the
common ancestor's DOM description), the declared term, the edge, and the
node the term sits on. It is computed from the repair proposal, so a
second row of the same component reporting the same edit lands in the
same group whatever its label, its measured value or which of the two
compared sides dominated.
Findings do not merge. Keys, baselines, evidence and the ratchet are
untouched: `repairGroup` is a grouping LABEL, and it never reaches a
key, exactly like the component name it is built from.
The report folds cards on it — one card per repair, naming the findings
it stands for — and folds strictly inside one ledger status, because a
`new` finding hidden under a reviewed one would be a review that never
happened. The `groupLabel` signature stays where it is: it is what
decides that N instances with identical member offsets are one
measurement-model divergence, which is a verdict rather than a dedup,
and its single card is that verdict's card.
`geometry:verify-fix` takes `--repair-group=<id>` so one edit can be
verified and closed for every finding it covers in one gate run.
Verified: `tsgo --noEmit`, unit tests (125 passed), lint, format.
Model: claude-opus-5[1m]
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 free
to 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.
Related issue
Same-repository branch; no intake Issue (
.github/AGENTS.md: same-repobranches are not subject to the external contribution template).
Post (A Baseline Nobody Runs Is Just a Comment)
Every mature UI codebase has a drawer. Inside it are the two-pixel problems: an icon sitting slightly high against its label, a tab whose left edge doesn't quite line up with the one above it, a padding that's 12 where its neighbours are 10. Nobody ships a bug for these. Nobody refuses a release over them. They accumulate, and after a couple of years the product has a faint blurriness that everyone can feel and nobody can name.
We built a system that finds them automatically. It renders the app across a matrix of stories, viewports and device scales; measures every rendered box, glyph and SVG path; clusters the coordinates into alignment rails; and reports every element sitting off one. Each finding gets an identity that is structural and coordinate-free — surface, section, row family, role, index, anchor — so that the same misalignment is recognisably the same finding tomorrow, in a different locale, on a renamed fixture. A human triages each one exactly once, and that verdict lands in a ledger checked into the repo.
That was the easy half. This post is about the other half: what happened when we tried to make the ledger do something.
The ledger that asserted nothing
The ledger had 76 findings in it. Seventy-four were marked as accepted debt, each with the offset a human had measured and agreed to live with. Two had been promoted into hard contracts. And CI checked... the two contracts.
Not the seventy-four. There was no assertion anywhere that could fail because a finding drifted past the number a reviewer had signed off on. Worse, there was no assertion that could fail because a new finding appeared — a fresh misalignment could land and the build stayed green. The system remembered yesterday perfectly and had no way at all to refuse tomorrow.
This shape is extremely common and worth naming, because it isn't really about pixels. Lint baseline files,
# type: ignoreinventories, snapshot allowlists, "known failures" lists — all of them are review state, and review state is worth exactly what it costs to violate. If violating it costs nothing, you haven't recorded a decision; you've written a comment with unusually good formatting.So the ledger got two assertions. The first is a ratchet: rerun the measurement pipeline and fail if any finding's
|offset|exceeds its reviewed|baseline|. The second is a completeness check: fail if any measured finding has no ledger entry at all, and name the triage command in the failure message, the way a lockfile check namesinstall. Together they say: this can get better, it can stay the same, and it cannot get worse or grow in the dark.The one subtlety worth stealing is the tolerance. Comparing floating-point geometry needs slack, and the honest amount is one device pixel — but a finding is often merged across captures at different device scales, and a measurement taken at 1× is simply less precise than one taken at 2×. So the tolerance is
1/DPRof the coarsest capture that contributed evidence. A finding seen at both 1× and 2× is only as precise as the 1× observation, and pretending otherwise would make the gate flaky in a way that's almost impossible to debug six months later.An un-executed number has no environment
Here is what happened the first time that ratchet actually ran.
It went red — not because anything had regressed, but because a batch of those carefully reviewed baselines had been recorded on macOS, and CI runs Linux. Of the 76 reviewed findings, only 27 reproduced on the new host at all; the rest were text-baseline and text-versus-icon rows that simply don't exist under different font rendering. Of the 27 that did reproduce, twelve were outside their baselines on arrival. One row's visual-center went from
1.75to−4. Another went from5.27to12.5. One pair had its inline-start and inline-end values cleanly swapped.None of that was new breakage. All of it had been sitting in the file the entire time, invisible, because a number nobody executes has no environment attached to it. The moment you make review state executable it stops being a description of the product and becomes a description of the product plus the machine that measured it — and you find out immediately whether you ever knew which machine that was.
The fix is unglamorous: re-baseline onto the platform the gate actually runs on, and write down every single value you moved, so that "we re-recorded twelve baselines" can never quietly become "we relaxed the gate until it passed." That distinction is the entire difference between a ratchet and a rubber stamp, and it lives in a changelog, not in the code.
Debt needs an exit
The second problem was in the vocabulary.
accepted-debtwas two decisions wearing one word: "this is wrong, we'll fix it later" and "this is not actually a defect." Any queue built from that status mixes work with non-work, which is a reliable way to make sure nobody ever works the queue.But the bigger gap was at the other end. There was no status meaning done. A finding you'd driven back onto its line had nowhere to go — it stayed marked as debt, still carrying the loose baseline from before the fix. The system protected the debt and refused to protect the repair that had just discharged it. Fix a two-pixel offset, and nothing stopped it drifting back to two pixels the next week.
So: split the ambiguous status into two honest ones, and add
fixed, whose baseline is the offset it was just measured at — near zero, which makes it the strictest row in the whole file. A repair, once made, becomes the tightest constraint the system has, rather than the loosest.Two details made this work rather than merely exist.
The only way in is through the gate. The command that marks something fixed reruns the entire ratchet, not a targeted check on the one finding. "Did this repair land?" and "did it cost something somewhere else?" are the same measurement, taken once. A verification path that's cheaper than the gate is a verification path that disagrees with the gate.
Migration doesn't guess. All 74 existing entries moved to
debt, none towont-fix. The distinction is a human review; inventing it during a migration would have written down 74 decisions nobody made, in a file whose entire value is that its contents were decided by someone.There's also a trap here that generalises to any state machine layered over a set of guarantees. In the first draft, a promoted finding — one backed by a hard contract — was allowed to transition to
fixed. That sounds fine and is in fact terrible: promoted entries are what compile into contracts, so marking one as fixed would remove the tightest rule in the file, and the build would stay green while doing it. Any transition that can silently weaken your strongest guarantee needs to be blocked explicitly, because by construction nothing downstream will notice.A bug report a machine can act on
These reports are increasingly read by agents, not people, and that changes what "a good bug report" means.
A finding used to describe its element as
div[role=button][data-slot=…]. A human can paste that into devtools and go hunting. An agent handed that string and told to fix the padding has been given a sentence with no verb — it names a rendered artefact, not a place in the source.React 19 removed
_debugSource, so there's no file and no line number to recover. But there is still a fiber tree, and in a dev build it's readable from the DOM: every rendered node carries a__reactFiber$…key. Walkfiber.returnupward, skip host nodes, unwrap the wrapper objects React puts intype's place —memostores the inner component ontype.type,forwardRefontype.render— prefer an explicitdisplayName, and take the first ancestor whosetypeis a function. That name, plus the element's verbatimclassattribute, is the whole pointer. On our tree, 3389 of 3429 measured nodes come back with one.The report line goes from "trailing edge: padding is 12 here against 10 on the reference" to a component name, a CSS property that actually exists on that edge, a delta, and the class list to grep for. That's the difference between a symptom and a work order.
The principle underneath it matters more than the trick: evidence must never become identity. The component name and the class list are attached to findings as labels, and they are rigorously excluded from the finding's key — the same rule that already applied to accessible names. If a rename or a locale switch could change a key, then every refactor mints phantom "new" findings and silently orphans every review attached to the old ones. Anything you gather to make a report legible is, almost by definition, exactly the sort of thing that changes for reasons unrelated to the defect. Keep it out of the fingerprint.
The honest limitation: the first function component above a node is frequently a library primitive rather than one of ours. Still a better pointer than a bare DOM string, still not the file you wanted.
Count defects, not symptoms
The report deduplicated cards by label prefix. So one wrong padding applied to ten rows produced ten cards, and the length of the queue measured blast radius rather than defect count. A queue like that can't be prioritised, because its biggest item is whatever touched the most elements, not whatever is most wrong.
The fix is to give each finding a repair identity: the owning component, the box-model term, the edge, and the node the delta actually comes from. Findings sharing one repair identity are one job. The report folds them into a single card that names every finding it stands for, and the verification command can close an entire group in one run.
Two constraints kept this from being cosmetic. First, the repair identity is computed in the findings stage, not in the view layer — the report isn't the only consumer, and anything that decides meaning has to live upstream of everything that needs the meaning. Second, folding never crosses a review status: hiding an untriaged finding underneath a reviewed one is a review that never happened, and a grouping feature that can swallow un-reviewed work is worse than no grouping at all. Findings themselves never merge; keys, baselines and evidence are untouched. Only the card folds.
What it costs, and what you can't subsample
Capturing the plan — twenty stories across viewports, scales, themes and locales — takes 345.9 seconds. Turning those captures into observations and then into findings takes 1.0 second. The part of this system that reasons runs in a second; everything else is a browser starting up and laying out pages.
Which raises the obvious optimisation, and the obvious answer to it. Could the gate measure six stories instead of twenty and run four times faster? No — because a finding's offset is the mean over every capture that produced evidence for it, and that mean is precisely what the reviewed baselines record. A subset doesn't produce a cheaper version of the same numbers, it produces different numbers, against which the reviewed baselines mean nothing at all. You'd be ratcheting a quantity no human has ever looked at, which is strictly worse than not ratcheting: it has the appearance of rigour and none of the content.
If the runtime becomes a problem, the honest fix is to move the ratchet to its own CI job. It is never to shrink the input to a statistic and keep the statistic.
The point
The original slogan for this system was discovery can be clever; validation must be dumb — the measurement side is allowed heuristics, clustering and inference, but the gating side must be a mechanical comparison a human decided once and a machine repeats forever.
Working through these four gaps, I'd add a second half to it. A system that can only ever add rules isn't a type system, it's a checklist. What makes it a ratchet is having the full set of transitions: a way to record debt honestly, a way to distinguish debt from a non-defect, a way to mark a repair and then hold it to the tighter line, and a way to prove a rule has become redundant before deleting it.
The ledger currently reads 74
debt, 0fixed. For the first time, that first number is capable of going down — and the second one, once it moves, can't quietly move back.Details
Problem / pressure
The geometry constraint system had a flywheel that did not close. Four
specific holes:
geometry-ledger.jsonwas never executed. CI ran onlychat-workspace-geometry.spec.ts, which checked that the twopromotedentries still compiled into
geometry-contracts.jsonand still held. Noassertion could fail because a finding drifted past its reviewed
baseline.offset, or because a finding appeared that no entry reviews.74 of 76 entries constrained nothing at all; the pipeline itself
(
capture → observation → findings) only ran inside the 40-minute,80-screenshot report, which is deliberately not in CI.
accepted-debtwas two decisions under one word — "wrong, fix later"and "not a defect" — so any queue built from it mixed work with non-work.
There was no status for the other end either: a finding driven back to its
line stayed
accepted-debtat its old loose baseline, so the fix it hadjust received was protected by nothing.
GeometryRepairTerm.elementwas
div[role=button][data-slot=…]— no class, no component — so an agenthanded a finding could not locate the source.
groupLabel.split(' · ')[0]plus amember-offset signature. Ten rows sharing one wrong padding produced ten
cards, so the queue's length measured how many elements a defect touched
rather than how many defects there were.
Summary
Four commits, one per hole.
1 —
feat(components): gate the geometry ledger baseline in CIThe capture walk the report already performed is extracted to
tests/e2e/support/geometry-capture-plan.ts.GEOMETRY_CAPTURE_PLANis nowthe ONE list of the 20 captures,
showGeometryCaptureStorythe one way to openand settle a story,
buildGeometryCapturethe one way to turn discovery into acapture record, and
runGeometryFindingPipelinethe one place the three stagesrun. The report walks it to take screenshots; the gate walks it to measure and
nothing else. No screenshot code reaches the gate, and no capture list is
written twice.
The new gate test reruns that pipeline and fails when
|offset|passes|baseline|plus one device pixel, wherethe device pixel is
1/DPRof the coarsest capture the finding merged(a finding merged across a 1× and a 2× capture is only as precise as the 1×
one), or
pnpm geometry:triagethe way a lockfile check points at an install.ignoredopts out;promotedis left to the contract check that already gatesit exactly, rather than being gated twice and loosely.
checkGeometryLedgerRatchet,geometryFindingDevicePixelandformatGeometryRatchetViolationsare pure and unit-tested.2 —
feat(components): split accepted-debt and close the loop with a fixed statusGeometryLedgerStatusbecomesnew | debt | wont-fix | fixed | ignored | promoted. All 74 entries migrate todebt— the debt/wont-fix distinction isa review, and guessing it here would record decisions nobody made — and
triagerecordsdebtfor the same reason. Onlypromotedcompiles into acontract;
debt,wont-fix,fixedandignoredcompile into nothing, nowstated in code and in
tests/e2e/AGENTS.md.New
pnpm geometry:verify-fix <dir> <findingKey...>. It reruns the ratchetgate itself — same plan, no screenshots — so "did this fix land" and "did it
cost something elsewhere" are one measurement. A finding measured back inside
one device pixel, or absent from findings entirely, becomes
fixedat its newbaseline; anything else prints why and leaves the ledger alone. A
fixedentryis then the strictest row in the file, so reopening it is a ratchet failure.
The decision is made in TypeScript (
verifyGeometryFixes) beside the ratchet;the
.mjsonly applies the file the gate writes, exactly astriagedoes.3 —
feat(components): point geometry repairs at their sourceObservation now records two more fields per box-model node: the
classattribute verbatim, and the nearest function component above it in the React
fiber tree.
src/lib/geometry-react-fiber.tswalksfiber.return, unwrappingthe
memo/forwardRefobjects React puts intype's place and preferring anexplicit
displayName. It is closure-free like every other function captureserializes into the page, and adds no dependency. React 19 dropped
_debugSource, so a component NAME is the whole pointer — no file, no line.Both fields are optional LABELS riding the repair term and proposal; neither
reaches a finding key, for the same reason an accessible name may not. A node
React never rendered contributes none and nothing fails.
Report repair text is now one executable sentence per term:
geometryRepairCssPropertynames the property that actually exists on eachedge:
padding-inline-end,border-inline-start-width, andcolumn-gap/row-gapfor a gap, which belongs to an axis rather than to one of its edges.4 —
feat(components): fold geometry cards by repair identityA
css-defectfinding gets arepairGroupin the findings stage:(component ?? common ancestor description, term, edge, dominant element).Findings do not merge — keys, baselines, evidence and the ratchet are
untouched. The report folds cards on it, one card per repair, naming the
findings it stands for, and folds strictly inside one ledger status: a
newfinding hidden under a reviewed one would be a review that neverhappened.
geometry:verify-fixtakes--repair-group=<id>to close a wholegroup in one gate run.
Before / after
pnpm test:geometry: 9 tests, 2.6 min, ledger baselines unreadpnpm geometry:triageaccepted-debt, no way to record a fixdebt, pluswont-fix/fixedandgeometry:verify-fix修复建议(结束边):padding 本项 12 vs 参照 10(Δ2px;差值来自本项的 div[role=button])SidebarRowShared 里 div[role=button] 的 padding-inline-end 多 2px(class: pe-3)repairGroup, listing the folded finding keys修复建议…(class: group relative flex h-7 max-w-[180px] …)inside a one-line summaryCI time budget
Measured on this machine (Debian 12, Playwright Chromium, Storybook already
warm), with an isolated run of the plan and no screenshots:
page.evaluateper capturepnpm test:geometrybefore (9 tests)pnpm test:geometryafter (10 tests)That more than doubles the geometry gate, so it is worth saying why the plan
was not trimmed to a subset of stories. A finding's
offsetis the meanover every capture that produced evidence for it, and that is exactly the
number the ledger baselines record. Measuring 6 stories instead of 20 does not
produce a cheaper version of the same numbers; it produces different numbers,
against which the reviewed baselines mean nothing. Enforcing a subset would be
a ratchet on a quantity no human ever reviewed. The
Testsjob's timeout is 30minutes and this leaves it comfortably inside; if the budget becomes a problem,
the honest fix is to split the ratchet into its own job, not to shrink the
plan.
Test plan
pnpm exec tsgo --noEmit— clean. Note:packages/components/tsconfig.jsonhas
include: ["src/**/*"], sopnpm typechecknever typecheckstests/**.The geometry test and spec files were typechecked with an ad-hoc project that
adds
tests/**; they are clean. Flagged below.pnpm exec vitest run tests/geometry-constraint-system.test.ts tests/chat-workspace-geometry.test.ts— 125 passed.pnpm test:geometry— 10 passed on the final branch (8.5 min).pnpm geometry:report /tmp/geo-report— full report regenerated: 20captures, 27 details, 78/80 screenshots, 9.4 min. The report's
findings.jsonand the gate's are identical — same 27 keys, zero offsetdifferences — which is the check that the extracted plan really is the walk
the report used to do inline.
nodes carry a component name and 3247 carry a class list.
Worth stating plainly: on the current tree the two
css-defectfindings landin two different repair groups, so nothing actually folds today. The
mechanism is unit-tested and starts paying the moment one wrong padding shows
up on several rows — which is the shape the 74 debt entries had before the
sidebar rail work landed.
pnpm lint(0 errors) andpnpm formatafter every commit.New unit coverage, all deterministic (no sleeps, no wall clock, fixture
artifacts only):
tests/geometry-constraint-system.test.tsgeometry:triageignoredskipped,promotedleft to the contract checkfixedheld to its near-zero baselineverifyGeometryFixesrefuses apromotedentry (its contract gates it exactly)verifyGeometryFixes: pass re-baselines tofixed; fail leaves the ledger identical; absent finding is a fix at 0; no ledger entry refusescompileGeometryContractscompiles nothing fromdebt/wont-fix/fixedtriageGeometryFindingsdefaults todebtand never moves a baselinerepairGroupsurvives a different label, value and dominant side; changes with component / term / edge; absent with no termsrepairGroupreaches the finding throughcreateGeometryFindingswhile its key is byte-identical toalignmentFindingKeygeometryRepairCssProperty/formatGeometryRepairProposalsentences, including the no-component fallback and the class-list-free summary formreturn, unwrapsmemo/forwardRef, prefersdisplayName, returns nothing for host/anonymous, bounded on a cyclic chain, finds__reactFiber$…tests/chat-workspace-geometry.test.tsFindings not fixed here
Recorded rather than acted on, per the task's scope limits (no discovery,
threshold, identity or report-visual changes).
Ledger baselines are platform-specific, and these were recorded on a
different one. Running the plan here reproduces 27 of the 76 reviewed
findings; the other 49 (mostly
text-baselineand text-vs-iconvisual-centerrows) do not appear at all under Linux font rendering. Ofthe 27, 12 baselines were moved in commit 1 so the ratchet starts green
on the platform CI runs:
FilesFilesConversation DiffDocs 2 + 100 − 20app.tsxPrivate to you: …MachineMachineMessageMore actions×3The 49 unreproduced entries keep their reviews untouched — they are simply
not enforced here. The residual risk is that GitHub's
ubuntu-latestfontsdiffer from this Debian host: entries that reappear there would be checked
against macOS baselines, and genuinely new keys would trip the
unreviewed-finding rule. The first CI run is the measurement; the fix in
either case is
geometry:report+geometry:triageon the CI platform.Note the
Filespair swappinginline-startandinline-endvalues, whichis worth a look on its own.
The report is fragile against concurrent edits. Storybook's Vite dev
server watches the whole package,
tests/**included, so saving any fileduring a report run triggers a full iframe reload and the next
page.evaluatedies with "Execution context was destroyed". Three runsfailed this way before the cause was clear. Not introduced here, but it
makes the 40-minute report much more brittle than it looks.
pnpm typecheckdoes not covertests/**.packages/components/tsconfig.jsonincludes only
src/**/*, so no spec or unit test in this package istypechecked by
pnpm check. Turning it on today surfaces a large number ofpre-existing errors in unrelated tests, so it was left alone.
groupLabel.split(' · ')is still increateReportDetails. It isinseparable from the
measurement-model-divergenceverdict it feeds — Ninstances with identical member offsets are one measurement-model
divergence — and the single card it emits is that verdict's card, not a
second dedup. Removing it would emit N identical cards with nothing to fold
them. Card folding for findings is now
repairGroup, which is where thereal duplication was.
Component names often resolve to a library wrapper. The walk takes the
first
typeof fiber.type === 'function'ancestor as specified, which on thistree is frequently a Radix internal —
Primitive.div,Primitive.span.SlotClone— rather than a Lody component. It is still abetter pointer than a bare DOM description, and skipping library primitives
would be a change to the specified rule, so it is recorded rather than done.
y-axis-parity.jsonno longer exists anywhere in the tree but was stillnamed in
tests/e2e/AGENTS.mdand as "the parity artifact" intests/e2e/support/AGENTS.md. Both references were corrected while editingthose sections.
Context handoff
Instructions for reviewing agents
checkGeometryLedgerRatchet/geometryFindingDevicePixel(the whole gate rests on them),
geometry-capture-plan.tsagainst thecapture walk it replaced in
chat-workspace-geometry-report.spec.ts, and the12 moved baselines in
geometry-ledger.json.than a subset; comparing
|offset|against|baseline|+ one device pixeltaken from the coarsest capture; moving 12 baselines to this platform's
rendering instead of leaving the gate red; folding report cards only inside
one ledger status.
Debian 12 and CI runs
ubuntu-latest, so the first CI run may still reportregressed or unreviewed findings; the fiber component name is only exercised
against Storybook's dev build and often lands on a Radix wrapper; the report
was verified by regeneration and an artifact diff against the gate, not by
image diff.
Authoring context
system — make the ledger executable in CI, split
accepted-debtand addfixed, put source pointers on repair tickets, and deduplicate report cardsby repair rather than by label — without touching discovery, thresholds,
identity rules or report visuals.
tolerances or finding identity; new fields must be labels/evidence and must
never enter a finding key; no new dependency (explicitly no react-grab); the
capture/observation/findings stages must be shared with the report rather
than copied; every
AGENTS.mdstays under 8 KiB.baseline.offsetvalues weremoved to the platform the gate runs on. Statuses, reasons and identities were
not touched, and every moved value is listed above; without this the new gate
would be red on arrival.
geometry:verify-fixrewritesgeometry-ledger.jsonin place, and only when every requested finding passed— a failed verification prints why and writes nothing.
geometry:triagestill never moves a baseline. Both are local developer commands; CI runs
neither.
--afterreplay path andthe pixel-witness pass were not re-verified beyond a full report
regeneration; the ratchet was not run on GitHub Actions before this PR; the
tests/**typecheck gap and the report's HMR fragility were recorded, notfixed.
and in the refactor (the report regenerates and the gate passes); moderate
confidence that the re-baselined ledger is green on
ubuntu-latest, whichthe first CI run settles.