Skip to content

fix(lint): remove root_composition_missing_data_duration#490

Merged
miguel-heygen merged 2 commits intoheygen-com:mainfrom
MuTsunTsai:fix/lint-narrow-root-data-duration
Apr 25, 2026
Merged

fix(lint): remove root_composition_missing_data_duration#490
miguel-heygen merged 2 commits intoheygen-com:mainfrom
MuTsunTsai:fix/lint-narrow-root-data-duration

Conversation

@MuTsunTsai
Copy link
Copy Markdown
Contributor

@MuTsunTsai MuTsunTsai commented Apr 25, 2026

What

Remove the root_composition_missing_data_duration lint rule. The looping shapes it was a proxy for are already covered by gsap_infinite_repeat and gsap_repeat_ceil_overshoot.

Why

Working through the runtime in #490, we landed on this precise model of collectRuntimeTimelinePayload:

const shouldEmitNonDeterministicInf =
  timelineLooksLoopInflated && attrDurationCandidate == null;

const timelineLooksLoopInflated =
  timelineDurationCandidate != null &&
  finiteWindowFloor != null &&
  timelineDurationCandidate > finiteWindowFloor + 1;

So Infinity is only emitted when all of:

  1. A finite GSAP timeline duration (timelineDurationCandidate present)
  2. A finiteWindowFloor from media/sub-comp windows
  3. timeline > floor + 1
  4. No root data-duration

A static linter cannot observe (1) or (3) — those need the real GSAP timeline. The signals it can observe — repeat: -1, large repeat: N, Math.ceil(d/c) overshoots — are exactly what gsap_infinite_repeat and gsap_repeat_ceil_overshoot already flag (also from #243). Once those rules have fired, root_composition_missing_data_duration adds nothing precise; it's a noisy duplicate of the same finding.

The original PR's narrowing predicate had it backwards (silenced media-floor cases that do still emit Infinity, and warned on div/span clip cases where loop-inflation can't even be detected). Trying to tighten it further still leaves an imprecise static approximation.

How

Delete the rule from packages/core/src/lint/rules/composition.ts. No type changes required — finding codes are untyped string literals.

Test plan

  • bunx oxlint packages/core/src/lint/rules/composition.ts packages/core/src/lint/rules/composition.test.ts — clean
  • bunx oxfmt --check ... — clean
  • bun run --filter @hyperframes/core typecheck — clean
  • bunx vitest run src/lint/rules/composition.test.ts (in packages/core) — 23 tests pass (22 baseline + 2 new pinning the removal)
  • New regression tests under describe("root_composition_missing_data_duration (removed)", ...):
    • does not warn on a docs-compliant root with no data-duration — the documented authoring shape (root with media + GSAP timeline, no data-duration)
    • does not warn even on the original Infinity-risk shaperepeat: -1 driving a div clip with no media; the deprecated rule must not fire, but gsap_infinite_repeat still surfaces the looping mistake (the actionable signal)

Follow-up (out of scope)

If perfect precision on durationInFrames = Infinity is wanted, the right place is the runtime/render path where shouldEmitNonDeterministicInf is actually known. That would be a separate change against the runtime/producer side, not the static linter. Happy to take that on in a separate PR if there's appetite.

@miguel-heygen miguel-heygen self-requested a review April 25, 2026 14:35
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

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

This PR is aimed at a real false-positive class, so I would not close it. But the new predicate does not match the runtime's actual Infinity branch yet: it suppresses the warning for the media-floor case that can still emit Infinity, and keeps warning for ordinary timed element clips that runtime resolves to a finite duration.

// which the runtime resolves to a finite value.
const hasSubComp = tags.some((t) => readAttr(t.raw, "data-composition-src") !== null);

if (hasMediaClip || hasSubComp) return findings;
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.

This early return is too broad for the runtime condition the rule is trying to mirror. In collectRuntimeTimelinePayload, a root with no authored data-duration, a finite media window, and a loop-inflated root timeline still returns durationInFrames = Infinity (timelineLooksLoopInflated && attrDurationCandidate == null). I verified that shape with <video data-start data-duration="10"> plus a 100s root timeline: this lint rule is silent, but runtime emits Infinity. The inverse also happens for ordinary timed element clips: the PR still warns, but runtime includes those clips in maxEnd and returns a finite duration. The predicate should be realigned with the runtime before this warning is narrowed.

@MuTsunTsai
Copy link
Copy Markdown
Contributor Author

You're right on both counts — I had the runtime model wrong. Re-reading collectRuntimeTimelinePayload: shouldEmitNonDeterministicInf = timelineLooksLoopInflated && attrDurationCandidate == null, and timelineLooksLoopInflated itself requires timelineDurationCandidate > finiteWindowFloor + 1. So the actual Infinity condition needs all of:

  1. A finite GSAP timeline duration (timelineDurationCandidate present)
  2. A finiteWindowFloor from media/sub-comp windows
  3. timeline > floor + 1
  4. No root data-duration

My PR's predicate (silence when there's any media or sub-comp) gets exactly those cases backwards: it silences (2)+(4) which is an Infinity-emitting shape, and warns when there's only div/span clips, where (2) is null so loop-inflation can't even be detected and runtime falls through to the safe path.

Now to the awkward bit — I think this surfaces a real policy question, not just a "tighten the predicate" fix.

