Skip to content

feat(check): let a held composition declare itself static - #2804

Draft
xuanruli wants to merge 1 commit into
mainfrom
xuanru/sweep-static-allow-static
Draft

feat(check): let a held composition declare itself static#2804
xuanruli wants to merge 1 commit into
mainfrom
xuanru/sweep-static-allow-static

Conversation

@xuanruli

Copy link
Copy Markdown
Contributor

Problem

sweep_static is hardcoded severity: "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 fails check, with or without any existing attribute. There was no opt-out.

Change

data-layout-allow-static on the composition root demotes the finding to info.

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. info does not gate even under --strict and survives --json, so the record stays:

Layout
  ℹ t=0s sweep_static [data-composition-id] — No geometry change across the sampled grid; this
    composition declares data-layout-allow-static, so every other verdict reflects a single frame.
        Fix: Remove data-layout-allow-static if this composition is meant to animate.
  0 error(s), 0 warning(s), 1 info(s)

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-timeline and read it document-wide. Review showed both halves were wrong, with in-repo evidence:

  • Its meaning at the engine (frameCapture.ts:1471,1524,1536) and lint (composition.ts:693) is per-host "does not register window.__timelines" — a render-timing opt-out — not "motionless". This repo ships packages/producer/tests/css-spinner-render-compat (@keyframes spin) and raf-ball-render-compat (requestAnimationFrame), both of which carry that attribute and do animate. Those are precisely the unseekable cases sweep_static exists 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.
  • Adding data-no-timeline to a card that does register a timeline would also change render behaviour (skipping the registration poll and the __hfForceTimelineRebind ordering guarantee) purely to silence a check.

A second version still did return []. That is the "no trace" problem above.

Verification

fixture attribute placement result baseline
held card root info, ok=true, exit 0 (also under --strict) error, ok=false
held card nested composition host only error, ok=false error
held card decorative child <div> error, ok=false error
held card inside a <template> error, ok=false error
genuinely frozen timeline none error, ok=false error
css-spinner-render-compat, raf-ball-render-compat (data-no-timeline) not exempted unchanged

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 clean HEAD and is environment-dependent, unrelated. oxlint / oxfmt / tsc --noEmit clean.

The predicate lives in layout-audit.browser.js beside 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

  • The hatch does let a genuinely frozen composition pass (verified) — same contract as 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 every data-layout-allow-* hatch; the info line 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.
  • A pre-root sibling host carrying data-composition-id + data-width + data-height hijacks root resolution. This is pre-existing across the whole layout audit — __hyperframesLayoutGeometry and 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:

  • FPs: it fires on a composition whose only animation is backgroundColor or SVG stroke-dashoffset (the fingerprint is geometry+opacity plus a pixel hash for canvas/video only), and on one whose motion all completes in the first 200 ms. The second is documented as intended behaviour in lint-validate-inspect.md, so changing it is a product decision, not a bug fix.
  • FN: it misses the bug its own fixHint names — an unregistered autoplaying timeline makes the layout stage run 0 samples, so nothing is compared and only lint catches it.

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-timeline turned 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 in skills/hyperframes-cli/references/lint-validate-inspect.md, a new row in skills/hyperframes-core/references/data-attributes.md (root-only, no render-path effect), and docs/packages/cli.mdx.

🤖 Generated with Claude Code

`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>
@mintlify

mintlify Bot commented Jul 26, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview Jul 26, 2026, 12:23 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@mintlify

mintlify Bot commented Jul 26, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟡 Building Jul 26, 2026, 12:23 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

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