feat(components): make a Y rail's row geometric, with the DOM row as an evidence prior - #354
Draft
wibus-wee wants to merge 10 commits into
Draft
Conversation
A Y rail was "ONE row instance", so the DOM row was an eligibility test: a control rendering outside the DOM row of the things it lines up with could not be compared to them at all, and every cross-structural misalignment was invisible by construction rather than by measurement. `session.topbar` reporting `candidateCount: 33, rails: []` is that rule, not a property of the page. The row is now GEOMETRIC. `assignGeometricRows` puts an extent on the line whose MEDIAN band it overlaps by half — to the median band, never neighbour to neighbour, because a ladder of half-overlaps would chain two lines of different heights into one, exactly as chaining intermediate coordinates would merge two indentation levels into one X rail. The threshold is the "at least half" a row band already asked of a slot, now named as `GEOMETRY_ROW_BAND_OVERLAP`. `selectVisualRowSlots` keeps the literal because capture serializes it into the page, where a module binding does not exist; a unit test rebuilds both serialized helpers outside this module so that scope is checked here rather than ten minutes into a report run. The DOM row stays, as a PRIOR that sets the evidence bar rather than eligibility. `discoverBlockAlignmentRails` returns both kinds, tagged: - `row-instance` is the rule that has always decided a Y rail, byte for byte — same grouping by (rowId, anchor), same `reachesLine` filter, same median, same tolerance. Two members, any anchor, one capture. - `cross-family` is what a geometric row still owes an explanation for: primitives no row-instance rail measures, from two or more (row family, scope) pairs, at `visual-center` only, three members at least and two of them reaching the line. Same claim, weaker evidence. ONE element, ONE rail, and the DOM prior WINS: an element a row-instance rail already measures never joins a cross-family one, so the rails that exist today keep their members and their lines. The parity oracle is a unit test — the same rows discovered with and without the singletons only a geometric row can group are deep-equal. Model: claude-opus-5[1m]
…pture repeats it A `cross-family` rail's whole support is that primitives no DOM structure relates happen to agree on a line, and one capture agreeing is a coincidence. So a capture only PROPOSES one, and an outlier becomes a finding when the SAME member set forms the rail again in another capture and the same member leaves the line in the same direction. Members are matched by structural identity, never by a coordinate: a line that moved as a whole is still the same line. - `observeGeometryCaptures` passes each Y candidate's declared scope into discovery, which is half of what decides a rail's evidence bar. - `collectGeometryCrossFamilyProposals` labels and identifies every proposed rail once, so a report card prints exactly what a finding would — one labelling pipeline, not a second one that could disagree about a member's name, its offset, or the key it would carry. The repeated-row labelling closure moves to module scope for it. - The per-row Y path takes `row-instance` rails only. A cross-family rail never picks a verdict anchor and never reaches the one-capture path; it is aggregated across captures at `visual-center` or not at all. - `GeometryFindingKind` gains `cross-family`, and `alignmentFindingKey` appends that term. The outlier is the same element a row Y finding would name, so the kind is what keeps the two questions apart. No coordinate, scope name or accessible name enters the key, and every existing key is unchanged. - Marker parity and marker-removal readiness read `row-instance` rails only: a marker rule names the members of one DOM row, so letting a weaker cross-family match stand in would report a marker as removable on evidence the marker never had. Model: claude-opus-5[1m]
A cross-family finding is a Y finding, so it takes the same card: `createFindingBlockDetail` draws its guide across the whole capture instead of between its outermost members — a line drawn only between them would read as a DOM row that happens to be wide — and dashes it, so a candidate line is never mistaken for a row median. A rail one capture proposed but no second capture confirmed becomes a `cross-family-candidate` detail. It is shaped as the finding it is not yet and passed through that SAME annotator, so every number it prints is the rail's own measurement of that member rather than a second pipeline's opinion; it carries no finding key, no ledger status and no classification, because nothing has reviewed it. Proposals share the existing deviation order and `MAX_Y_FINDING_CARDS` screenshot budget rather than asking for one of their own. Model: claude-opus-5[1m]
…d translateY Same shape as the row-alignment gate: strip every alignment marker, discover, inject a `translateY` this repository owns, discover again, and diff. The probe is any aligned icon on any tight cross-family geometric row rather than a named header control — naming one would make the gate about a product surface instead of about the rule. Asserts the injected element is the only one added to its rail's outliers, at the offset that was injected, and that no row-instance rail changed at all: the DOM prior decides the evidence bar, so a cross-family probe must not reach a DOM row. Model: claude-opus-5[1m]
Replace "A Y rail is ONE row instance" with the rule that replaced it: the row is geometric, assignment is to the row's median band rather than neighbour to neighbour, and the DOM row is a prior that sets the evidence bar instead of a gate on eligibility. Names the two bars (`row-instance`, `cross-family`), the same-family priority that keeps today's rails intact, and where a single capture's proposal lives. Model: claude-opus-5[1m]
Capture emitted Y candidates only for primitives inside a DETECTED DOM row, so the DOM row was still an eligibility test one layer before discovery: a control the row detector rejected could not be compared to anything vertically, whatever the rules downstream said. With it, the geometric row had nothing to group — at most one unclaimed element per capture, and zero cross-family rails in the whole report. An atom now reports the same five anchors a row member does. Its row id is `visual-atom:<primitiveId>`, not the coordinate-derived id its X candidates carry: two atoms whose centre and left edge round alike would otherwise share a row and be compared at the two-member bar — the one place a single capture is evidence enough — on a coincidence rather than on any structure relating them. That happened, and it minted a `row-spread` finding for the permission dialog's Submit label and icon; naming the row after the primitive removes it and changes nothing else. Each atom is therefore alone in its row and can never reach the row-instance bar by itself. Every existing row-instance rail keeps its members, its line and its key. Model: claude-opus-5[1m]
"Overlaps the row's median band by half" divided by the SMALLER of the two extents, so it only ever asked whether the shorter one was covered. A 44px landing `<h1>` covers a 17px sidebar row's band completely and joined that row at "100%" — while the band covered barely a third of the heading — and the heading's visual centre then read 12px off a line it was never on. Divide by the LARGER instead: the overlap has to cover half the member AND half the band. The threshold is unchanged, still `GEOMETRY_ROW_BAND_OVERLAP`. The fixture is that capture verbatim — the landing heading and the four sidebar row primitives beside it, with the coordinates `workspace:wide-expanded` measured — and it fails under the old rule. No output moves: over the whole capture plan, all 798 row-instance rails are byte-identical (they group by DOM row, so a geometric row cannot reach them), all 12 cross-family rails keep the same member sets, lines and outlier counts, and findings stay at 27 with `new=0 changed=0 resolved=48 rekeyed=0`. What this removes is a latent false positive: a rail-level comparison over geometric rows reported this heading, and only this heading, in 8 of 32 rows. Model: claude-opus-5[1m]
No capture rendered more than ONE region header. The Sidebar's lives in `geometry-chatworkspace--*`, the Session tab bar's in `sessions-sessionconversationpage--*`, the right panel's in `sessions-sessionsidepaneltabbar--*`, and a geometric row is per CAPTURE — so "do these three headers share a line?" was not a question discovery could answer wrongly. It was a question nothing could ask. `WorkspaceSessionSidePanel` composes them through the production shell that positions them: `WebWorkspaceFrame` → `DesktopSessionDetailLayout` → `SessionTabBar` + `SessionSidePanelTabBar`, over the Sidebar, agent-config and session fixtures this story file already declares. 1440×900 at 2×, in `GEOMETRY_SESSION_STATE_CAPTURES`, so the report and the gate walk the same entry. No marker, and no attribute beyond the discovery scope the side panel already declares in its own story. Determinism: `DesktopSessionDetailLayout` persists its split under an `autoSaveId`, so a size left by a previous drag would move every measured x here. The key is cleared during render rather than in an effect, which would race the first paint. Settling is the existing `measureSettledChatWorkspace` (the story id starts with `geometry-chatworkspace--`); no new wait, no sleep. It measures 63 row-instance rails and one cross-family rail, none of which existed before, and the 20 pre-existing captures are untouched: their 798 row-instance rails stay byte-identical. Nine findings follow from rows nobody had measured — the sidebar and side panel at this composition's widths — and `geometry:triage` records them, seven as new `debt` plus two re-keyed reviews carried over from resolved keys. Model: claude-opus-5[1m]
… row Symmetric membership made the old probe unsound: it picked the first aligned icon, which on the landing chip rail is the member whose height IS the median band. Moving that member drags the band, a shorter member then falls off the row, and the rail vanishes instead of reporting the injection — the gate failed with `probedMember` undefined rather than with an offset. Pick the TALLEST member that still clears the overlap after the shift, and say the arithmetic — `(band - shift) / height >= GEOMETRY_ROW_BAND_OVERLAP` — in the test rather than trusting a particular icon to survive. The band height is a median, so the tallest member never sets it and moving it leaves the row intact. Model: claude-opus-5[1m]
Row membership is half of BOTH extents, not half of the smaller one. Names the case that forced it — a 44px heading covering a 17px row's band whole — beside the chaining rule it sits next to, since the two guard opposite directions of the same test. 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.
What this changes
A Y rail was "ONE row instance", so the DOM row was an eligibility test: a control
rendering outside the DOM row of the things it lines up with could not be compared to them
at all. Every cross-structural misalignment was invisible by construction rather than
by measurement.
The row is now geometric, and the DOM row is a prior that sets the evidence bar.
row-instancecross-familyvisual-centeronlycross-familyassignGeometricRowsassigns an extent to the line whose median band it overlaps byhalf — to that band, never neighbour to neighbour, because a ladder of half-overlaps would
chain two lines of different heights into one, the same failure
intermediate coordinates must not chain distinct indentation levelsnames on X.row-instancerailmeasures never joins a
cross-familyone.row still gated eligibility one layer earlier and the new path was dead code (≤1 unclaimed
element per capture, 0 cross-family rails). Its row id is
visual-atom:<primitiveId>, notthe coordinate-derived id its X candidates carry — see Findings during the work.
Acceptance evidence
Baseline:
geometry:reportongeometry-flywheel-ratchet-and-repair@faf5969(this PR'sbase). Head: the same command on this branch.
Existing findings are byte-identical.
finding-diffresolved=48is identical on both sides, so it is pre-existing ledger drift on the basebranch, not introduced here.
Rails. All 798
row-instancerails match the base exactly — samerowId,anchor,line,sampleSize,support, and per-member coordinate and outlier flag — verified railby rail, not just in aggregate. Plus 12 new
cross-familyrails.The 12 cross-family rails, and why they produce no finding. All 12 are the same
geometric row in
main.chat-landing, one per workspace capture — the landing composer'sproject chip, whose three primitives come from three different row families that no DOM row
relates:
They are aligned, so there is no outlier and therefore no finding — the rule fires,
measures, and reports agreement. Each is a
cross-family-candidatecard in the report.session.topbar— correcting the premise. ThecandidateCount: 33, rails: []in thetask description is the X rail count for that scope; this PR does not touch the X axis,
so it is 0 before and after. On the Y axis those controls were never invisible: all 11
primitives share one DOM row and already formed 3 rails, unchanged by this PR:
So the acceptance criterion "a
session.topbargeometric row forms at least one rail" holds,but it held before this PR too. The real gap the architecture change closes is elsewhere, and
the honest measured result is the 12 cross-family rails above.
Findings during the work
A serialized helper closed over a module binding.
installGeometryBrowserHelpersships
selectVisualRowSlotsto the page as source, so naming the newGEOMETRY_ROW_BAND_OVERLAPconstant in its default made every capture die withReferenceError: GEOMETRY_ROW_BAND_OVERLAP is not defined. The helper keeps the literal;a unit test now scans each serialized helper's source for every exported name, so this
fails in one second instead of ten minutes into a report run. Verified by reintroducing
the closure and watching the test fail.
Coordinate-derived row ids can collide. Giving atoms Y candidates under the
coordinate-derived id their X candidates use put the permission dialog's
Submitlabeland icon in one row purely because their centre and left edge rounded alike — minting a
row-spreadfinding at the two-member bar, the one place a single capture is evidenceenough, on a coincidence rather than on structure.
visual-atom:<primitiveId>removes itand changes nothing else. The 4.5px spread inside that button is real but is only visible
through that coincidence; catching it properly means the row detector accepting narrow
rows, which is out of scope here.
Three sibling
AGENTS.mdfiles are at the 8 KiB cap. Fitting the new invariant meantsplitting it by ownership — the contract (two bars, priority, key) in
tests/e2e, themechanism (median band, no chaining, atoms) in
src/lib— and compressing wording in thebase branch's prose. Those edits are wording only; every invariant is preserved. Worth a
look since some of that text is this base branch's. Final sizes:
tests/e2e8188,src/lib8164.Verification
pnpm --filter @lody/components typecheck— passes.pnpm lint— 0 errors.priority, a two-member row spending its members, cross-family needs three from two
families, non-overlapping extents, DPR snap,
visual-centeronly, and the parityoracle (the same rows discovered with and without the singletons only a geometric row
can group are deep-equal).
pnpm test:geometry— 11/11 pass, including this base branch'severy measured geometry finding stays inside its reviewed ledger baseline, the existingvertical row alignment is discovered without geometry marker attributes, and the newa cross-family geometric row reports its outlier and no DOM row.pnpm format— no changes.pnpm checkfails on 677 pre-existingact is not a functionfailures across 134.tsxtest files. Verified unrelated: the same tests fail identically onorigin/geometry-flywheel-ratchet-and-repairwith none of this PR's commits, and onfeat/geometry-constraint-system@5036a11. Not fixed here.Review focus
row-instancerails are untouched: theparity oracle is a unit test, and the 798-rail comparison above is the empirical check.
visual-atom:<primitiveId>for atom Y candidates is the right call, or whether theSubmitspread should be caught by widening row detection instead.tests/e2e/AGENTS.md, which touch this base branch's prose.🤖 Generated with Claude Code