The runtime's true condition turns on (1) and (3), and lint can't statically observe either. The static signals available are the looping shapes from the script body — repeat: -1, large finite repeat: N, Math.ceil(d/c) overshoots — and those are exactly what gsap_infinite_repeat and gsap_repeat_ceil_overshoot already cover (also from #243). Once those two rules have fired, what is root_composition_missing_data_duration adding?

I see three honest answers:

A. Keep the rule, narrow it to "looping shape detected in scripts AND no data-duration." This is the closest static proxy for the runtime condition. The downside is it overlaps significantly with the two GSAP rules — most projects that trigger this would already see one of those, and the duplicate noise is probably worse than the marginal coverage.

B. Deprecate this rule and let gsap_infinite_repeat / gsap_repeat_ceil_overshoot carry the load. Those rules already say "use a finite count derived from total duration" in their fixHints, which implicitly tells the author to author duration. The Infinity emission is a runtime symptom of the looping shape, not an independent authoring mistake — so flagging it twice is redundant.

C. Move duration verification out of static lint entirely and report it from the runtime/render path when shouldEmitNonDeterministicInf is about to fire. That has perfect precision (it sees the actual timeline) at the cost of no longer being a pre-render check.

I lean B — it's the smallest surface area that still catches the original #243 motivation. Happy to implement whichever direction you want, including a v2 of this PR doing A. The current PR's predicate is wrong and I'll either revise or close it; just want to align on the policy first since the rule's value isn't obvious once the runtime model is precise.

@miguel-heygen
Copy link
Copy Markdown
Collaborator

Yes, I agree with your revised model.

I would choose B for the static lint surface. Once the runtime condition is written out precisely, root_composition_missing_data_duration is not really an independent authoring invariant; it is a noisy proxy for loop-inflated GSAP duration. Trying to do A still leaves us with an imprecise static approximation, and it duplicates the more actionable GSAP rules that already point at the real authored mistake.

So my preferred direction is:

  1. Do not keep this current predicate.
  2. Either close this PR, or pivot it to remove/deprecate root_composition_missing_data_duration from static lint.
  3. Let gsap_infinite_repeat and gsap_repeat_ceil_overshoot carry the pre-render lint coverage for the original fix(player): handle Infinity duration; add lint rules for data-duration and Math.ceil overshoot #243 class.
  4. If we want perfect precision for durationInFrames = Infinity, add that separately in the runtime/render/validate path where shouldEmitNonDeterministicInf is actually known.

If you want to keep this PR open, I would make it the B version: remove the static rule and update tests around the docs-compliant “composition without root data-duration” shape. I would not merge a narrowed media/sub-comp predicate.

Lint cannot statically observe the runtime's true Infinity-emission
condition: it requires a finite GSAP timeline duration AND a finite
media/sub-comp window AND timeline > floor + 1, none of which are
visible to the static linter. The looping shapes that drive the
condition are already covered by `gsap_infinite_repeat` and
`gsap_repeat_ceil_overshoot` (both from heygen-com#243), which point at the
real authoring mistake — flagging the missing duration separately
was a noisy proxy for the same signal.

Per heygen-com#490 review discussion, deprecate the static rule and let those
two GSAP rules carry the pre-render coverage. If perfect precision
on `durationInFrames = Infinity` is needed, that belongs on the
runtime/render path where `shouldEmitNonDeterministicInf` is
actually known.

Add regression tests pinning the removal: a docs-compliant root
without `data-duration` no longer warns, and the canonical
loop-inflated shape now surfaces only via `gsap_infinite_repeat`
instead of two duplicate findings.
@MuTsunTsai MuTsunTsai force-pushed the fix/lint-narrow-root-data-duration branch from fb83f9c to ddbf0ad Compare April 25, 2026 15:38
@MuTsunTsai MuTsunTsai changed the title fix(lint): narrow root_composition_missing_data_duration to the unsafe shape fix(lint): remove root_composition_missing_data_duration Apr 25, 2026
@MuTsunTsai
Copy link
Copy Markdown
Contributor Author

Pivoted to B. Force-pushed: the narrowing rule and tests are gone, replaced with two regression tests pinning the removal — one for the docs-compliant shape, one for the original Infinity-risk shape (where gsap_infinite_repeat now carries the signal). PR title/description updated to match. The branch name (fix/lint-narrow-...) is a leftover from the original direction; happy to rename if preferred, otherwise treating it as a contributor-side artefact.

Drops the `root_composition_missing_data_duration` reference now that
the rule is gone. Keeps the authoring recommendation (and explains the
runtime Infinity case) but points authors at the GSAP rules that
actually carry the lint signal: `gsap_infinite_repeat` and
`gsap_repeat_ceil_overshoot`.
@MuTsunTsai
Copy link
Copy Markdown
Contributor Author

Spotted one more reference while sweeping the repo: skills/website-to-hyperframes/references/step-6-build.md line 117 cites the removed rule by name as part of an authoring checklist. Pushed a follow-up commit (cb040ff) to update the bullet — keeps the data-duration recommendation for repeating-animation cases but points authors at the GSAP rules that actually carry the signal now.

Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

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

This addresses my requested change. The PR now follows the B direction from the discussion: the static root_composition_missing_data_duration rule is removed instead of narrowed, and the new tests pin both the docs-compliant no-root-duration case and the original loop-risk shape where gsap_infinite_repeat carries the signal. I re-ran the focused composition/GSAP lint tests plus touched-file lint/format checks on head ddbf0ad.

@miguel-heygen
Copy link
Copy Markdown
Collaborator

Checked the follow-up commit cb040ff too. The delta from the code-reviewed head is only the skills/website-to-hyperframes/references/step-6-build.md checklist wording: it removes the stale root_composition_missing_data_duration reference, keeps data-start as the linted root requirement, and points repeating-GSAP cases at gsap_infinite_repeat / gsap_repeat_ceil_overshoot. That matches the B direction, so the approval still stands on the current head.

@miguel-heygen miguel-heygen merged commit 9b72a87 into heygen-com:main Apr 25, 2026
31 checks passed
@MuTsunTsai MuTsunTsai deleted the fix/lint-narrow-root-data-duration branch April 25, 2026 21:07
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.

2 participants