Problem
The hardest class of bug in agent-authored compositions is render ≠ preview: animation that looks right while scrubbing the studio but is wrong in the rendered MP4, because the renderer seeks a paused timeline rather than playing it. Today the only lever for this is render --docker (font/Chrome determinism) — there is nothing that verifies the motion itself.
lint catches structural issues and inspect catches layout at sampled frames (overflow, clipping, and — via #1435 / #1436 — occlusion and text overlap). But neither can answer questions about motion correctness:
An author (human or agent) currently has no way to state an intent and have it checked against what the renderer will actually produce. The feedback loop is "render the MP4, watch it, eyeball it" — which is exactly what doesn’t scale for agent workflows.
Proposed solution
A declarative motion-assertion spec, evaluated against the same seeked timeline the renderer uses (not a simulation). The engine already seeks; this samples a handful of selectors on a fine time grid, builds an element × time matrix of { rect, opacity, visible }, and evaluates constraints, emitting findings in the existing audit shape (code, severity, time, selector, message, fixHint).
Proposed assertion kinds (all framework-agnostic, no app coupling):
| Assertion |
Meaning |
appearsBy(selector, t) |
element becomes visible (opacity ≥ threshold) no later than t — catches reveals the seek skips |
before(a, b) |
a first appears strictly before b — catches broken stagger order |
staysInFrame(selector) |
once visible, the element’s box never leaves the canvas — catches off-frame drift mid-tween |
keepsMoving([withinSelector]) |
no static window longer than N seconds (nothing moves ≥2px / opacity ≥0.08) — catches frozen shots |
Sketch:
{
"duration": 6,
"assertions": [
{ "kind": "appearsBy", "selector": "#headline", "bySec": 0.5 },
{ "kind": "before", "a": "#headline", "b": "#cta" },
{ "kind": "staysInFrame", "selector": ".card" },
{ "kind": "keepsMoving" }
]
}
✗ motion_appears_late t=0.83s #headline — appears at 0.83s but should be visible by 0.50s (check its entrance reveal fires under seek)
✗ motion_out_of_order #cta before #headline — reorder the entrances
Two delivery options — would value maintainer steer:
- A new verb
hyperframes verify <spec> (spec file alongside the composition), or
- Extend
inspect to read an optional assertions file and fold motion findings into its output.
This builds directly on the layout-audit work in #1435 / #1436 (same seek, same finding shape, same JSON envelope) and shares the engine’s existing seek path — no extra Chrome launch or render.
Alternatives considered
render --docker — fixes font/Chrome nondeterminism only; says nothing about whether a reveal fired or an element drifted off-frame.
- Visual snapshot diffing — brittle, needs golden frames per composition, and reports that something changed without the intent ("this should be visible by 0.5s"). Poor signal for agents.
- Manual preview review — the status quo; it’s exactly the step that doesn’t scale and that misses seek-only divergences.
Additional context
We run a version of this in production (assertions evaluated against the seeked GSAP timeline via the engine) and it reliably catches the seek-only failures above before they reach a render. Happy to contribute the implementation as a PR (or a stack) if there’s appetite and once the surface is agreed.
Out of scope for a first cut: beat/music-relative timing (onBeat) — that’s app-specific and can live downstream.
Open questions for scoping:
- New
verify command vs. an inspect extension?
- Spec format/location — sidecar JSON,
data-* attributes on elements, or both?
- Severity model — should a failed assertion be
error by default, and should --strict gate CI?
(Author of #1435 / #1436, which this extends.)
Problem
The hardest class of bug in agent-authored compositions is render ≠ preview: animation that looks right while scrubbing the studio but is wrong in the rendered MP4, because the renderer seeks a paused timeline rather than playing it. Today the only lever for this is
render --docker(font/Chrome determinism) — there is nothing that verifies the motion itself.lintcatches structural issues andinspectcatches layout at sampled frames (overflow, clipping, and — via #1435 / #1436 — occlusion and text overlap). But neither can answer questions about motion correctness:An author (human or agent) currently has no way to state an intent and have it checked against what the renderer will actually produce. The feedback loop is "render the MP4, watch it, eyeball it" — which is exactly what doesn’t scale for agent workflows.
Proposed solution
A declarative motion-assertion spec, evaluated against the same seeked timeline the renderer uses (not a simulation). The engine already seeks; this samples a handful of selectors on a fine time grid, builds an
element × timematrix of{ rect, opacity, visible }, and evaluates constraints, emitting findings in the existing audit shape (code, severity, time, selector, message, fixHint).Proposed assertion kinds (all framework-agnostic, no app coupling):
appearsBy(selector, t)t— catches reveals the seek skipsbefore(a, b)afirst appears strictly beforeb— catches broken stagger orderstaysInFrame(selector)keepsMoving([withinSelector])Sketch:
{ "duration": 6, "assertions": [ { "kind": "appearsBy", "selector": "#headline", "bySec": 0.5 }, { "kind": "before", "a": "#headline", "b": "#cta" }, { "kind": "staysInFrame", "selector": ".card" }, { "kind": "keepsMoving" } ] }Two delivery options — would value maintainer steer:
hyperframes verify <spec>(spec file alongside the composition), orinspectto read an optional assertions file and fold motion findings into its output.This builds directly on the layout-audit work in #1435 / #1436 (same seek, same finding shape, same JSON envelope) and shares the engine’s existing seek path — no extra Chrome launch or render.
Alternatives considered
render --docker— fixes font/Chrome nondeterminism only; says nothing about whether a reveal fired or an element drifted off-frame.Additional context
We run a version of this in production (assertions evaluated against the seeked GSAP timeline via the engine) and it reliably catches the seek-only failures above before they reach a render. Happy to contribute the implementation as a PR (or a stack) if there’s appetite and once the surface is agreed.
Out of scope for a first cut: beat/music-relative timing (
onBeat) — that’s app-specific and can live downstream.Open questions for scoping:
verifycommand vs. aninspectextension?data-*attributes on elements, or both?errorby default, and should--strictgate CI?(Author of #1435 / #1436, which this extends.)