fix(check): key content_overlap and text_occluded collapse by pair, not text#2801
Draft
xuanruli wants to merge 2 commits into
Draft
fix(check): key content_overlap and text_occluded collapse by pair, not text#2801xuanruli wants to merge 2 commits into
xuanruli wants to merge 2 commits into
Conversation
`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>
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.
Problem
staticIssueKey(packages/cli/src/utils/layoutAudit.ts) putissue.textin 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 withoccurrences: 1,applyPersistenceTierdemotes all of them toinfo, and a genuinely held finding never reaches theerrortier 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: 48xinfo,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 isseverity: "error"at every sample by construction, yet it collapsed into 12 single-occurrenceinforows and the run passed. Nowerror occ=12,ok=false.The precondition (found in review, fixed here)
The first commit dropped
texton the premise that the selector pair was already a complete identity. That premise was false.overlapIssueusedselectorFor, which returnstag.class1.class2with no uniqueness check (uniqueSelectorForexists in the same file for exactly this reason, andframePositionKeyreturns""for these codes, so no geometry backstops it).Consequence, measured on an aliased-sibling fixture — two
div.numsibling stat cards, each with one unrelated ~125ms dip onto its own label, one at t=1 and one at t=5:selectorFor(first commit)errorocc=2div.num, span 1→5.125s,ok=falseuniqueSelectorFor(this PR)warnings,ok=trueTwo 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
slow-crossfade,fast-crossfade,static-sustained-overlap,brief-static-overlap,real-600ms-collision,odometer-reel,odometer-labelclear,countup-over-label, plusclip-pathreel control — every verdict unchanged.bunx vitest runinpackages/cli: 2148 passed. The one failure (src/telemetry/agent_runtime.test.ts, agent-env detection) reproduces on a cleanHEADwith 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 atext_box_overflowguard that text-identified codes still separate.Known limitation, pre-existing and not addressed here
collapseStaticLayoutIssuesbuilds its span frommin(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 changesoccurrences/firstSeen/lastSeensemantics for all nine tiered codes — its own change, with its own evidence.Not in scope
Two other empirically-confirmed
content_overlaperror-tier defects, each needing its own fix and evidence:error. This reproduces on the framework's own scene-transition idiom, including realdata-composition-srcsub-composition scenes, so ordinary multi-scene films can failcheck. 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).isClippedAwayprobes onlyclip-path, neveroverflow, so anoverflow:hiddenodometer reel errors on digits masked out of view. Aclip-pathreel with identical geometry is clean — confirmed with a control fixture.clipsOverflowalready 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