Source-code line counts and the embedded code block picked up the previous
commit's reflow; payload JSON re-derives from the reformatted composition
HTML. No docs.json change: the nav fix already matches what this branch had.
scripts/generate-catalog-pages.ts's own docs.json rebuild -- the one
CONTRIBUTING.md tells a contributor to run alone -- wrapped any section with
exactly one child shelf of the same name (3D motion, Carousels, Cursors,
Blocks) in a redundant identical-name parent: {group:"3D motion",
pages:[{group:"3D motion", pages:[...]}]}. Reproduced on a clean origin/main
worktree: running the generator with nothing else changed still rewrote 959
lines of docs.json. sync-docs-catalog.mjs, run afterward, silently repaired
it, so the bug only ever surfaced when the documented single-command recipe
ran on its own.
Extracted the collapsing decision into sectionEntry(), a pure function:
flatten to the child's own pages when a section wraps exactly one
identically-named shelf, otherwise keep the nested shelf (a differently-named
single child, like "Data & charts" wrapping "Data", still nests on purpose).
Shown red without the fix (packages/core's node --test file):
31 passed, 1 failed -- "flattens a section that wraps exactly one shelf of
its own name". Green with it: 32 passed. Ran the full generator against this
branch's own registry afterward and confirmed programmatically: no group in
docs.json's Catalog tab nests a child whose own "group" equals its parent's.
cursor-drag-drop.html drew its hand's outline and fill passes as two full
copies of the same six shapes; one <defs> group referenced twice by <use>
draws the same pixels. cursor-text-select.html had one hand-inlined clamp
that already matched the file's own seg() helper. Pixel-diffed against the
originals at four times each: 0 changed pixels.
The eight items added in one batch got their pages generated before their
own payload JSON existed, so they carried an empty variables prop and a
recorded-clip fallback instead of the live player. Regenerating now that
every payload exists fixes that; no hand edits to the pages.
Drag and drop, marquee select, text select, context menu, magnetic pointer,
zoom follow, annotate and agent cursors. Each is a mountable component with
a demo scene, variables and a live catalog page, and every frame is a pure
function of timeline time so seeking works both ways.
Items whose first tag is cursor now sit on their own Cursors shelf: the six
existing pointer items move there, and dart-cursor joins them, an outlined
arrowhead that dives in from depth, flies nose-first with motion blur,
squashes on two clicks and whips out.
Running the generators on a clean main changes these committed files:
registry.json was missing week-in-merges, three item pages carried stale
source, and the nav and gallery data still placed text-match-cut and
colorama-wipe on shelves their tags no longer pick. No hand edits.
* fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice
transcript-cut extracts each kept range to its own final-codec file, then
concat-copies them. That produces two audible artifacts at every cut:
1. No boundary fades. Concat splices raw segment edges together, so the
waveform steps discontinuously and each cut clicks.
2. Encoder priming silence. Each segment is encoded to AAC independently,
so the encoder pads every one with priming samples. Stream-copy concat
bakes that padding in as a silent gap at each splice. Measured 24.4 ms
and 24.1 ms at the two splices of a three-range cut. It accumulates:
the nth splice lands roughly 25n ms late, so splice positions drift
away from the sum of the preceding segment durations.
The second survives the first -- adding fades alone leaves the gap.
Fix: intermediates carry PCM (.mkv for video, .wav for audio-only) instead
of the final codec, so audio is encoded exactly once, at concat. Boundary
fades of 30 ms are applied per segment, scaled down on segments shorter
than 4x the ramp and skipped on degenerate ones.
Measured on the same three-range cut, widest run of digital silence at
each splice:
before 24.4 ms / 24.1 ms
after 0.1 ms / 0.1 ms
--copy is unchanged: stream copy cannot filter, so it keeps producing
final-codec segments and a copy concat. Audio-only output is byte-identical
in duration before and after. scripts/lib test suite: 34/35, the one
failure (lut-preset-provider) pre-exists on main and is unrelated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(skills): regenerate manifest for media-use
* fix(media-use): honor the fadeIn/fadeOut flags fadeFilterFor was passed
The caller already computed {fadeIn, fadeOut} per segment, but
fadeFilterFor still took one parameter and always emitted both
afade ramps -- so a single kept segment (no splice at all) and every
segment's true start/end got faded regardless. Extracted the pure
function into lib/transcriptCutFade.mjs, made it honor both flags,
and added its first unit test: first/middle/last/single-segment
filter strings, no ffmpeg needed, fails against the previous version.
Also installs ffmpeg in the Test: skills CI job (same action the
Producer job uses), so transcript-cut.test.mjs's real-tone RED/GREEN
proof runs in CI instead of skipping silently for lack of the binary.
* fix(media-use): fix the packed-CLI import and lint on transcript-cut.mjs
build-copy.mjs replaces dist/skills/media-use/scripts/lib with a copy
of packages/cli/src/media-use/lib, so transcriptCutFade.mjs needed a
byte-identical twin there (and in MEDIA_USE_COPY_NAMES) or the packed
CLI's transcript-cut.mjs imported a file that no longer existed.
Moved its test alongside, matching every other lib helper's tests.
Also drops three redundant Boolean(args.copy) wrappers (a ternary
condition already coerces) that oxlint's no-extra-boolean-cast
flagged.
* style(media-use): reflow the shortened ext ternary to oxfmt's shape
Dropping the Boolean() wrapper shortened the line enough that oxfmt
0.41 now collapses the ternary onto one line; format:check was failing
on the stale multi-line version.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
The shadow reload promotes on frame-driven readiness (the player's
paint-and-idle, then two requestAnimationFrames) but failed on a
wall-clock 15s setTimeout. A hidden tab renders no frames, so every
external edit made while Studio was in the background failed with
"it took too long to load", the toasts stacked, and the stale preview
stayed up even after returning to the tab.
The budget now only runs while the document is visible: hiding the tab
clears the timer, and showing it again restarts the full budget for the
pending shadow, which then promotes normally.
Fixes#4290
— Rames Jusso
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(registry): add text-match-cut — one text becomes another mid-move on a single 85 % curve
Video primitive reversed from an After Effects tutorial (Editing Snapped, "The Match
Cut Technique Every After Effects User Needs"), measured per frame at 60 fps. Both
texts ride one keyframe pair, cubic-bezier(.85, 0, .15, 1) over 2 s, and hard-swap at
the speed peak (1.0 s). A `mode` variable picks the property that carries the cut:
scale down (the tutorial), scale up, tracking in, or a same-direction slide in four
directions. Optional cyan-to-blue gradient sweep on its own 87 % curve, navy or
transparent background.
* chore(registry): integrate text-match-cut with current main
---------
Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
* feat(registry): add extended-keyframe, the never-fully-lands arrival with motion blur
A hyperframes:component. An element covers most of its move in the first fifth of the
duration and creeps the rest to the last frame (After Effects' extended-last-keyframe
technique), exits along the vector it arrived on, and carries shutter motion blur:
slides via the bundled feOffset filter smear, scale beats via a velocity-driven blur.
var a = attachExtendedArrival("#word", tl, { at: 0.3, from: { x: -1400 } });
attachExtendedArrival("#next", tl, { at: a.end, from: { scale: 0.35 } });
Direction and property agnostic (x / y / scale). The ease is exposed on its own as
EK.ease() for any tween, with EK.next() for sequencing and EK.gate() as a numeric check
(single velocity peak, no rises after it, tail above zero to the last frame).
registry/registry.json, docs/docs.json and docs/public/catalog-index.json regenerated
with the two generator scripts; they also pick up drift already present on main.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(registry): publish live extended keyframe payload
* fix(registry): tear down extended keyframe blur
* fix(catalog): refresh extended keyframe payload
* fix(catalog): preserve merged catalog entries
* fix(catalog): regenerate catalog index
* fix(registry): trim extended-keyframe comments
* chore(registry): retrigger checks
* fix(registry): refresh extended-keyframe artifacts
* fix(catalog): refresh extended-keyframe payload
* fix(registry): refresh extended-keyframe artifacts against current main
---------
Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
check-registry-set-delta.mjs only recognized removals: an added name in
registry.json had no matching allowance, so any pure-addition PR failed
with "unexpected in registry.json: <name>" even though the item's own
registry-item.json was added in the same diff. Mirror the deletion rule
for additions: a name is expected when its own registry-item.json was
added or deleted in the same diff, same condition either side already
required.
CI's audit tool flagged the CLI entry itself (high-crap-score rule,
scripts/check-registry-set-delta.mjs:30 main, CRAP 42.0 threshold 30.0,
cyclomatic 6): main() shelled to git and parsed JSON inline. The git/fs
boundary is now injected as runRegistrySetDeltaCheck(base, { run,
readRegistryJson }), covered directly by a test with a fake run/read, so
main() is a thin print-and-exit wrapper.
* feat(studio): render geometry-sized video filmstrips
* fix(studio): keep poster tier during timeline scroll
* fix(studio): cap rendered thumbnail tiles
* chore(studio): refresh filmstrip checks
* chore(studio): refresh filmstrip captures
* chore(studio): refresh four-times captures
* fix(studio): stop capping filmstrip render tiles at 33
computeThumbnailStrip no longer clamps frameCount; the decode-side cap already lives in quantizeThumbnailFrameCount. VideoThumbnail only acquires the rich filmstrip lease when requestFrameCount > 1, so a narrow clip issues one decode job instead of two identical ones. Deletes the unreachable duplicate defaults in createVideoThumbnailRequest.
* style(studio): format filmstrip changes
Implements the fixes from the 2026-09-21 cutover dashboard read
(plans/sdk/2026-09-21-cutover-dashboard-fixes-plan.md):
- sdk_cutover_failed now carries `family` and `error_kind`
(network/conflict/http/sdk). The one production gate event was a raw
`TypeError: Failed to fetch` from an unwrapped fetch (readProjectFile /
writeProjectFile's preflight), which an instanceof-only classifier would
still file as "sdk" and trip the rollback gate — added a message-based
fallback for the browser's fetch-rejection strings.
- studio:sdk_session_unavailable's `network` reason now carries `elapsed_ms`
and `hidden` (document.visibilityState at rejection), separating a
near-instant policy block (CSP/PNA/extension) from a dropped connection
the tab or server going away mid-flight.
- The ownership-race branch in useSdkSession's open effect now emits
sdk_session_superseded instead of sdk_session_unavailable{stage:ownership}
— it's a benign race (identity moved on mid-open), not a broken
precondition, and was being counted as one on the dashboard.
- Root-caused and fixed the read-404 "storm": /api/events (CLI host) and
the vite-dev hot-reload channel are one connection per SERVER, not per
project. A stale tab left open after `hyperframes preview` restarts on a
different folder (same port) received every save the CURRENT project
made, reloading its preview (which then 404s) and re-reading its own
composition on each one. Both transports now carry `projectId`; the
client-side coordinator drops a delivery whose projectId doesn't match
the tab's (absent field still passes, for one release of skew).
- studio-server's file routes report `why` on 403/404: `project_dir_missing`
(new — the CLI host's static resolveProject kept answering for a project
whose folder was renamed/deleted under a running server, which used to
fail closed as an indistinguishable path-traversal 403) vs `nul` vs
`outside_project`.
- absent_or_empty reads now carry `path_in_tree` (against the caller's
loaded file tree). Instrumentation only, per the graveyard page
(clearing-activecomppath-on-tree-absence) that refuted the "clear
activeCompPath" fix this was the proposed next step for.
Tests: unit coverage added for every change (sdkCutover.test.ts,
useSdkSession.lifecycle.test.tsx, useExternalFileChangeCoordinator.test.tsx,
studioServer.test.ts, files.pathSafety.test.ts). oxlint/oxfmt clean,
tsc --noEmit clean on studio/studio-server/cli.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* feat(registry): add colorama-wipe, a colour-mapped feathered wipe reveal for headlines
A hyperframes:component recreating the After Effects "shiny wipe": masked text +
Compound Blur + Colorama over a moving gradient, with a crisp duplicate fading on top.
One call per text element:
attachColoramaWipe("#headline", tl, { at, in, hold, out, cycle, feather, tilt, blur })
adds a single linear clock tween to the host timeline and paints the effect on two
canvases over the element's box; the element is hidden (visibility) with its layout
kept. All sizes scale from the element's font size. Deterministic: hash noise keyed by
frame, no Math.random, no clocks; seeks outside the window paint an empty stage.
registry/registry.json, docs/docs.json and docs/public/catalog-index.json were
regenerated with the two generator scripts as the contributing guide asks; they also
pick up drift already present on main.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(registry): publish live colorama wipe payload
* style(registry): format colorama wipe demo
* fix(registry): tear down colorama wipe on timeline kill
* fix(catalog): refresh colorama wipe payload
* fix(catalog): preserve merged catalog entries
* fix(catalog): regenerate catalog index
---------
Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* feat(studio): timeline clips take the designed material
Picture clips use an 8px radius and audio clips a 21px pill.
Clips sit one step above the lane. The label is a chip that
drops under 60px. Selection keeps the inset outline plus a
1px inner highlight. Hover background is the only transition.
* style(studio): format timeline clip shadow
* fix(studio): restore handle-bar transition and de-pin box-shadow test
* feat(studio): menus and popovers paint instantly
Menus and popovers open and close with no fade. Hover colour is unchanged.
* fix(studio): fade menus in from a visible shape
Menus, popovers, and dropdowns start at opacity 0.8 and scale 0.97, then settle in 120ms. They close in 90ms. Tooltips use the same shape in 100ms.
* ci: select the remaining orphan tests in their owning runners
* ci: provide FFmpeg for the catalog preview witness
* ci: pass the installed Chrome path to catalog rendering
* test: align creator capability checks with speed ramp support
#4153 added supported speed ramps as rate lanes in data-automation. The
previously unselected capability tests still required preprocessing.
Require the documented rate-lane representation and its precedence over
the constant playback rate, matching the runtime resolver.
* feat(registry): add a week-in-merges leaderboard block
Authors race across the row at a speed set by their merge count, bouncing at both edges.
* chore(registry): cover week-in-merges in the catalog search index
* style(registry): format the week-in-merges block
Wrap the lines oxfmt rejects so the format check passes.
* fix(registry): regenerate the week-in-merges catalog payload
The format pass left the committed payload off the generator output.
* feat(registry): cap week-in-merges at twelve rows
The block races at most rows_max authors, highest merge count first, and writes one line for everyone left out. The default is 12, so a long list cannot shrink the rows.
* fix(registry): make week-in-merges speed follow merge count
A row now travels the leader's pace times its merges over the leader's count. Only a zero stays on the floor, so a near-tie no longer looks like last place.
* feat(studio): audio meter faders for groups and master
* fix(studio): meter's mid band uses amber-500, the theme's actual token
yellow-500 was reset to nothing by palette-reset.css (--color-*: initial)
and never redefined in theme.css, so the mask never revealed any color
for -6 to -3dB; the token gate test already catches this and was red.
* refactor(studio): rename YELLOW_AT to AMBER_AT to match the amber token
* fix(studio): fader nudge and aria-valuenow match the visual dB position
* refactor(studio): use core StereoLevel for meter channel shape
* fix(core): emit levelTap in the packed core build
* fix(studio): name the preview fader Monitor and share clampNumber
* fix(studio): split the meter rAF tick and cover its branches
* style(studio): oxfmt AudioMeterStrip tests
* fix(studio): preserve authored gain in audio meter faders
* fix(studio): make timeline audio meters opt-in
* feat(studio): typed audio values, clip fades, audible video drops
Audio rows in the media inspector (Volume, Speed, Media start) take a
typed value: click the readout, type "-6 dB", "1.5x" or "2.5s", Enter.
Speed presets are hidden on <audio> clips.
Clip-edge fades: data-fade-in / data-fade-out in seconds, honoured by the
preview runtime (per-tick gain), the render mixer (afade after volume) and
Studio (Fade in / Fade out rows, and drag dots at the top corners of audio
clips and audible videos). Linear on both sides so preview and render
agree; fades that outrun the clip scale to meet inside it.
Dropped videos with an audio stream now land with data-has-audio="true"
instead of muted, so the mixer keeps their sound. The media metadata
endpoint reports hasAudio from the ffprobe call it already makes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(audio): one owner for fades, dB, and afade-after-volume
Illegal fade values become no-fade at fadeGain; typed inspector input
still returns null so the readout stays red. Master and group mixes
share one filter chain with volume then afade. Fade-out stays anchored
to the trimmed clip end.
* fix(studio): share audio helpers and bound typed media values
* fix(studio): reserve sibling fade duration when editing
---------
Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
* feat(cli): move media-use engine into the CLI
* fix(cli): replace media-use compatibility asset
* fix(cli): replace moved media-use tree
* fix(cli): copy moved media-use files cleanly
* fix(cli): copy media-use engine files individually
* fix(cli): keep audio helper compatibility import
* fix(cli): clear copied skill destinations
* test(cli): preserve moved media-use test paths
* fix(cli): preserve media-use script compatibility imports
* fix(cli): preserve media-use telemetry shim
* fix(cli): resolve packaged media-use LUT assets
* fix(cli): resolve packaged bundled SFX assets
* chore(skills): regenerate media-use manifest
* fix(cli): preserve media-use parity coverage
* fix(cli): cover relocated media-use tests
* chore(skills): regenerate media-use manifest
* fix(cli): format bundled media provider
* fix(ci): run relocated media-use tests with node
* fix(ci): preserve fallow script exemptions
* fix(ci): scope media-use fallow exemption
* refactor(cli): trim media-use essay comments
* feat(cli): index media-use assets for catalog search
* feat(catalog): commit media-use search vectors
* chore(catalog): track media vector metadata
* fix(catalog): validate media vector row order
* fix(catalog): validate media artifact freshness
* fix(catalog): cover media metadata and credits
* fix(cli): reject malformed media rows
* fix(catalog): compare complete media rows
* feat(cli): route media search through media-use
* refactor(cli): trim relocated media-use comments
* fix(cli): make media search runtime-safe
* fix(cli): preserve sfx fallback diagnostics
* fix(cli): probe sfx fallback provider explicitly
* chore(skills): regenerate manifest
* refactor(cli): keep typed media ranker source
* fix(cli): align media search checks with local-first flow
* fix(cli): scope vector audit exceptions
* fix(cli): ship resolver registry beside skill
* fix(cli): keep media-use skill scripts standalone
* test(cli): cover local vector validation
* test(cli): correct media vector fixtures
* fix(cli): prefer exact media keys
* fix(cli): stem exact media keys
* test(cli): skip media lint for snippets without media
* fix(cli): align skills test discovery
Keep the CI skills job's bash discovery as the single source of truth so its empty-list guard covers the exact files Node executes. Make hasMediaVectors private because it has no external callers and needs no Fallow suppression.
* fix(cli): close media-use review gaps
Prioritize literal ids before stemmed ids and cover the collision alongside the plural regression. Add command seam tests for engine discovery, actionable failure, and argv passthrough. Add a deterministic parity check for standalone media-use copies, with documented allowlists for the standalone media-fetch shim and self-contained npx resolver. Keep the bash-discovered skills test list as the CI source of truth.
* fix(cli): lower parity check complexity
Split missing-copy and unallowlisted-drift checks so the parity guard keeps its behavior without a threshold CRAP score.
* fix(cli): close media-use review seams
Run the parity test in the skills job so skills-only edits exercise the same check. Export the verb mapping and inject command dispatch for direct coverage. Keep missing-copy diagnostics separate from drift reads.