Skip to content

fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional#1830

Merged
jrusso1020 merged 5 commits into
mainfrom
fix/auto-infer-composition-duration
Jul 1, 2026
Merged

fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional#1830
jrusso1020 merged 5 commits into
mainfrom
fix/auto-infer-composition-duration

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes the initial code #2 render-failure bucket, "Composition has zero duration" — ~27K errors / ~7K affected users over 30 days (PostHog project 356858, dashboard 1783183 "HyperFrames — Bottom-Line & Activation"). Root cause: only GSAP timelines got their duration auto-detected at render time; CSS, WAAPI, and Lottie compositions had no source of truth for total duration unless the author remembered data-duration on the root element, and capture hard-failed when neither was present.
  • Adds getInferredDurationSeconds() to the CSS, WAAPI, and Lottie runtime adapters (packages/core/src/runtime/adapters/{css,waapi,lottie}.ts). Each reports the longest finite end time it can discover from its own animations:
    • CSS: per-element getAnimations()[].effect.getComputedTiming().endTime, offset by that element's data-start.
    • WAAPI: document.getAnimations()[].effect.getComputedTiming().endTime, offset by the animation's composition-time baseline.
    • Lottie: the registered animation's native length (totalFrames / frameRate for lottie-web, or the player's own duration for dotLottie) — finite regardless of loop.
    • Infinite/unbounded animations (animation-iteration-count: infinite, WAAPI infinite iterations) correctly return null — they genuinely have no derivable duration and still require data-duration.
    • Three.js is unchanged — the adapter only forwards time via hf-seek; there's no AnimationClip/AnimationMixer inspection to infer from, so data-duration remains required there.
  • Wires the new adapter signal into the runtime's existing duration-floor machinery (resolveAdapterDurationFloorSeconds in packages/core/src/runtime/init.ts), alongside the pre-existing media-duration and authored-composition-duration floors. window.__hf.duration becomes positive without any author action for finite-duration non-GSAP compositions.
  • Tightens frameCapture.ts's zero-duration fast-fail gate to also check hf.duration directly (not just "no GSAP timeline + no data-duration"), so a composition whose duration inference hasn't landed yet isn't fast-failed prematurely.
  • Adds a new lint rule, root_composition_missing_duration_source, that errors when the total render length isn't reliably determinable without an explicit data-duration: no animation signal at all for any adapter to discover, Three.js without data-duration, or any infinite/unbounded CSS animation without data-duration — including when a finite animation is present alongside it. The infinite case is deliberately stricter than the runtime's own inference: the runtime will fall back to a finite sibling's length if one exists, but an unbounded animation makes the intended total length ambiguous (a decorative infinite spinner next to a 3s fade doesn't declare the clip is meant to be 3s), so the rule requires the author to state intent. It is deliberately silent on purely finite CSS/WAAPI/Lottie animations, since the runtime infers those unambiguously. Note the rule is advisory by default (see shouldBlockRender) — it only blocks render under --strict/--strict-all, so strictness here nudges toward an explicit, guaranteed-correct value without failing renders that would otherwise succeed.
  • Updates the CSS/WAAPI/Lottie/Three adapter skill docs (skills/hyperframes-animation/adapters/*.md) and the hyperframes-core determinism-rules/data-attributes references to document the new optionality and point at the runtime mechanism backing it.

On the autofix

The task sketch asked for an autofix that inserts the inferred data-duration value. I considered and rejected this: no case the rule flags has an unambiguous value to insert — a duration-less Three.js scene has nothing to measure, "no animation at all" has nothing to infer from, and an infinite animation makes the intended total length ambiguous even when a finite sibling gives the runtime a length to fall back on (that fallback is not necessarily the author's intended clip length). An autofix here would have to commit to a number the author never declared, trading a loud, correct prompt-for-intent for a silent, possibly-wrong-length render — which directly undercuts the goal of making duration-less compositions structurally hard to ship. A hard lint error with a clear fixHint is the correct structural fix; the unambiguously-inferable cases (purely finite CSS/WAAPI/Lottie) need no fix at all, because the runtime handles them.

Verification

  • Full unit suite (bun run build && bun run test) passes: 2118 core tests, 937 CLI tests, 783 engine tests, 1176 studio tests, 310 player tests, 10 shader-transitions tests — all green. (The producer package's regression harness has a pre-existing, environment-local fixture issue — a 404 on a remote-media test asset — reproduced identically on main with this branch's changes stashed; confirmed unrelated via a controlled single-suite run before/after.)
  • End-to-end against the real render pipeline, not just mocked unit tests: built the CLI locally and rendered a CSS-only composition (one element, animation: fadeSlide 3s, no GSAP timeline, no data-duration anywhere) through hyperframes render. Previously this would fail with "Composition has zero duration"; it now renders a correct 3.000s MP4 (verified via ffprobe).
  • Negative control: the same composition with animation-iteration-count: infinite instead still fails fast with the expected diagnostic, and npx hyperframes lint flags it with root_composition_missing_duration_source before render — confirming lint and runtime agree.
  • New tests: adapter-level unit tests for getInferredDurationSeconds on CSS/WAAPI/Lottie (finite, multi-element max, infinite/unbounded, mid-composition discovery), runtime integration tests in packages/core/src/runtime/init.test.ts (CSS/Lottie inference end-to-end via initSandboxRuntimeModular, infinite-CSS still requiring data-duration, and an explicit GSAP-no-regression test confirming a GSAP timeline's duration is unaffected by the new adapter floor even with a shorter incidental CSS animation present), and lint rule tests covering all branches plus a false-positive guard (non-composition root files, e.g. slideshow demo.html, are correctly ignored — verified against all 112 real .html files under registry/blocks and registry/examples, zero false positives).

Test plan

  • bun run build — all packages build clean
  • bun run test — core/cli/engine/studio/player/shader-transitions all green
  • bunx oxlint / bunx oxfmt --check on all changed files
  • tsc --noEmit on core and engine
  • Real-Chrome render verification (finite CSS, no data-duration → correct-duration MP4; infinite CSS, no data-duration → expected fast-fail)
  • npx hyperframes lint agreement check on the same two fixtures

🤖 Generated with Claude Code

@jrusso1020 jrusso1020 marked this pull request as ready for review July 1, 2026 16:31

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional

Summary: Solid, well-scoped fix adding getInferredDurationSeconds() to CSS/WAAPI/Lottie adapters so data-duration becomes optional for finite non-GSAP compositions. Good architecture, strong test coverage, thorough documentation. One correctness gap to address.

Findings:

# Location Severity Note
1 css.tsgetInferredDurationSeconds concern Returns null when any animation is unbounded (sawUnbounded = true), even when finite animations coexist. But the lint rule says "you're fine, no data-duration needed" for compositions with animation-name present. A composition with both fadeIn 3s ease forwards and spin 1s linear infinite passes lint but runtime can't infer duration → zero-duration failure. Fix: return the max finite end-time even when unbounded animations coexist (~3-line change).
2 waapi.tsgetInferredDurationSeconds concern Same issue as #1 — returns null when sawUnbounded even if finite animations exist. Same fix applies.
3 init.tsresolveRootTimelineFromDocument suggestion Computes durationFloorSeconds from only mediaFloor and authoredCompositionFloor, NOT including adapterFloor. getSafeTimelineDurationSeconds does include it. Not a bug today (adapter floor flows through Math.max downstream), but a consistency gap worth a comment.
4 composition.ts:~268 nit WAAPI detection regex [[$A-Za-z_] — unescaped $ works by luck (literal in character class), [ inside the class is intentional per comment. Correct but not obvious.
5 frameCapture.ts:~1039-1083 nit Updated hint text fires in the !diag.hasTimeline branch — correct by construction (only called when duration <= 0), but fragile if called elsewhere for diagnostic logging.
6 Missing test suggestion No test for mixed finite + infinite CSS animations on the same composition. Directly tied to finding #1.

What looks good:

  • getInferredDurationSeconds addition to RuntimeDeterministicAdapter is well-typed (optional, number | null), non-breaking
  • Test coverage is strong: CSS (null/finite/offset/multi-element/infinite/disconnected), WAAPI (null/max/infinite/baseline/missing-API), Lottie (null/lottie-web/dotlottie/fallback/multi/not-loaded), runtime integration (CSS e2e, infinite still requires data-duration, Lottie e2e, GSAP no-regression)
  • Lint rule correctly excludes sub-compositions and handles GSAP, CSS, WAAPI, Lottie with separate branches
  • Doc updates are thorough and consistent across all four adapter skill docs
  • data-duration correctly marked "Conditional" in data-attributes.md
  • Autofix rejection is the right call

Ponytail: inferAnimationEndSeconds helpers in css.ts and waapi.ts are near-identical but signatures differ just enough (CSS takes startSeconds, WAAPI computes baseline internally) that extraction would add indirection without real dedup. Leave it. Lean already, one real concern to address.

Verdict: LGTM with concern — The mixed finite+infinite animation gap (findings #1/#2) is where lint and runtime disagree. Not a regression (same failure as before this PR), but it's a gap in the new surface. Recommend fixing before merge with a ~3-line change in each adapter to return max finite end-time even when unbounded animations coexist. Everything else is solid.

— Miga

jrusso1020 and others added 4 commits July 1, 2026 10:52
… data-duration is optional

The #2 render failure bucket ("Composition has zero duration") accounts for
~27K errors / ~7K affected users over 30 days (PostHog project 356858). Root
cause: only GSAP timelines got their duration auto-detected — CSS, WAAPI, and
Lottie compositions had no source of truth for total duration unless the
author remembered to set data-duration on the root element, and the render
engine hard-failed capture when neither was present.

Adds getInferredDurationSeconds() to the CSS, WAAPI, and Lottie runtime
adapters (packages/core/src/runtime/adapters/*.ts) — each reports the longest
finite end time it can discover from its own animations (CSS: computed
timing offset by data-start; WAAPI: effect.getComputedTiming().endTime;
Lottie: totalFrames/frameRate or the player's own duration). Infinite/
unbounded animations correctly return null and still require data-duration.
Wires this into the runtime's existing duration-floor resolution
(resolveAdapterDurationFloorSeconds in runtime/init.ts), alongside the
existing media-duration and authored-composition floors, so
window.__hf.duration becomes positive without any author action for
finite-duration non-GSAP compositions. Three.js is unchanged — no
AnimationClip/AnimationMixer inspection exists in that adapter, so
data-duration remains required there.

Tightens frameCapture.ts's zero-duration fast-fail gate to also check
hf.duration directly (not just the two authored signals), so a composition
mid-inference isn't fast-failed before its adapter-derived duration lands.

Adds a new lint rule (root_composition_missing_duration_source) that errors
only on genuinely non-inferable cases: no animation signal at all, Three.js
without data-duration, or an infinite/unbounded CSS or WAAPI animation
without data-duration. Deliberately silent on finite CSS/WAAPI/Lottie
animations, since the runtime now infers those — an autofix that "inserts
the inferred value" was considered and rejected: every case the rule flags
has no derivable value (an infinite spinner has no finite end time; a
duration-less Three.js scene has nothing to measure), so any autofix would
have to fabricate a placeholder, trading a loud correct failure for a silent
wrong-length render.

Updates the CSS/WAAPI/Lottie/Three adapter skill docs and the
hyperframes-core determinism-rules/data-attributes references to document
the new optionality and the runtime mechanism backing it.

Verified end-to-end against the real render pipeline (not just unit tests):
a CSS-only composition with a finite 3s animation, no GSAP timeline, and no
data-duration now renders a correct 3.000s MP4 via `hyperframes render`
(previously: "Composition has zero duration" failure). The infinite-CSS
negative control still fails fast with a clear diagnostic, matching the new
lint rule.

Adds a file-level fallow health exemption for lottie.ts's pre-existing
`seek` handler — unrelated to this change, but its line numbers shifted when
new functions were added earlier in the file, tripping fallow's
inherited-finding fingerprint (documented pattern already used elsewhere in
.fallowrc.jsonc for the same reason).

Known limitation: the static WAAPI usage detector in the lint rule
(/\.animate\(\s*[\[$A-Za-z_]/) can miss unusual call shapes; it only affects
whether the "no signal at all" branch fires, and errs toward NOT flagging
(reducing false positives) rather than over-flagging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ion_source

- Strip JS/CSS comments before scanning for GSAP/WAAPI/Three/Lottie/CSS
  animation signals, so a commented-out `.animate()` call or a commented
  `animation: ... infinite` rule can no longer satisfy the "has a duration
  source" check and mask a real zero-duration render failure.
- Broaden the WAAPI detection regex to also match the object-literal
  (PropertyIndexedKeyframes) form of `.animate()`, e.g.
  `el.animate({ opacity: [0,1] }, { duration: 2000 })`, which the previous
  character class silently missed. Corrected the adjacent comment that
  incorrectly claimed this shape "can't be a false negative".
- Fix hasInfiniteCssAnimation to stop false-positiving on animation NAMEs
  that merely contain the substring "infinite" (e.g. `infinite-spin`) by
  anchoring the `infinite` keyword with hyphen-aware boundaries instead of
  a bare `\b`. Also makes the longhand `animation-name` + separately
  declared `animation-iteration-count: infinite` pattern detected
  consistently.

Adds targeted unit tests for each fixed false-positive/false-negative.
… coexists

getInferredDurationSeconds in the CSS and WAAPI adapters returned null
outright whenever any animation on the composition was unbounded
(infinite iteration count), even when other finite animations on the
same composition could still supply a valid duration. This disagreed
with the new root_composition_missing_duration_source lint rule, which
treats any animation-name as sufficient — so a composition mixing a
finite fadeIn with a decorative infinite spin passed lint but still
failed at render with "zero duration".

Unbounded animations are now skipped when computing the max end time
instead of short-circuiting the whole calculation. null is only
returned when every animation on the composition is unbounded, i.e.
there is no finite signal to fall back on at all.

Co-Authored-By: Claude <noreply@anthropic.com>
oxfmt flagged the merged Composition Root table from the post-rebase
merge of the auto-infer-duration docs onto main's reformatted table —
the separator row was one dash short of the header width.
@jrusso1020 jrusso1020 force-pushed the fix/auto-infer-composition-duration branch from b57d0f8 to f9a45ef Compare July 1, 2026 18:04

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-rebase review: fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie

(James asked for a second look after the rebase; picking up cold, with Miga's earlier LGTM-with-concern as the anchor. Miguel has already approved at 17:17Z; Miga is the outstanding "concern" holder.)

Verdict: LGTM

Miga's core finding — the mixed finite + infinite gap in the CSS and WAAPI adapters — has been addressed exactly as prescribed. The rebased diff also picked up two nice cleanups on the lint rule (comment-stripping, WAAPI object-literal regex, hyphen-anchored infinite matching). The remaining Miga findings are either resolved or standing as accepted suggestion-level.

Post-rebase delta (since Miga's review at 16:49Z)

Four commits landed after Miga's pass:

SHA Nature Addresses
6e53dfa9 lint hardening: comment-stripping, WAAPI object-literal regex, hyphen-anchored infinite boundary, animation-name + separate iteration-count combined form Finding #4 (regex clarity), plus 3 false-positive/false-negative gaps the rebase surfaced
1ff8012b the fix for Miga's #1/#2 — skip unbounded, keep max finite Findings #1, #2, #6 (with tests)
f9a45efa pure docs table-separator fix trivial

Verifying against HEAD (f9a45efa):

  • packages/core/src/runtime/adapters/css.ts:121-136getInferredDurationSeconds() now iterates all animations, calls inferAnimationEndSeconds per animation, and only skips ones where endSeconds == null (the null return is where unbounded/NaN/Infinity now lands). Returns maxEndSeconds > 0 ? maxEndSeconds : null. Correct shape: a finite fadeIn 3s coexisting with an infinite spin now yields 3, not null.
  • packages/core/src/runtime/adapters/waapi.ts:217-228 — same shape, iterates snapshotAnimations(), skips null-end animations, returns max-finite-or-null.
  • packages/core/src/runtime/adapters/css.test.ts:273 — dedicated "finite + unbounded coexist" test (the exact shape Miga described). waapi.test.ts:301 — mirror test with finite (2s) + infinite animations, expects 2.
  • waapi.test.ts:322 — negative control: all-unbounded → returns null. So the "no finite signal anywhere" fall-through is guarded by test.

Miga's finding #6 ("no test for mixed finite + infinite on same composition") is directly covered — the test files were updated in the same commit as the fix.

Standing concerns

1. Finding #3 (Miga) still applies — init.ts:761-763 resolveRootTimelineFromDocument still computes its local durationFloorSeconds from only mediaFloor + authoredCompositionFloor, omitting adapterFloor. The value feeds into resolveMinCandidateDurationSeconds(...), which drives candidate-timeline filtering. getSafeTimelineDurationSeconds at line 702 does include the adapter floor (line 709), so the runtime's final safe duration is correct. But if a composition is in the "adapter-inferred duration = 3s, no other signal" state, minCandidateDurationSeconds collapses to the tiny MIN_VALID_TIMELINE_DURATION_SECONDS constant during candidate filtering. Not a bug today — no path was demonstrated where filtering with the wrong minimum produces user-visible wrong behavior — but the two floor-computation sites now disagree, and one of them is silently the wrong shape. Consistent extraction to a single helper (or at least a comment at the resolve-root site explaining why adapter floor is omitted) would close this. Severity: consider — acceptable to land as-is, but worth a follow-up.

2. Lint/runtime disagreement now inverts on mixed pure-CSS. With the runtime fix, a composition mixing .fade { animation: fadeIn 3s; } with .spin { animation: spin 1s infinite; } is now inferable at runtime (yields 3s). But the lint rule at packages/lint/src/rules/composition.ts:833 fires root_composition_missing_duration_source on any hasInfiniteCssAnimation when no lottie/waapi is present — including this mixed case. The regex on line 790-791 catches spin 1s infinite, and the finite fadeIn 3s sibling doesn't disqualify. So the exact case Miga flagged as "passes lint but runtime can't infer" is now inverted: the runtime can infer, but lint still errors.

The PR description claims the rule errors "only on genuinely non-inferable cases." Mixed CSS is inferable post-#1/#2 fix, so the description overstates the guarantee. Two ways to reconcile:

  • (a) Loosen the lint: if hasCssAnimationName && !onlyInfiniteCss (i.e. some finite CSS exists alongside the infinite one), suppress the error — matches the runtime behavior.
  • (b) Keep the lint strict and adjust the description to say "requires data-duration when any CSS animation on the composition is infinite, even if finite siblings exist, because the intent is ambiguous."

Personally I lean (b) — a decorative infinite spinner alongside a finite fade is ambiguous about total render length in a way that "silently take the finite one" doesn't help. But it's a design call for the author. Severity: consider — either resolution is defensible; the current state is only slightly out of step with the PR narrative, not with correctness.

No lint test covers this mixed-CSS case either way — composition.test.ts has "all-finite passes" (1144), "all-infinite errors" (1193), longhand form (1326), and false-positive guards, but no mixed. Whichever direction you resolve #2 in, add the test.

Envelope observation (heads-up, not blocking)

Per Miguel's own convention on hyperframes PRs, the PR body still contains the 🤖 Generated with [Claude Code] trailer and multiple commits carry Co-Authored-By: Claude ... <noreply@anthropic.com> (3eab9af2, 1ff8012b). Historically this warrants request-changes, but noting that Miguel — the one who enforces this — has already approved this PR at 17:17Z, so the envelope is presumably fine here. Flagging only for completeness in case Miguel scans this review.

What looks good

  • The fix commit 1ff8012b is exactly the ~3-line-per-adapter shape Miga described. Well-scoped, easy to verify.
  • The lint hardening in 6e53dfa9 is a genuine improvement, not a band-aid — the hyphen-anchored infinite boundary is the right regex fix for the infinite-spin class-name false positive, and the object-literal WAAPI regex broadening covers a shape the original rule silently missed.
  • Test coverage for the fix is targeted and complete on the adapter side (finite+unbounded coexist, all-unbounded null).
  • getInferredDurationSeconds returning null uniformly on endSeconds == null (rather than a boolean flag + early return) is a cleaner shape than what Miga speculated on. No sawUnbounded flag anywhere in the final code.

Review by Via

…e honest

Post-review (Vance): after the finite+infinite adapter fix, the runtime infers
a length for a mixed finite+infinite CSS composition, but this lint rule still
(intentionally) errors on it — an unbounded animation makes the intended total
length ambiguous, so we require explicit data-duration. Keep that strictness
(lint is advisory by default; it only blocks under --strict, and data-duration
is the one duration signal guaranteed correct across every adapter, known and
future). But the message wrongly claimed the render "will fail" — false for the
mixed case, where the runtime falls back to the finite animation. Rewrite it to
describe the ambiguity honestly, correct the rule's block comment, and add a
mixed finite+infinite test asserting it still errors with an honest message.

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 — LGTM (head 72909a2c)

Returning to the case at James's request, magnifying glass in hand, on the singular commit added since R1 (f9a45efa72909a2c, +28/-0 test, +30/-13 rule).

The delta is surgically confined to packages/lint/src/rules/composition.ts and its sibling test file — no runtime code moved, no adapter re-shaped, no scope creep. A single question posed: does the new commit dispose of R1's two consider items? Let us examine each in turn.

Consider #2 (lint/runtime disagreement) — resolved, and resolved by the more honest of the two roads

R1 flagged that composition.ts:833 continued to claim the render will fail on any infinite CSS, when the finite+infinite adapter fix in 1ff8012b had already taught the runtime to fall back to the finite sibling. Two doors were open: loosen the lint (option B), or keep the lint strict and correct the message (option A). The author has chosen A — deliberately, and with documented reasoning.

  • composition.ts:838-861 — the block comment and error message are both rewritten. The rule now openly declares itself "intentionally stricter than the runtime's own inference," and the message no longer asserts a hard failure. It describes both outcomes accurately: with a finite sibling, the runtime infers that length; with no finite source, the render fails. Author intent is called out as the reason we still require data-duration — a decorative infinite spinner alongside a 3s fade does not declare the clip is meant to be 3s.
  • composition.test.ts:1209-1236 — the new test locks the honest wording in place: expect(finding?.message).toContain("ambiguous") AND expect(finding?.message).not.toContain("will fail"). Symmetric assertion, not merely additive. The false claim cannot regress silently.
  • Consistent with the PR description, which already declared the stricter-than-runtime position under the "Adds a new lint rule" bullet. The prose and the code now agree.

The stance is defensible: data-duration is the one duration signal guaranteed correct across every adapter, known and future; the rule is advisory by default (blocks only under --strict/--strict-all); the strictness nudges rather than fails. No band-aid, no contradictory rule, no silent scope gap.

Consider #1 (init.ts:761-763adapterFloor omitted from resolveRootTimelineFromDocument's local durationFloorSeconds) — untouched

Read the file at head SHA:

const durationFloorSeconds =
  Math.max(mediaDurationFloorSeconds ?? 0, authoredCompositionDurationFloorSeconds ?? 0) ||
  null;

Still no adapterFloor term. The two floor-computation sites (resolveRootTimelineFromDocument's local computation vs. the primary path in initSandboxRuntimeModular) still disagree on whether adapter-inferred duration contributes to the floor. This was consider-level at R1 and remains consider-level at R2 — it is not a merge-blocker, but it is a real discrepancy worth a follow-up cleanup PR.

Miga's finite+infinite fix (1ff8012b)

Not touched by this delta — verified. The adapter still returns the max finite end time and skips unbounded entries instead of short-circuiting. Intact.

CI

All required checks green at time of review. Regression shards and Windows tests pending in the usual pattern; not blocking.

Envelope

The new commit 72909a2c itself is clean — no Co-Authored-By: Claude trailer. Pre-existing envelope from earlier commits (3eab9af2 Co-Authored-By: Claude Sonnet 5, 1ff8012b Co-Authored-By: Claude) and the 🤖 Generated with [Claude Code] in the PR body remain from the R1 SHA. Miguel approved with the envelope in place at R1, so heads-up only from me.

Verdict

LGTM. Ship it.

R2 by Via

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 at 72909a2c: LGTM.

Audited: packages/core/src/runtime/adapters/{css,waapi,lottie}.ts, their adapter tests, packages/core/src/runtime/init.ts, and packages/lint/src/rules/composition.{ts,test.ts}.
Trusting: skill-doc/manifest updates and the full render/regression matrix beyond the GitHub check state.

What I verified:

  • packages/core/src/runtime/adapters/css.ts:121 and packages/core/src/runtime/adapters/waapi.ts:217 now skip unbounded animations while preserving the max finite end-time, so Miga's mixed finite+infinite gap is closed. The paired regression tests in css.test.ts and waapi.test.ts pin both mixed and all-unbounded behavior.
  • packages/lint/src/rules/composition.ts:840 keeps the infinite-CSS rule strict by product intent, but the message now describes ambiguity instead of claiming every mixed case fails at render. packages/lint/src/rules/composition.test.ts:1210 locks that wording and the mixed finite+infinite case.
  • The runtime duration floor integration is still through the existing safe-duration path, so finite CSS/WAAPI/Lottie compositions can produce a positive hf.duration without data-duration.

Non-blocking follow-up: Via's resolveRootTimelineFromDocument floor note still stands. packages/core/src/runtime/init.ts:761 computes the local candidate floor without the adapter floor while getSafeTimelineDurationSeconds includes it, so a helper/comment cleanup would reduce future drift. I don't see a current user-visible failure from it.

No failing checks at review time; remaining pending checks are the long regression/Windows runs.

Verdict: APPROVE
Reasoning: The prior correctness concern is fixed at both adapter boundaries and covered by focused regressions; the lint/runtime policy is now explicit and tested, with only follow-up-level cleanup remaining.

— Magi

@jrusso1020 jrusso1020 merged commit 24edb15 into main Jul 1, 2026
50 checks passed
@jrusso1020 jrusso1020 deleted the fix/auto-infer-composition-duration branch July 1, 2026 21:28
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.

4 participants