* fix(core): a data script in a sub-composition no longer stops every timeline
A sub-composition's <script type="application/json"> was wrapped and run as
JavaScript, and because the bundle merges every composition script into one,
its parse error stopped every scene's timeline. Data scripts now stay in the
DOM as authored, and a script run that does not parse is left unmerged so a
bad script fails alone.
* fix(core): render and preview keep a broken composition script from stopping the rest
The render path's script merge had the same gap as the bundler: one script
that does not parse stopped every timeline. Both now share one parse check.
A run left unmerged, and every scene-part script, is still escaped, so a
string holding "</script>" can no longer end the script early in preview.
* refactor(core): carry the bundler's script-merge reasons in names, not comments
* fix(core): every composition path runs only the scripts a browser would
A script check now matches the browser (esbuild accepted export and top-level
await), so such a script can no longer stop every timeline. Inline template
compositions keep data scripts and run module and import map scripts as such.
A data script beside the composition root is mounted next to it, a nomodule
script never runs, and the render path merges scripts on a page with no head.
* fix(core): keep inline-template data scripts and run a script with a top-level return alone
- parsesAsScript compiles with vm.Script, so a top-level return or new.target
no longer passes as mergeable and stops every timeline.
- The bundler's inline <template id="X-template"> path asks the shared
assembly plan for its root and mounts the inert scripts authored beside it.
- nomodule only stops classic scripts; type="module" nomodule runs, as in
browsers.
* refactor(core): the inline template path reads its root id from the shared assembly plan
* fix(core): a broken composition script stays apart under Bun as well as Node
parsesAsScript now parses with acorn in script mode instead of vm.Script.
Bun 1.3's vm.Script compiles lazily and accepted any text, so the Studio dev
server merged a broken script with the rest and every timeline stopped.
acorn is already locked for parsers and lint; core now declares it.
* test(core): name the parse-check test for what it pins
* style(core): wrap the data-block comment under the line limit
* fix(lint): check passes valid sub-compositions and scene-relative asset paths
* fix(lint): keep the browser lint build free of node modules
* refactor(lint): tidy sub-composition lint helpers and guard bare node imports
* fix(lint): resolve stylesheet asset paths from the stylesheet's own folder
* fix(lint): catch every written form of a path that climbs out of the project
* fix(parsers): sub-composition asset paths written with backslashes load in preview
* fix(parsers): an encoded backslash stays in the file name and a leading backslash stays absolute
* fix(lint): an encoded %2f counts as a separator when checking for a climb out of the project
* fix(cli): renders on Windows stop cancelling when an old parent's pid is reused
* test(cli): a Windows parent created in the same tick as its child stays an ancestor
* fix(studio): a resized corner stays on the pointer when the size rounds to whole pixels
Resized sizes stay on whole CSS pixels (GSAP centres a -50% translate from the
integer offset size), so the centre-anchored resize left the grabbed corner up to
a third of a pixel times the element's scale off the pointer: 0.53 px on a 1.5x
scaled layer. The centre pin now takes half the rounding remainder along the
element's rendered edges, so the grabbed corner lands where the wanted size puts
it while the written size stays whole.
* fix(studio): a scale resize saves the box exactly where it was dropped
finalizeScaleResizeCommit skipped its position correction whenever the box sat
within 0.5 px of the drop point on each axis, so a sub-pixel drop (now the
norm, with the grabbed corner on the pointer) saved up to half a pixel off.
It now skips only when the correction rounds to nothing at the precision the
file is written in.
* fix(studio): a scale resize lands centred on the box that was dropped
finalizeScaleResizeCommit lined the committed box up with the drop by its
top-left corner. When one scale cannot reproduce the dragged width and height
(the draft rounds each to whole pixels), that put the whole gap on the far
edges, up to half a pixel. The resize is centre-anchored, so it now lines up
centres and the gap splits evenly.
* chore(studio): keep the complexity note on the resize intercept, not the new helper
* test(studio): bank the two scaled resize cases that now pass the edit accuracy bench
* fix(studio): a scale resize leaves animated positions alone and mirrored corners track
* test(studio): bank only the two newly passing scaled resize cases
* fix(studio): a mirrored or parent-rotated layer's resize corner tracks the pointer
* fix(studio): a resize corner on a skewed layer picks its side from the box's own edges
* fix(studio): resizing a layer that renders with no area moves nothing
* fix(cli): name the unzip errors when every Windows unzip tool fails
* fix(cli): a cached empty or folder Chrome counts as missing, not found
* fix(engine): skip an empty cached Chrome the same way the CLI does
* fix(engine): keep scanning the Chrome cache past a build it cannot read
* test(engine): a folder at the cached Chrome path is skipped
* fix(studio-server): the preview's proxy copy jumps queued thumbnails, and abandoned copies drop
* fix(studio-server): a re-ask right after a dropped proxy copy gets a fresh copy
* fix(studio-server): leave Studio's preview route on the normal proxy queue
* docs(studio-server): shorten the proxy transcoder's detachment note
* fix(studio-server): a refused proxy copy still invalidates thumbnail captures
* fix(catalog): compare search vectors by direction so the publish PR can pass
* test(catalog): pin the vector tolerance between machine noise and different items
* refactor(catalog): split the per-row cosine out of the vector comparison
* fix(cli): preserve subtitle cues across line endings
* refactor(cli): preserve newline handling while reconciling subtitle tests
* refactor(cli): normalize WebVTT line endings at the parser
* style(cli): format the retained subtitle test block
* docs: clarify async font measurement and timeline registration
* chore: align skill manifest with main before regeneration
* chore: regenerate animation skill manifest
* fix(studio): clip strips resize on the next frame, ending ResizeObserver loop errors
* test(studio): verify strip resize frame scheduling deterministically
* fix(studio): a menu set to render inline opens beside its trigger
* fix(studio): a popover set to render inline opens beside its trigger
* fix(studio): a remounted clip strip keeps its own size instead of a queued one
* test(studio): zoom thumbnail tests run resize frames on fake timers
* test(studio): zoom preview test runs the resize frame it guards
* fix(core): preview stops at the film's end when a scene loops forever
* refactor(core): the loop length threshold lives with the composition length
* test(core): pin the endless-loop length at the boundary and through the runtime
* style(core): keep composition length comments under the file's share
* refactor(studio): preview duration guard reads the core loop length limit
* docs(core): length doc says a loop yields only to a floor or fallback
* perf(engine): wait for a missing composition timeline once per render, not once per browser
A composition host that never registers a timeline (CSS, canvas or rAF animation
without data-no-timeline) made every capture session of a render wait the full
player-ready timeout: calibration, then the workers, then each static-frame check
page. The first session that times out now records the host ids in a memo shared
by the render's sessions; later sessions report the same timeout warning without
waiting again.
* perf(engine): the static-frame check page stops on a failed script and chunks share one memo
The verification page now gets the session's failed-script list, so it bails after
the short grace like the main page instead of waiting the full timeout. Distributed
chunks share one timeline-wait memo across their sessions. The poll takes an
options object instead of eight positional parameters.
* fix(engine): replay only a clean timeline timeout so a script failure still fails the render
A session that skipped the wait classified it at once, before a late page error
could arrive, so a render that used to stop on a script failure could ship. The
memo is now written only by a wait that ran its full length with no page errors
and no failed script loads; a replaying session that already sees a failed script
load reports a script failure.
* fix(cli): browser ensure fails when the unzip leaves no Chrome executable
* refactor(cli): keep install debug output and report only the last unzip attempt
* fix(cli): browser ensure exits after a failure or a --force download
* fix(cli): browser checks name why Chrome could not be installed
* fix(cli): render shows why its setup step failed instead of a crash dump
* refactor(cli): build the browser download label and staged test path once
* refactor(cli): one owner for the render setup result line
* fix(cli): render setup failures report through one error line
* refactor(cli): one reader and writer for both render setup lines
* feat(registry): eight more WebGPU shader backgrounds and a bundle with thirty new shaders
* refactor(registry): simpler bundle copy step in the shader build
* test(registry): every shader block ships the same bundle and names a shader in it
* refactor(registry): one block loader for the shader block tests
* test(registry): name the stale block and match only shader names
* feat(registry): eight more WebGPU shader backgrounds, from blob to fractal noise
* chore(registry): shorter licence line at the top of the shader bundle
* chore(registry): bring in the latest shader bundle branch
* chore(registry): shorter licence line at the top of the shader bundle
* test(registry): every shader block installs the bundle's licences
* fix(studio): let small picks move when dragged by their middle
* fix(studio): keep the grabbed corner under the cursor while resizing
* fix(studio): let picks a few pixels across resize from their corner
* docs(studio): say which picks the resize guard still lets resize
* feat(registry): eight more WebGPU shader backgrounds and a bundle with thirty new shaders
* refactor(registry): simpler bundle copy step in the shader build
* test(registry): every shader block ships the same bundle and names a shader in it
* refactor(registry): one block loader for the shader block tests
* test(registry): name the stale block and match only shader names
* chore(registry): shorter licence line at the top of the shader bundle
* test(registry): every shader block installs the bundle's licences
* feat(cli): models list shows which speech models transcribe can use
* refactor(cli): models list and transcribe share one Parakeet runner check
* fix(cli): models list skips whisper links transcribe would download again
* style(cli): keep models list comments under the package share
* perf(producer): extract distributed video frames in each chunk
Plan v2 no longer extracts long constant-frame-rate videos. It ships
each source once and every chunk extracts only the frames it shows, so
Plan time stops growing with video length (#5260).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs(aws-lambda): plan no longer bounds video length
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(producer): pin deferred compiled sources to every chunk
Each chunk's page loads every <video> src during session init and a
missing file fails the chunk, so a deferred source under compiled/ must
keep reaching every chunk, as it does on main.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>