`lintAudioSrcNotFound` resolves every `<audio src>` against the project
root, which is correct for index.html but wrong for sub-compositions —
their srcs are written relative to the sub-composition file (e.g.
`../assets/foo.mp3`), and the bundler rewrites them at compile time.
Carry the sub-composition path alongside each html source and run
`<audio src>` strings starting with `../` through the existing
`rewriteAssetPath` helper before checking existence on disk. Mirrors the
runtime/bundler behaviour so the lint check sees the same path the
renderer will fetch.
Original (un-rewritten) src is still surfaced in the finding message so
authors can grep for it in their HTML.
Three new lint rules that catch structural issues causing compositions
to fail silently in preview:
- root_composition_missing_data_start: Root composition needs data-start="0"
for the runtime to begin playback
- standalone_composition_wrapped_in_template: index.html should not be
wrapped in <template> (only sub-compositions use that)
- root_composition_missing_html_wrapper: index.html needs <!DOCTYPE html>
and <html> wrapper for the bundler
Also adds rawSource to LintContext so rules can inspect pre-template-stripped
HTML, and isSubComposition to linter options so rules can distinguish root
from sub-composition files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## What
Added GSAP CDN imports to all template compositions and improved project linting to skip template placeholders.
## Why
Templates were missing GSAP script imports, causing JavaScript errors when GSAP animations tried to execute. The linter was also incorrectly flagging template placeholder values like `__VIDEO_SRC__` as missing audio sources.
## How
- Added `<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>` to all composition HTML files
- Updated `lintAudioSrcNotFound` function to skip template placeholders matching the pattern `__[A-Z_]+__`
- Fixed GSAP animation properties by adding `overwrite: "auto"` to prevent conflicts
- Replaced CSS transforms with GSAP `gsap.set()` calls for better animation control
- Moved video elements outside timed containers in some templates to avoid nesting issues
- Standardized transcript data format to use double quotes for JSON consistency
- Added proper timeline initialization with `window.__timelines = window.__timelines || {}`
- Enhanced caption styling with overflow handling and max-width constraints
## Test plan
- [x] Manual testing performed
- [x] Verified GSAP animations work correctly in all templates
- [x] Confirmed linter no longer flags template placeholders as errors
- [x] Tested video playback and audio synchronization
## Summary
- Respect `--json` flag on all lint exit paths so agents always get machine-readable output
- Separate `infoCount` from `warningCount` in linter results (was conflated)
- Display `info` vs `warning` severity distinctly in lint output
## Summary
- **Lint rules** catch media elements missing `id` (renderer silently skips them), missing `src`, `preload="none"` (blocks renderer), and video nested in timed divs (freezes playback). Upgraded `video_nested_in_timed_element` from warning to error.
- **Compiler** strips `preload="none"` from media during compilation. Runs parallel, cached keyframe interval analysis via ffprobe — warns on sparse keyframes (>2s) that cause seek failures and audio/video desync. Suggested ffmpeg command preserves audio (`-c:a copy`).
- **Pre-render lint** lints `index.html` + all `compositions/*.html` sub-compositions before render via shared `lintProject()` helper. Warns by default; `--strict` blocks on errors, `--strict-all` blocks on errors + warnings.
- **Render orchestrator** logs a hint to retry with `--workers 1` when parallel capture times out on video-heavy compositions.
- **Refactor**: extracted `runFfprobe()` + `parseProbeJson()` helpers to deduplicate ~80 lines of spawn boilerplate across 3 ffprobe functions. Extracted `shouldBlockRender()` so strict flag tests exercise production code. Shared `lintProject()` used by both `lint` and `render` commands.
## Context
Discovered during a real composition build session where:
1. `<audio>` without `id` rendered silently (preview worked fine because runtime queries `[data-start]`, but renderer queries `[id][src]`)
2. `<video>` inside timed `<div>` froze on first frame
3. `preload="none"` caused 45s renderer timeout
4. YouTube clips with sparse keyframes from `yt-dlp --download-sections` caused audio/video desync
5. Parallel workers timed out on video-heavy compositions
## Test plan
- [x] Core: 365/365 tests passing (5 new lint tests)
- [x] Engine: 24/24 tests passing
- [x] CLI: 14/14 tests passing (7 lintProject + 7 shouldBlockRender)
- [x] Lint + format hooks pass
- [ ] Manual: create a composition with `<audio data-start="0" src="test.wav">` (no id) — verify `npx hyperframes lint` catches it
- [ ] Manual: run `npx hyperframes render --strict` with lint errors — verify it blocks
- [ ] Manual: run `npx hyperframes render --strict-all` with lint warnings — verify it blocks