feat(check): let a held composition declare itself static - #2804
Draft
xuanruli wants to merge 1 commit into
Draft
Conversation
`sweep_static` is hardcoded `error`, exempt from persistence tiering, and gates the run. It fires whenever the geometry+opacity fingerprint is identical across every sample — so a held title card or logo lockup had no passing spelling at all. Measured: a fully lint-clean static card, GSAP timeline registered, paused and seekable, still failed `check`. `data-layout-allow-static` on the composition root now demotes the finding to `info`. It is not suppressed: the claim is the author's and can be wrong, and dropping the finding produced a confidently green report in which every layout, occlusion and contrast verdict came from one repeated frame with no trace left. `info` does not gate even under `--strict` and survives `--json`. Root-scoped, resolved by the same selector chain the fingerprint itself uses, so the opt-out and the measurement agree on what "the composition" is. A first attempt reused `data-no-timeline` and read it document-wide; review showed both halves were wrong. Its meaning at the engine and lint is per-host "does not register window.__timelines", not "motionless" — and this repo ships `css-spinner-render-compat` and `raf-ball-render-compat`, which carry it and DO animate via CSS keyframes and rAF. Those are exactly the unseekable cases the guard exists to catch, so that version silenced it where it matters most; a stray attribute on any child disabled the guard for the whole run as well. The predicate lives in layout-audit.browser.js beside the fingerprint it must agree with, so the root-only rule is unit-testable — reverting it to a document-wide query now fails two tests instead of passing all of them. Docs updated: the escape-hatch list and the "no way out" paragraph in lint-validate-inspect.md, a row in data-attributes.md, and cli.mdx. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
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
sweep_staticis hardcodedseverity: "error", is excluded from persistence tiering, and gates the run. It fires whenever the geometry+opacity fingerprint is identical across every sample — so a held title card or logo lockup had no passing spelling at all.Measured on
main: a fully lint-clean static card (GSAP timeline registered, paused, seekable) still failscheck, with or without any existing attribute. There was no opt-out.Change
data-layout-allow-staticon the composition root demotes the finding toinfo.Not suppressed — demoted. The claim is the author's and can be wrong. Dropping the finding produced a confidently green report in which every layout, occlusion and contrast verdict came from one repeated frame, with no trace anywhere.
infodoes not gate even under--strictand survives--json, so the record stays:Root-scoped, resolved by the same selector chain the fingerprint itself uses (
[data-composition-id][data-width][data-height]→[data-composition-id]→body), so the opt-out and the measurement agree on what "the composition" is.Two rejected attempts, and why
A first version reused
data-no-timelineand read it document-wide. Review showed both halves were wrong, with in-repo evidence:frameCapture.ts:1471,1524,1536) and lint (composition.ts:693) is per-host "does not registerwindow.__timelines" — a render-timing opt-out — not "motionless". This repo shipspackages/producer/tests/css-spinner-render-compat(@keyframes spin) andraf-ball-render-compat(requestAnimationFrame), both of which carry that attribute and do animate. Those are precisely the unseekable casessweep_staticexists to catch, so v1 silenced the guard where it matters most.document.querySelector("[data-no-timeline]")matched any element anywhere: a nested sub-composition host, or a bare decorative<div>, disabled the guard for the whole run. Reviewers reproduced both.data-no-timelineto a card that does register a timeline would also change render behaviour (skipping the registration poll and the__hfForceTimelineRebindordering guarantee) purely to silence a check.A second version still did
return []. That is the "no trace" problem above.Verification
info,ok=true,exit 0(also under--strict)error,ok=falseerror,ok=falseerror<div>error,ok=falseerror<template>error,ok=falseerrorerror,ok=falseerrorcss-spinner-render-compat,raf-ball-render-compatdata-no-timeline)13 registry examples: none gains or loses
sweep_static.packages/cli: 2153 tests pass — the single failure (src/telemetry/agent_runtime.test.ts, agent-env detection) reproduces on cleanHEADand is environment-dependent, unrelated. oxlint / oxfmt /tsc --noEmitclean.The predicate lives in
layout-audit.browser.jsbeside the fingerprint it must agree with, which makes the root-only rule unit-testable: reverting it to a document-wide query fails two tests instead of passing all of them. A reviewer specifically asked for this — without it, a future refactor back to v1 would have kept every test green and silently reinstated the defect.Known shapes, deliberately not fixed here
data-layout-allow-overflow. The residual risk is an attribute added once for a legitimately held card that stays after the piece later gains real, broken animation. Inherent to everydata-layout-allow-*hatch; theinfoline is the mitigation.data-layout-allow-static="false"still opts out (hasAttribute), consistent with the rest of the family. Noted as boolean-by-presence in the docs.data-composition-id+data-width+data-heighthijacks root resolution. This is pre-existing across the whole layout audit —__hyperframesLayoutGeometryand every other detector root themselves on that same first match — so reusing that definition is the consistent choice. Fixing root resolution belongs in its own change.Also confirmed about this guard, not fixed here
From the same testing round, each needing its own change:
backgroundColoror SVGstroke-dashoffset(the fingerprint is geometry+opacity plus a pixel hash forcanvas/videoonly), and on one whose motion all completes in the first 200 ms. The second is documented as intended behaviour inlint-validate-inspect.md, so changing it is a product decision, not a bug fix.The principled fix for both is a positive seek probe (compare the registered timeline's reported
time()across seeks) rather than inferring from rendered geometry. That makes the guard louder, not quieter, so it must not ride along with this change.Docs
data-no-timelineturned out to be essentially undocumented (one changelog hit), and the docs described the opposite stance with a workaround. Updated in this PR: the escape-hatch list and the "no way out" paragraph inskills/hyperframes-cli/references/lint-validate-inspect.md, a new row inskills/hyperframes-core/references/data-attributes.md(root-only, no render-path effect), anddocs/packages/cli.mdx.🤖 Generated with Claude Code