* fix(engine): the drawElement self-check no longer rejects count-up text it should accept
The check's ground-truth screenshot was taken after a seek that suppressed timeline callbacks, so text a callback writes (a count-up) was stale in the truth while the real render, which runs callbacks, showed it correctly. The check failed and the whole render restarted on the slower screenshot path.
* docs(engine): shorten a comment to the line limit
* fix(engine): the drawElement self-check reads its truth on a page the render does not use
Callbacks now run in the truth seek (a count-up or class swap no longer looks stale), and they run on a disposable page so a callback that changes the page cumulatively cannot change what the render draws. Compositions with videos keep the in-page path, because the video injector is tied to the render page.
* docs(engine): cut a comment to three lines
* fix(engine): a truth page that cannot open falls back to the render page
* feat(producer): fail a render that cannot finish within its maxRenderMs budget
* chore(producer): raise the producer size budget to the measured size of the render time guard
On macOS the extractor checks once per process whether ffmpeg has zscale. The test ran that
check through its counting shim, so it saw 11 ffmpeg runs instead of 8 cuts plus 2 probes and
failed on every Mac with zscale. Take the check with the real ffmpeg before the shim counts.
* test(cli): check the browser that ran the hanging script, and that every browser it started is gone
* test(cli): type the launch recorder's pid file and drop the added comment
* fix(producer): --workers N is a ceiling, drawElement renders keep their single worker
* docs(producer): keep the inversion predicate's doc as short as before
A sub-composition file with a <template> is mounted from the template alone, so a <style>
in its <head> (or a stylesheet link in its body) never applies in preview, snapshot or render.
Warn on it, with the fix in the message: move it inside <template>.
* fix(cli): a failed frame check no longer switches parallel capture off for good
The per-install breaker tripped on the first self-verify fallback and nothing ever cleared it. Now a second fallback within 20 router renders turns the router off, a new CLI version or 20 clean renders turn it back on, and the stored record keeps the rejected frame (reason, frame, dB, threshold, version, time).
* docs(cli): shorten three comments to the line limits
* refactor(cli): keep the breaker record parsing small and the comments within the ratchet
* fix(producer): extract HDR video frames a window at a time so scratch disk stays bounded
* fix(producer): drop the added compositor comment for the Comments gate
* fix(producer): keep the HDR scratch read plain and fail the render when a window cannot be extracted
* fix(producer): settle a looped HDR source's frame count before capture starts
* chore(producer): raise the packed size budget by the windowed HDR extraction's measured delta
The last fallback took whatever audio came first in the preview, which is usually the voiceover, whenever the music row's own element was momentarily absent.
* fix(core): the preview keeps a voiceover's decoded audio across an in-place reload
The decode cache and the release pass keyed the same asset two ways, so a clip the release pass was handed as live could lose its buffer and be re-fetched mid-playback.
* refactor(core): resolve the decode key through the runtime's own assetUrl
Smaller than the inline URL resolution it replaces, which tipped the producer package-size budget, and one owner for "resolve against the document".
* refactor(core): drop the decode-key comment the comment ratchet rejects
* fix(core): an unreadable clip src no longer stops the preview from playing
Resolving the decode key threw on a src that is not a URL, out of the release pass and through play(), so one bad clip silenced the whole preview instead of just failing its own fetch.
* fix(cli): a long video copy reports progress, resumes in 15 s pieces and stops only when stalled
* perf(core): a video copy's size box follows the stage within a fifth, not a half
* docs(cli): shorten two comments to the line limit
* fix(cli): the copy heartbeat drops a finished copy; size budgets follow the stall and progress code
* chore: size budgets rise by this change's measured bytes
* perf(core): mounting a scene no longer restyles it to look for raster hints
* perf(core): raster hints read inline hints directly instead of matching every style
* perf(core): raster hints read the stylesheets once per page check, not per batch
* perf(core): raster hints watch no attributes while nothing is marked or hinted
* fix(core): a copied or re-attached marked layer keeps being followed
* chore(producer): raise the packed size budget by the 336 bytes the raster-hint fix adds
The producer bundles the runtime. On main (aae32e43ed) it packs at 773560 bytes
(2821768 unpacked) and this branch at 773896 (2822665): packed rises by 336, unpacked by 897.
* perf(producer): cut distributed chunks by estimated capture cost, same chunk count
Auto-sized plans place chunk boundaries so each chunk carries about the same estimated capture
cost (page base plus active video megapixels); chunk count, explicit chunkSize, uniform-cost
compositions and the targetChunkFrames ceiling are unchanged. Frame-cost calibration samples by
cost interval plus the heaviest frame, so a heavy window is always measured.
* fix(producer): hold cost-weighted chunks to the worker time limit's frame cap
The weighted cut could grow a chunk up to MAX_CHUNK_SIZE frames, past the per-chunk frame count resolveMaxParallelChunks sized against the worker time limit. The cut now takes the same cap, from one shared helper, and keeps the equal-frame size as its floor.
* fix(producer): keep the chunk-cut doc comment under 120 characters
* perf(producer): drop the calibration sampler change from the cost-weighted cut
The cut no longer touches how capture cost is measured; that keeps the producer bundle within its size budget and the change to the chunk boundaries alone.
* perf(producer): compute the chunk frame cap inline to keep the bundle small
* fix(cli): snapshot waits for a slow scene to finish building instead of saving a stale frame
* fix(cli): drop the added comments, the names say it
* test(cli): give the contact-sheet engine mock the render-ready timeout snapshot reads
A VP8/VP9 source with no alpha tag has one frame decoded with the alpha-aware decoder when its
metadata is read; if that frame has no alpha plane, its frames extract as jpeg instead of png.
Tagged alpha, a decoded alpha plane, a failed probe, or a plan written without the field keep png.
* fix(sdk): release the parsed film when a session is disposed
A disposed session kept its whole parsed document and element caches. Anything
that still referenced the session, such as a stale closure or React fiber,
therefore kept the replaced film in memory, so memory grew with every film swap.
dispose() now drops the document and caches.
* fix(sdk): warn when a disposed session is read
A disposed session answers reads from an empty document. Outside production it
now logs once, with a stack, so a caller still holding a stale session shows up
instead of silently reading nothing.
* fix(cli): snapshot fails with the clip and time instead of a silent black frame
* fix(cli): one clip timing out in a snapshot no longer loses the other frames
* fix(cli): snapshot fallback seeks to a real frame time so the right frame is picked
* fix(cli): only a frame-extract timeout is recorded as a finding; other errors still stop snapshot
* fix(cli): drop added comments, the names carry the meaning
* fix(cli): a check that is stopped by a signal prints the report it has so far
Killed by SIGTERM or SIGINT, check closed Chrome and exited 143 or 130 with
nothing on stdout, so a caller that stops a long check got no result at all.
The signal now ends the check the way an expired deadline does: findings
already made are kept, one finding says the check stopped before finishing,
and the report is printed before the process exits with the signal's code.
* fix(cli): stop 'hyperframes add' hanging when a desktop clipboard is available
* test(cli): assert the clipboard copy options instead of waiting on real time
* fix(cli): remove stale briefs when re-freezing recipes
* chore: prepare conflict resolution against current main
* chore: preserve reviewed fix on current main
* chore: prepare conflict resolution against current main
* fix(cli): retain recipe brief cleanup with safe directory copies
* chore: prepare conflict resolution against current main
* chore: merge main and refresh recipe skill manifest
* fix(cli): check finishes on long projects with many video clips
check loaded every video in the project at once. On a 500 s project with 137
clips of one 1440p source a single renderer reached 6.4 GB, the second sampling
Chrome crashed, and a crashed puppeteer page never settles its calls, so the
run sat until the 120 s deadline.
- Load the check page in Studio's capture mode so only media near the
playhead loads (peak renderer memory 15.75 GB -> 1.77 GB).
- Read audio durations for the clip-fit audit from a detached element when the
page has not loaded the one in the composition.
- Settle every driver call with the page's crash or close, so a dead helper
hands its samples back and a dead main page fails at once.
* fix(cli): shorten a comment to the line limit
* perf(core): Safari stops re-serializing inline styles on every GSAP write in a playing film
* perf(core): declare the clip-reveal subpath in its descriptor and cut added comment lines
* fix(shader-transitions): shader transitions across a scene boundary keep the scene's contents
Every revealer now goes through one core helper that shows the clip and
lifts the runtime's hidden mark together; the page-side compositor's scene
copies were still carrying the mark. The contents rule keeps its old weight.
* fix(core): films on an older shader-transitions build keep their transition scenes
Published shader builds reveal scenes by inline style without lifting the
hidden mark. When one is on the page, the runtime keeps the inline-style
rules; builds that lift the mark declare it on load and keep the fast rules.
* test(core): pin the hidden-clip contents rule's weight on its selector
* style(core): format the reveal test and move hidden-clip rule notes into names
* test(producer): pin hidden-clip behaviours in real Chrome under the mark-keyed rules
* test(producer): read the forced restyle's result so lint accepts the restyle test
* feat(studio): load GSAP from the CLI's bundled copy by default
Studio preview now loads the GSAP, CustomEase and MotionPathPlugin scripts
it adds from files the CLI ships and serves same-origin at /api/vendor/npm,
instead of jsDelivr. HYPERFRAMES_GSAP_BASE_URL still wins when set, and a
composition on another GSAP version gets its plugin from jsDelivr.
Renders now honor HYPERFRAMES_GSAP_BASE_URL when they replace a missing
local GSAP script, and the setting accepts plain http only for loopback
hosts.
Closes#6205
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(studio): keep comment share flat for the GSAP local root
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): find the installed gsap with node resolution when run from source
The Windows CI lane installs with the hoisted linker, so gsap is not under
packages/core/node_modules there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(producer): load a nested composition's missing GSAP from the mirror
A sub-composition's missing local gsap/dist script was hoisted into the
render unchanged, so HYPERFRAMES_GSAP_BASE_URL (and the jsDelivr fallback)
only reached the entry document. Run the same rewrite on every
sub-composition before it is compiled, resolving its path the way the
inliner will hoist it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(engine): software renders reach SwiftShader when VK_DRIVER_FILES pins a GPU
ANGLE (and Dawn) load SwiftShader by pointing the Vulkan loader at its ICD
through VK_ICD_FILENAMES. The loader ignores VK_ICD_FILENAMES while
VK_DRIVER_FILES is set, and still applies VK_LOADER_DRIVERS_SELECT/DISABLE,
so on a machine that pins its Vulkan driver that way a --no-browser-gpu
render gets ANGLE on the real GPU and assertSwiftShader refuses it.
Launch SwiftShader browsers without those three variables. Hardware
launches, and environments that set none of them, are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(engine): raise the packed size budget by the 342 bytes launchEnv adds
The increase sits on top of main's engine budget, 456055 after #6194.
Main packs the engine at 435883 bytes and this branch at 436205.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): load GSAP core when only a GSAP plugin is on the page
The Studio preview checked for GSAP core by matching "gsap" anywhere in a
script src, so the MotionPathPlugin it had just injected (served from
gsap@x/dist/) counted as core. A composition with a motion path and
Studio-edited motion but no GSAP script of its own then got the plugin and
no core, and its animations did not run.
Detect core by file name (gsap.js, gsap.min.js, or a vendored
gsap-<version>.min.js) on any host or path, and inject Studio motion's core
before the MotionPathPlugin so the plugin can register onto it. Apply the
same core-only check to the sub-composition preview fallback and the
missing_gsap_script lint rule.
Refs #6205
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(studio): keep GSAP core detection within the comment budget
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(studio): load GSAP core ahead of head plugins and only on a real core script
Studio motion and sub-composition previews now insert GSAP core before the
first GSAP plugin script in the head, so an authored MotionPathPlugin or
CustomEase registers on core instead of running first. Core detection keys on
a core script (src, inlined marker, license banner or library body), not on
usage like gsap.registerPlugin() or gsap.defaults(). The missing_gsap_script
lint rule drops the same usage-based and plugin-banner shortcuts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(studio-server): undo and restore keep the media they delete or replace
* fix(studio-server): set-aside media is freed only after a whole undo or restore lands
* fix(studio-server): set-aside media does not count toward the history budget
* fix(studio-server): set-aside media keeps its own ledger apart from copied blobs
* fix(studio-server): a set-aside copy waits for every path that counts on it
* fix(studio-server): an unreadable history folder fails the read instead of looking empty
* fix(studio-server): history opens past a temp file a crash left behind
* fix(studio-server): trim a comment so the comment share holds