Skip to content

RFC: declarative motion verification — assert entrance/order/in-frame/liveness against the seeked timeline #1437

@LeopoldTr

Description

@LeopoldTr

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:

  1. A new verb hyperframes verify <spec> (spec file alongside the composition), or
  2. 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:

  1. New verify command vs. an inspect extension?
  2. Spec format/location — sidecar JSON, data-* attributes on elements, or both?
  3. Severity model — should a failed assertion be error by default, and should --strict gate CI?

(Author of #1435 / #1436, which this extends.)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions