Skip to content

feat(components): make a Y rail's row geometric, with the DOM row as an evidence prior - #352

Closed
lodystage[bot] wants to merge 6 commits into
geometry-flywheel-ratchet-and-repairfrom
geometry-geometric-row-discovery
Closed

feat(components): make a Y rail's row geometric, with the DOM row as an evidence prior#352
lodystage[bot] wants to merge 6 commits into
geometry-flywheel-ratchet-and-repairfrom
geometry-geometric-row-discovery

Conversation

@lodystage

@lodystage lodystage Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5940ec34cd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1454 to +1455
row.centers.push(center);
row.heights.push(height);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent the median band from drifting across rows

For a ladder of 16px-high extents centered at 8, 16, 20, and 24, the first two establish a band centered at 12, the third joins and moves its median center to 16, and the fourth then joins even though it has zero overlap with the first extent. Mutating the reference band after every accepted member therefore still permits neighbor-to-neighbor chaining, which can merge distinct visual lines and suppress or misassign cross-family outliers; keep the row's comparison band stable or revalidate the completed row against a non-drifting reference.

AGENTS.md reference: packages/components/src/lib/AGENTS.md:L99-L103

Useful? React with 👍 / 👎.

@wibus-wee
wibus-wee marked this pull request as draft September 3, 2026 12:27
@wibus-wee wibus-wee closed this Sep 3, 2026
@wibus-wee

Copy link
Copy Markdown
Member

Superseded by #354, recreated under the intended author account and included in Stack #355.

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