diff --git a/packages/core/src/lint/rules/gsap.test.ts b/packages/core/src/lint/rules/gsap.test.ts index acce70fb..f7680b02 100644 --- a/packages/core/src/lint/rules/gsap.test.ts +++ b/packages/core/src/lint/rules/gsap.test.ts @@ -411,4 +411,57 @@ describe("GSAP rules", () => { expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); }); + + it("errors on repeat: -1 (infinite repeat breaks capture engine)", () => { + const html = ` + +
+ + +`; + const result = lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "gsap_infinite_repeat"); + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("error"); + expect(finding?.message).toContain("repeat: -1"); + }); + + it("does not error on finite repeat values", () => { + const html = ` + +
+ + +`; + const result = lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "gsap_infinite_repeat"); + expect(finding).toBeUndefined(); + }); + + it("does not false-positive on repeat: -10 (invalid GSAP but not infinite)", () => { + const html = ` + +
+ + +`; + const result = lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "gsap_infinite_repeat"); + expect(finding).toBeUndefined(); + }); }); diff --git a/packages/core/src/lint/rules/gsap.ts b/packages/core/src/lint/rules/gsap.ts index a4dff282..db9f3b67 100644 --- a/packages/core/src/lint/rules/gsap.ts +++ b/packages/core/src/lint/rules/gsap.ts @@ -482,6 +482,34 @@ export const gsapRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [ return findings; }, + // gsap_infinite_repeat + ({ scripts }) => { + const findings: HyperframeLintFinding[] = []; + for (const script of scripts) { + const content = script.content; + // Match repeat: -1 in GSAP tweens or timeline configs + const pattern = /repeat\s*:\s*-1(?!\d)/g; + let match: RegExpExecArray | null; + while ((match = pattern.exec(content)) !== null) { + const contextStart = Math.max(0, match.index - 60); + const contextEnd = Math.min(content.length, match.index + match[0].length + 60); + const snippet = content.slice(contextStart, contextEnd).trim(); + findings.push({ + code: "gsap_infinite_repeat", + severity: "error", + message: + "GSAP tween uses `repeat: -1` (infinite). Infinite repeats break the deterministic " + + "capture engine which seeks to exact frame times. Use a finite repeat count calculated " + + "from the composition duration: `repeat: Math.ceil(duration / cycleDuration) - 1`.", + fixHint: + "Replace `repeat: -1` with a finite count, e.g. `repeat: Math.ceil(totalDuration / singleCycleDuration) - 1`.", + snippet: truncateSnippet(snippet), + }); + } + } + return findings; + }, + // scene_layer_missing_visibility_kill ({ scripts, tags }) => { const findings: HyperframeLintFinding[] = [];