fix(lint): remove root_composition_missing_data_duration (#490)

* fix(lint): remove root_composition_missing_data_duration

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 #243), which point at the
real authoring mistake — flagging the missing duration separately
was a noisy proxy for the same signal.

Per #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.

* docs(skills): update step-6 build checklist after rule removal

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`.
This commit is contained in:
Mu-Tsun Tsai
2026-04-25 17:52:12 +02:00
committed by GitHub
parent 45f70004a1
commit 9b72a87c17
3 changed files with 60 additions and 21 deletions
@@ -380,4 +380,63 @@ describe("composition rules", () => {
expect(finding).toBeUndefined();
});
});
describe("root_composition_missing_data_duration (removed)", () => {
// The rule was a static proxy for the runtime's loop-inflation Infinity
// emission, but lint cannot observe GSAP timeline duration statically and
// the looping shapes that drive it are already covered by
// `gsap_infinite_repeat` and `gsap_repeat_ceil_overshoot`. The rule has
// been removed (#243's Infinity-emission concern is now carried by those
// GSAP rules); these tests pin the removal so the rule does not silently
// come back.
it("does not warn on a docs-compliant root with no data-duration", () => {
// The documented authoring model: root composition without
// data-duration, runtime derives it from the GSAP timeline.
const html = `<!DOCTYPE html><html><body>
<div data-composition-id="docs" data-width="1920" data-height="1080" data-start="0">
<video src="clip.mp4" data-start="0" data-track-index="0" muted playsinline></video>
</div>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["docs"] = gsap.timeline({ paused: true });
</script>
</body></html>`;
const result = lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "root_composition_missing_data_duration",
);
expect(finding).toBeUndefined();
});
it("does not warn even on the original Infinity-risk shape (no media, looping timeline)", () => {
// This was the canonical "warn" case under the old rule — root with no
// data-duration, no media, GSAP timeline driven by repeat: -1. The
// looping shape itself is now flagged by `gsap_infinite_repeat`; the
// duplicate `root_composition_missing_data_duration` warning is gone.
const html = `<!DOCTYPE html><html><body>
<div data-composition-id="loopy" data-width="1920" data-height="1080" data-start="0">
<div class="caption" data-start="1" data-duration="2">hello</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.to(".caption", { x: 100, duration: 1, repeat: -1 });
window.__timelines["loopy"] = tl;
</script>
</body></html>`;
const result = lintHyperframeHtml(html);
// The deprecated rule must not fire.
const removedFinding = result.findings.find(
(f) => f.code === "root_composition_missing_data_duration",
);
expect(removedFinding).toBeUndefined();
// The looping shape is still surfaced — by `gsap_infinite_repeat`,
// which is the more actionable signal pointing at the real authoring
// mistake.
const gsapFinding = result.findings.find((f) => f.code === "gsap_infinite_repeat");
expect(gsapFinding).toBeDefined();
});
});
});
@@ -210,26 +210,6 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
return findings;
},
// root_composition_missing_data_duration
({ rootTag }) => {
const findings: HyperframeLintFinding[] = [];
if (!rootTag) return findings;
const compId = readAttr(rootTag.raw, "data-composition-id");
if (!compId) return findings;
const hasDuration = readAttr(rootTag.raw, "data-duration") !== null;
if (!hasDuration) {
findings.push({
code: "root_composition_missing_data_duration",
severity: "warning",
message: `Root composition "${compId}" is missing data-duration. Without an explicit duration, the runtime may infer Infinity for compositions with repeating animations, causing playback issues.`,
fixHint:
'Add data-duration="X" to the root composition element, where X is the total duration in seconds.',
snippet: truncateSnippet(rootTag.raw),
});
}
return findings;
},
// standalone_composition_wrapped_in_template
({ rawSource, options }) => {
const findings: HyperframeLintFinding[] = [];