From 844bb8055220ab8ef3bba1813f6e96c4c4236a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 7 Apr 2026 02:36:31 +0200 Subject: [PATCH 1/2] fix(core): add lint rule for infinite GSAP repeat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detects `repeat: -1` in GSAP timelines and reports it as an error. Infinite repeats break the deterministic capture engine which seeks to exact frame times — the timeline never resolves a finite duration. This caused the loading-spinner eval (prompt 20, 2.0/5) to render as a blurry compressed mess. The rule instructs authors to calculate a finite repeat count from the composition duration instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/lint/rules/gsap.test.ts | 36 +++++++++++++++++++++++ packages/core/src/lint/rules/gsap.ts | 28 ++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/packages/core/src/lint/rules/gsap.test.ts b/packages/core/src/lint/rules/gsap.test.ts index acce70fb7..15a3036f4 100644 --- a/packages/core/src/lint/rules/gsap.test.ts +++ b/packages/core/src/lint/rules/gsap.test.ts @@ -411,4 +411,40 @@ 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(); + }); }); diff --git a/packages/core/src/lint/rules/gsap.ts b/packages/core/src/lint/rules/gsap.ts index a4dff2829..47b3a050a 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/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[] = []; From 17791d1faf68adaae2b49620d9af77fa03cc549a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 7 Apr 2026 03:24:50 +0200 Subject: [PATCH 2/2] fix(core): prevent false positive on repeat: -10 in lint rule Add negative lookahead (?!\d) to the repeat: -1 regex so it only matches exactly -1, not -10, -100, etc. Add test case. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/lint/rules/gsap.test.ts | 17 +++++++++++++++++ packages/core/src/lint/rules/gsap.ts | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/core/src/lint/rules/gsap.test.ts b/packages/core/src/lint/rules/gsap.test.ts index 15a3036f4..f7680b026 100644 --- a/packages/core/src/lint/rules/gsap.test.ts +++ b/packages/core/src/lint/rules/gsap.test.ts @@ -442,6 +442,23 @@ describe("GSAP rules", () => { tl.to("#spinner", { rotation: 360, duration: 0.8, repeat: 4, ease: "none" }, 0); window.__timelines["main"] = tl; +`; + 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"); diff --git a/packages/core/src/lint/rules/gsap.ts b/packages/core/src/lint/rules/gsap.ts index 47b3a050a..db9f3b671 100644 --- a/packages/core/src/lint/rules/gsap.ts +++ b/packages/core/src/lint/rules/gsap.ts @@ -488,7 +488,7 @@ export const gsapRules: Array<(ctx: LintContext) => 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/g; + 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);