Skip to content

fix(check): key content_overlap and text_occluded collapse by pair, not text#2801

Draft
xuanruli wants to merge 2 commits into
mainfrom
xuanru/content-overlap-collapse-key-text
Draft

fix(check): key content_overlap and text_occluded collapse by pair, not text#2801
xuanruli wants to merge 2 commits into
mainfrom
xuanru/content-overlap-collapse-key-text

Conversation

@xuanruli

@xuanruli xuanruli commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Problem

staticIssueKey (packages/cli/src/utils/layoutAudit.ts) put issue.text in the collapse key. When the animating element is the one the finding names, its text differs at every dense-pass sample, so every sample forms its own group with occurrences: 1, applyPersistenceTier demotes all of them to info, and a genuinely held finding never reaches the error tier that gates a run (ok = errorCount === 0).

Two codes are affected — both carry their identity in two selectors and both are persistence-tiered:

  • content_overlap — a count-up overlapping a static label. Reproduced: identical geometry and an identical 5.9s overlap, differing only in which element comes first in the DOM. With the animating block first: 48x info, ok=true. With the static label first: error, ok=false. Same pixels, opposite verdict.
  • text_occluded — a count-up held under an opaque scrim for the full 6s. This is severity: "error" at every sample by construction, yet it collapsed into 12 single-occurrence info rows and the run passed. Now error occ=12, ok=false.

The precondition (found in review, fixed here)

The first commit dropped text on the premise that the selector pair was already a complete identity. That premise was false. overlapIssue used selectorFor, which returns tag.class1.class2 with no uniqueness check (uniqueSelectorFor exists in the same file for exactly this reason, and framePositionKey returns "" for these codes, so no geometry backstops it).

Consequence, measured on an aliased-sibling fixture — two div.num sibling stat cards, each with one unrelated ~125ms dip onto its own label, one at t=1 and one at t=5:

verdict
text dropped from key, selectorFor (first commit) error occ=2 div.num, span 1→5.125s, ok=false
text dropped from key, uniqueSelectorFor (this PR) two separate warnings, ok=true

Two independent sub-floor transients were merged into one fabricated 4.1s "held" collision and promoted past the 500ms floor. That is a false failure — strictly worse than the false pass being fixed. Both builders now use uniqueSelectorFor, so the pair genuinely is the identity the collapse key assumes.

Verification

  • Aliased-sibling and occluded-count-up fixtures: both verdicts corrected, shown above.
  • Regression sweep over 9 overlap fixtures — slow-crossfade, fast-crossfade, static-sustained-overlap, brief-static-overlap, real-600ms-collision, odometer-reel, odometer-labelclear, countup-over-label, plus clip-path reel control — every verdict unchanged.
  • bunx vitest run in packages/cli: 2148 passed. The one failure (src/telemetry/agent_runtime.test.ts, agent-env detection) reproduces on a clean HEAD with these changes stashed — pre-existing and environment-dependent, unrelated.

Four unit tests added: the count-up promotion (verified to fail with the key change reverted), multi-pair separation on containerSelector (which this change makes load-bearing as identity for the first time), a sub-floor same-pair guard, and a text_box_overflow guard that text-identified codes still separate.

Known limitation, pre-existing and not addressed here

collapseStaticLayoutIssues builds its span from min(firstSeen)/max(lastSeen), so one genuine pair with two far-apart brief transients is still reported as a single long held finding: [b(1.0), b(1.125), b(5.0), b(5.125)] -> error occ=4, firstSeen 1, lastSeen 5.125. This predates the PR (it fires with constant text too) and needs contiguous-run grouping, which changes occurrences/firstSeen/lastSeen semantics for all nine tiered codes — its own change, with its own evidence.

Not in scope

Two other empirically-confirmed content_overlap error-tier defects, each needing its own fix and evidence:

  • A scene seam cross-dissolve >= ~0.95s with adjacent headlines in the same zone is promoted to error. This reproduces on the framework's own scene-transition idiom, including real data-composition-src sub-composition scenes, so ordinary multi-scene films can fail check. It is also not opacity-specific — a text block that merely crosses another and resolves cleanly errors too — so the fix must ask "is either layer transitioning / does the overlap resolve", not special-case crossfades. Related: the boundary is sampling-phase dependent (900ms on-grid = warning, 905ms = error, 900ms off-grid = warning).
  • isClippedAway probes only clip-path, never overflow, so an overflow:hidden odometer reel errors on digits masked out of view. A clip-path reel with identical geometry is clean — confirmed with a control fixture. clipsOverflow already exists in the same file.

Fixtures

The fixtures above are ad-hoc scratch projects, not committed — there is no permanent regression net for them in this PR. Happy to land the two decisive ones (countup-first / aliased-siblings) as committed fixtures if that is wanted.

🤖 Generated with Claude Code

xuanruli and others added 2 commits July 26, 2026 00:52
`staticIssueKey` put `issue.text` in the collapse key, but `overlapIssue`
already names a collision by both of its selectors. When the leading element's
text changes per sample — a count-up, typewriter, or rotating word — every
dense-pass sample formed its own group with `occurrences: 1`, so
`applyPersistenceTier` demoted all of them to `info` and a genuinely held
collision never reached the `error` tier that gates a run.

Empirically: two projects with identical geometry and an identical 5.9s
overlap, differing only in DOM order, verdicted differently — label-first gave
`error`/`ok=false`, count-up-first gave 48x `info`/`ok=true`. After this change
both give `error`/`ok=false` with `occurrences: 48`. Seven other overlap
fixtures (crossfades, static, masked reel, 600ms transient) are unchanged.

Codes that identify themselves by text keep it; only `content_overlap`, which
carries both selectors, drops it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review found the previous commit's premise false: `overlapIssue` used
`selectorFor`, which returns `tag.class1.class2` with no uniqueness check, so
dropping `text` left nothing to separate two aliased siblings. Two unrelated
125ms transients on sibling stat cards merged into one group whose span
bridged the gap between them, promoting past the 500ms floor to `error` — a
false failure worse than the false pass being fixed.

Both builders now use the file's own `uniqueSelectorFor`, so the pair really is
the identity the collapse key assumes. Measured on an aliased-sibling fixture:
`error occ=2 div.num` / `ok=false` before, two separate warnings / `ok=true`
after.

`text_occluded` had the identical defect and is included: it carries both
selectors plus the animating element's text, and is persistence-tiered, so a
count-up held under an opaque scrim for 6s — a hard `error` at every sample —
collapsed into 12 single-occurrence `info` rows and the run passed. Now
`error occ=12` / `ok=false`.

Per-code policy moved next to `PERSISTENCE_TIERED_CODES` as a named set, and
`dedupeLayoutIssues` keeps raw text deliberately (it pins an exact time, where
text still separates concurrent findings) with a note so the divergence is not
"fixed" back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xuanruli xuanruli changed the title fix(check): key content_overlap collapse by pair, not text fix(check): key content_overlap and text_occluded collapse by pair, not text Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant