Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/core/src/lint/rules/composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
20 changes: 0 additions & 20 deletions packages/core/src/lint/rules/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
2 changes: 1 addition & 1 deletion skills/website-to-hyperframes/references/step-6-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ After building the composition, check WITH ACTUAL CODE:
- [ ] No full-screen dark linear gradients (H.264 creates visible banding — use solid + localized radial glows)
- [ ] Timeline registered: `window.__timelines["comp-id"] = tl`
- [ ] Colors match DESIGN.md exactly (paste the HEX value, don't approximate)
- [ ] **Every `<template>` root element** — not just `index.html`, but every sub-composition's root — has `data-start="0"` **and** `data-duration="<beat_seconds>"`. The linter warns `root_composition_missing_data_start` / `root_composition_missing_data_duration` when missing; without `data-duration` the runtime may infer `Infinity` on repeating animations and stall playback.
- [ ] **Every `<template>` root element** — not just `index.html`, but every sub-composition's root — has `data-start="0"`. The linter warns `root_composition_missing_data_start` when missing. Authoring `data-duration="<beat_seconds>"` on the root is also recommended for compositions whose GSAP timeline uses repeating animations (`repeat: -1` or large `repeat: N`); without it the runtime may infer `Infinity` and stall playback. The linter flags those repeating shapes directly via `gsap_infinite_repeat` and `gsap_repeat_ceil_overshoot`.
- [ ] **Caption exits have a hard kill.** If you animate captions out with `tl.to(groupEl, { opacity: 0 }, group.end)`, follow it with `tl.set(groupEl, { opacity: 0, visibility: "hidden" }, group.end)` as a deterministic kill — per-word karaoke tweens can override the exit tween and leave captions stuck on screen. Linter: `caption_exit_missing_hard_kill`.
- [ ] **No duplicate media nodes.** If the same image/video source is referenced twice with identical `data-start` + `data-duration`, the compiler discovers it twice and can double-render. Dedupe by using a single `<img>` with appropriate z-layering, or stagger the `data-start` values. Linter: `duplicate_media_discovery_risk`.

Expand Down
Loading