| 435 | } |
| 436 | |
| 437 | function lintMultipleRootCompositions(projectDir: string): HyperframeLintFinding[] { |
| 438 | const findings: HyperframeLintFinding[] = []; |
| 439 | try { |
| 440 | const rootHtmlFiles = readdirSync(projectDir).filter((f) => f.endsWith(".html")); |
| 441 | const rootCompositions: string[] = []; |
| 442 | for (const file of rootHtmlFiles) { |
| 443 | if (file === "caption-skin.html") continue; |
| 444 | const content = readFileSync(join(projectDir, file), "utf-8"); |
| 445 | if (/data-composition-id/i.test(content)) { |
| 446 | rootCompositions.push(file); |
| 447 | } |
| 448 | } |
| 449 | if (rootCompositions.length > 1) { |
| 450 | findings.push({ |
| 451 | code: "multiple_root_compositions", |
| 452 | severity: "error", |
| 453 | message: `Multiple root-level HTML files with data-composition-id: ${rootCompositions.join(", ")}. The runtime may discover both as entry points, causing duplicate audio playback.`, |
| 454 | fixHint: |
| 455 | "A project should have exactly one root index.html with data-composition-id. Remove or rename extra files.", |
| 456 | }); |
| 457 | } |
| 458 | } catch { |
| 459 | /* directory read failed — skip */ |
| 460 | } |
| 461 | return findings; |
| 462 | } |
| 463 | |
| 464 | function lintDuplicateAudioTracks(htmlSources: HtmlSource[]): HyperframeLintFinding[] { |
| 465 | const findings: HyperframeLintFinding[] = []; |