Skip to content

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
geometry-flywheel-ratchet-and-repairfrom
geometry-geometric-row-discovery
Draft

feat(components): make a Y rail's row geometric, with the DOM row as an evidence prior#354
wibus-wee wants to merge 10 commits into
geometry-flywheel-ratchet-and-repairfrom
geometry-geometric-row-discovery

Conversation

@wibus-wee

Copy link
Copy Markdown
Member

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-instance cross-family
grouping one DOM row a geometric row spanning ≥2 (row family, scope) pairs
members ≥2 ≥3, and ≥2 of them reaching the line
anchors any visual-center only
captures 1 2 — one capture only proposes
finding key unchanged same structural key + cross-family
  • assignGeometricRows assigns an extent to the line whose median band it overlaps by
    half — 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 levels names on X.
  • One element, one rail, and the DOM prior wins: an element a row-instance rail
    measures never joins a cross-family one.
  • Capture now measures an atom outside every row on the Y axis too. Without it the DOM
    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>, not
    the coordinate-derived id its X candidates carry — see Findings during the work.

Acceptance evidence

Baseline: geometry:report on geometry-flywheel-ratchet-and-repair@faf5969 (this PR's
base). Head: the same command on this branch.

Existing findings are byte-identical.

base head
findings 27 27
keys removed 0
offset / kind / anchor / captureCount changed 0
new findings 0
finding-diff new=0 changed=0 resolved=48 rekeyed=0 new=0 changed=0 resolved=48 rekeyed=0

resolved=48 is identical on both sides, so it is pre-existing ledger drift on the base
branch, not introduced here.

Rails. All 798 row-instance rails match the base exactly — same rowId, anchor,
line, sampleSize, support, and per-member coordinate and outlier flag — verified rail
by rail, not just in aggregate. Plus 12 new cross-family rails.

base rails total: 798
head rails by evidence: {'row-instance': 798, 'cross-family': 12}
captures whose row-instance rails differ: 0
cross-family rails: 12   outliers: 0

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's
project chip, whose three primitives come from three different row families that no DOM row
relates:

workspace:wide-expanded  line=779.5  members=3  support=3  spread=0.5  outliers=0
    dom-401  svg   family=span[text]>svg[img]                     coord=779.5  delta=0
    dom-396  svg   family=button[button]>svg[img]                 coord=780    delta=0.5
    dom-403  text  family=button[button]>span[text],span[text]    coord=779.5  delta=0

They are aligned, so there is no outlier and therefore no finding — the rule fires,
measures, and reports agreement. Each is a cross-family-candidate card in the report.

session.topbar — correcting the premise. The candidateCount: 33, rails: [] in the
task 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:

anchor=block-center   line=46.00  n=11  support=11  outliers=0
anchor=visual-center  line=46.00  n=11  support=10  outliers=1   ← dom-137 'Private' 1.00px
anchor=text-baseline  line=50.50  n=3   support=2   outliers=1   ← dom-137 'Private' 1.00px

So the acceptance criterion "a session.topbar geometric 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

  1. A serialized helper closed over a module binding. installGeometryBrowserHelpers
    ships selectVisualRowSlots to the page as source, so naming the new
    GEOMETRY_ROW_BAND_OVERLAP constant in its default made every capture die with
    ReferenceError: 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.

  2. Coordinate-derived row ids can collide. Giving atoms Y candidates under the
    coordinate-derived id their X candidates use put the permission dialog's Submit label
    and icon in one row purely because their centre and left edge rounded alike — minting a
    row-spread finding at the two-member bar, the one place a single capture is evidence
    enough, on a coincidence rather than on structure. visual-atom:<primitiveId> removes it
    and 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.

  3. Three sibling AGENTS.md files are at the 8 KiB cap. Fitting the new invariant meant
    splitting it by ownership — the contract (two bars, priority, key) in tests/e2e, the
    mechanism (median band, no chaining, atoms) in src/lib — and compressing wording in the
    base 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/e2e 8188,
    src/lib 8164.

Verification

  • pnpm --filter @lody/components typecheck — passes.
  • pnpm lint — 0 errors.
  • Geometry unit tests — 150 pass (82 + 68), covering: chaining blocked, same-family
    priority, a two-member row spending its members, cross-family needs three from two
    families, non-overlapping extents, DPR snap, visual-center only, and the parity
    oracle
    (the same rows discovered with and without the singletons only a geometric row
    can group are deep-equal).
  • pnpm test:geometry11/11 pass, including this base branch's
    every measured geometry finding stays inside its reviewed ledger baseline, the existing
    vertical row alignment is discovered without geometry marker attributes, and the new
    a cross-family geometric row reports its outlier and no DOM row.
  • pnpm format — no changes.
  • pnpm check fails on 677 pre-existing act is not a function failures across 134
    .tsx test files. Verified unrelated: the same tests fail identically on
    origin/geometry-flywheel-ratchet-and-repair with none of this PR's commits, and on
    feat/geometry-constraint-system@5036a11. Not fixed here.

Review focus

  • The same-family-wins rule and the claim that row-instance rails are untouched: the
    parity oracle is a unit test, and the 798-rail comparison above is the empirical check.
  • Whether visual-atom:<primitiveId> for atom Y candidates is the right call, or whether the
    Submit spread should be caught by widening row detection instead.
  • The wording compressions in tests/e2e/AGENTS.md, which touch this base branch's prose.

🤖 Generated with Claude Code

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]
Leeeon233 and others added 4 commits September 3, 2026 13:07
"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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants