From efb09e33bada0346a2f484edda5506aaa692f70a Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 1 Apr 2026 21:29:56 -0700 Subject: [PATCH] feat(lint): warn when captions use fetch() instead of inline TRANSCRIPT Adds two lint rules for caption compositions: - caption_transcript_not_inline: warns when transcript is loaded via fetch() instead of an inline var TRANSCRIPT array - caption_transcript_parse_error: warns when the inline TRANSCRIPT array is not valid JSON (common with unquoted keys + apostrophes) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/lint/rules/captions.ts | 53 ++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/core/src/lint/rules/captions.ts b/packages/core/src/lint/rules/captions.ts index f9169aed0..2a19ee4a2 100644 --- a/packages/core/src/lint/rules/captions.ts +++ b/packages/core/src/lint/rules/captions.ts @@ -55,6 +55,59 @@ export const captionRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> return findings; }, + // caption_transcript_not_inline + ({ scripts, styles, options }) => { + const findings: HyperframeLintFinding[] = []; + // Only check files that look like caption compositions + const isCaptionFile = + (options.filePath && /caption/i.test(options.filePath)) || + styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content)); + if (!isCaptionFile) return findings; + + const allScript = scripts.map((s) => s.content).join("\n"); + const hasInlineTranscript = /(?:const|let|var)\s+(?:TRANSCRIPT|script)\s*=\s*\[/.test( + allScript, + ); + const hasFetchTranscript = /fetch\s*\(\s*["'][^"']*transcript/i.test(allScript); + + if (!hasInlineTranscript && hasFetchTranscript) { + findings.push({ + code: "caption_transcript_not_inline", + severity: "warning", + message: + "Captions composition loads transcript via fetch(). The studio caption editor " + + "requires an inline `var TRANSCRIPT = [...]` array to detect and edit captions.", + fixHint: + 'Embed the transcript as `var TRANSCRIPT = [{ "text": "...", "start": 0, "end": 1 }, ...]` ' + + "with JSON-quoted property keys. See the captions skill for details.", + }); + } + + if (hasInlineTranscript) { + // Verify the inline transcript can be parsed + const varPattern = /(?:const|let|var)\s+(?:TRANSCRIPT|script)\s*=\s*(\[[\s\S]*?\]);/; + const match = allScript.match(varPattern); + if (match?.[1]) { + try { + JSON.parse(match[1]); + } catch { + findings.push({ + code: "caption_transcript_parse_error", + severity: "warning", + message: + "Inline TRANSCRIPT array is not valid JSON. The studio caption editor may fail " + + "to parse it. Common cause: unquoted property keys with apostrophes in text.", + fixHint: + 'Use JSON-quoted keys: { "text": "don\'t", "start": 0, "end": 1 } instead of ' + + '{ text: "don\'t", start: 0, end: 1 }.', + }); + } + } + } + + return findings; + }, + // caption_container_relative_position ({ styles }) => { const findings: HyperframeLintFinding[] = [];