Commit Graph
104 Commits
Author SHA1 Message Date
Miguel Ángel 0e32ee9b56 ci: keep the bun cache warm and skip binary downloads in preflight (#6648)
* ci: keep the bun cache warm and skip binary downloads in preflight

* test(ci): pin the single cache writer and preflight's skipped downloads

* test(ci): keep the cache-writer scan out of the test body
2026-10-28 09:52:07 +00:00
Miguel Ángel d94132e609 ci: a release PR runs a fast release check instead of the heavy suites (#6524)
* ci: a release PR runs a fast release check, not the heavy suites

* ci: keep the release check's header within the comment limit

* ci: shorten the release check's usage line

* ci: trim the release check's comments

* ci: the release check's classifier hands the manifest test to a helper
2026-10-27 07:39:56 +00:00
Miguel Ángel 1819276fc3 test: real-Chrome and CI smoke pages inline GSAP so they wait on no CDN (#6406)
* test(studio): real-Chrome tests inline GSAP so their page waits on no request

* test(ci): smoke projects inline GSAP so required checks wait on no CDN
2026-10-26 08:20:55 +00:00
Miguel Ángel 603f4214ff fix: Remotion ports keep box sizes, the manifest hook runs after format, a drag test stops flaking (#6392)
* feat(skills): a Remotion port carries Remotion's box-sizing rule so bordered boxes keep their size

* chore(skills): regenerate the skills manifest after the format pass

* fix(hooks): the skills manifest is hashed after the formatter has run

* test(studio): a drag is measured against the box the preview shows

* test(studio): another element is read bare until it is selected
2026-10-26 07:20:03 +00:00
Miguel Ángel 35a28e913a ci: re-run changed test files to catch flaky tests before merge (#6339)
* ci: re-run changed test files to catch flaky tests before merge

* ci: give the flake gate the full test workspace setup

* ci: share the test workspace setup between the Test lanes and the flake gate

* ci: split the flake gate main for the fallow complexity gate and format

* ci: run node:test files with node, skip uncollected files, bound the gate's run time
2026-10-25 21:51:52 +00:00
Miguel Ángel 38afc0b013 fix(deps): update esbuild for the security advisory (#6277) 2026-10-25 04:52:58 +00:00
Poetry Of CodeandClaude Opus 5.5 9ac16b4aa1 fix: install git hooks in linked worktrees (#4387) (#4722)
The prepare guard used `test -d .git`, which is false in a linked
worktree where .git is a file, so lefthook install was silently skipped.
Use `test -e .git` so clones and linked worktrees both install hooks
while non-git directories remain a no-op.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:30:56 +00:00
Miguel Ángel 2faf660ac1 fix(ci): packed consumer installs keep same-day dependency bumps on every Bun release (#6059)
* fix(ci): packed consumer installs keep same-day dependency bumps on every Bun release

Bun 1.4.4 swapped the quoted package and range in its minimum-release-age
error, so the retry excluded the range instead of the package and the
install failed again. The package is now the token that bun's
"<name>@<range> failed to resolve" line pairs with the other one, and an
unrecognised message fails visibly instead of excluding the wrong name.

CI installs the newest Bun when nothing pins it, which let the 1.4.4
release turn every PR's support tests red mid-day. engines.bun in the
root package.json pins it for every setup-bun step that does not set a
version (packageManager would make corepack's pnpm refuse the repo).

* fix(ci): keep the release-age parser's doc comment to one line
2026-10-22 11:28:16 +00:00
WaterrrForeverandClaude Opus 5.5 f5873b8089 ci(skills): hide retired skills on ClawHub once the sync has run (#5656)
* ci(skills): hide retired skills on ClawHub once the sync has run

clawhub sync stops publishing a retired skill but leaves its last version
listed. A new step inspects each RETIRED_SKILLS name under heygen-com and runs
`clawhub hide --yes` with its retirement notice for the ones still listed. A
hidden skill reads as not found, so later runs hide nothing; a dry run only
names the skills still listed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(skills): import the retirement manifest at the top of the unlist script

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 08:49:29 +00:00
WaterrrForeverandClaude Opus 5.5 688229314f feat(skills)!: publish seven knowledge skills aligned with Studio and retire the workflow skills (#5651)
* refactor(skills): fold six domain skills into hyperframes and media-use guides

hyperframes-core, -animation, -keyframes, -creative and -studio move to
skills/hyperframes/{core,animation,keyframes,creative,studio}/, and
hyperframes-audio moves to skills/media-use/mix/. Each moved SKILL.md becomes
a frontmatter-free GUIDE.md so installers that scan recursively for SKILL.md
no longer see them as skills; the published list goes from 21 to 15.

Relative links, code paths and skill-name mentions are rewritten to the new
locations, the catalog surfaces, marketplace core-skills list, fallback core
set, module-copy targets and skills-manifest.json follow, and
`hyperframes skills update` now removes the retired skills (RETIRED_SKILLS)
when they are still installed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): slim the core entries and give media-use a HeyGen and a free path

The four entries every run opens are rewritten to what an agent cannot infer:
hyperframes 21.8 KB to 7.7 KB, hyperframes-cli 24.8 KB to 6.9 KB,
hyperframes-registry 9.9 KB to 4.3 KB, media-use 9.7 KB to 4.9 KB. Detail moved
whole into references (routing close calls and creator edits, CLI pin upgrades,
catalog search tiers, CLI conventions and feedback reports).

- hyperframes: state table, routing, the render contract, a terminal work loop
  (check, snapshot, preview before render), the desktop-app hand-off, and a
  table of the folded guides.
- media-use: one table of HeyGen versus free local tools per need; HeyGen
  sign-in, credits and the CLI's video-agent, avatar, clone, lipsync, translate
  and clipping commands; every free path works with no account.
- scripts/check-core-skills.mjs holds entries and descriptions to budget and
  checks every taught `npx hyperframes` command and path; runs in lint.
- Core entries are exempt from the formatter: its table padding cost ~1.5 KB
  per entry on every run.
- Content tests follow the text to its new home.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): plugin runs skip skills update; media-use explains HeyGen sign-in and TTS setup

The smoke eval showed a presenter request never reaching media-use and the
free voiceover failing on a missing kokoro-onnx; plugin runs also ran the
skills update the plugin rules forbid.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(skills): drop a committed Python bytecode file and ignore bytecode

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): align the entry with the Studio skills and keep the workflows' measured facts

The entry drops routing and the intent interview and carries the contract, the
editable-project rules and the work loop. Studio's format, runtimes, motion,
look and captions references come over for terminal use, frame presets move to
assets/, media-use gains Studio's mixing and local-command references, and
film.md and overlays.md keep what the workflow skills measured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills)!: retire the workflow skills and folded guides; publish seven knowledge skills

The skill pack now publishes exactly seven skills - hyperframes,
hyperframes-cli, hyperframes-registry, media-use, figma, slideshow and
remotion-to-hyperframes - carrying knowledge, not process. The eight
workflow skills, the folded domain guides and the routing/interview
references are deleted; their measured facts already live in the
hyperframes references (format, runtimes, motion, look, captions, film,
overlays) and media-use's mixing reference.

- bake-basemap.mjs and locate.mjs move to skills/hyperframes/scripts/
  (bake-basemap gains --help); extract-audio-data.py moves to
  media-use/audio/scripts/ for the caption motion reference.
- skills update: the eight workflows join RETIRED_SKILLS so existing
  installs prune them (by lock attribution only, since their names are
  generic); naming a retired skill reports where its knowledge went and
  succeeds instead of failing as unknown.
- Capture prompts point at the hyperframes skill's references/film.md;
  the live-map warning names the new bake-basemap path.
- Content tests that pinned knowledge are retargeted to the kept
  references; tests of deleted workflows, scripts and process go.
- Catalog surfaces, templates and docs describe the seven skills.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): resolve the local.md captions link and allow capture's output files in citations

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): regenerate the skills manifest from tracked files only

The committed media-use hash included an ignored __pycache__ bytecode file present
in the local checkout, so CI's clean checkout hashed differently.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(docs): restore generated catalog pages and docs navigation to main

Catalog PRs commit item sources only; CI regenerates the pages from the edited
registry sources. docs/docs.json is catalog navigation, so its Workflows group
rename is left for a non-catalog PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* build(cli): raise the CLI size budget for the skill references it bundles

The CLI bundles skills/hyperframes, which now carries the folded domain guides and
frame presets (155 KB to 1.08 MB in git). Budgets are the CI-measured size plus 5%:
packed 7526634, unpacked 22593213.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: keep repo-sweep guards meaningful after the workflow skills left

The last shell ffprobe caller and most root index.html examples lived in retired
skills. The shell detector is now pinned on a literal command line, and the
structure-rule sweep needs more than 5 roots (11 remain) rather than 30.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): slim figma, slideshow and remotion to the Studio shape; restore the storyboard board

slideshow 33.5 KB to 4.9, figma 18.2 to 6.2, remotion-to-hyperframes 11.0 to
4.9, each keeping the facts only it holds (the figma motion helpers, verify gate,
storyboard decoding and usage beacons; present and hand-off; the Remotion lint,
SSIM check and API map). The board page format from the storyboard recipe comes
back as hyperframes/references/storyboard.md: the desktop app's Storyboard tab
reads boards/storyboard.html by its frame, title and time classes. The entry and
the init template now say to snapshot the moments an edit changed before
replying, which the terminal eval found skipped in 22 of 38 runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): manifest without local bytecode; Studio's edit prompt names kept files

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): render when the person asks for the file or for no stops, not only after approval

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): keep the voiceover carve in media-use; presets stage their own fonts

Review follow-ups on #5651:
- carve.mjs and its tests move to skills/media-use/audio/scripts; the audio
  docs, mixing.md and two Studio comments point there.
- code-editorial's FRAME.md says to copy its bundled WOFF2s and add the
  @font-face blocks, instead of promising the retired build-frame.mjs.
- media-use states the host rule before the HeyGen sign-in steps.
- storyboard-format marks transition_in as a note the agent builds itself.
- Two stale citations (creator-editing-recipes.md, sub-compositions.md).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(skills): regenerate the manifest without local bytecode

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): the CLI render step follows step 4's rule, not approval alone

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ci): run the core-skills check from lint again after the main merge

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): films that tell a story start from three storylines; maps use real imagery

Opus 5.5 arm D read motion.md and applied it, but its launch film was a
feature list (pain, three feature cards, proof, logo): film.md left story
to the model. And the map case never opened film.md, so it drew Natural
Earth outlines instead of baking satellite tiles.

- references/story.md: the generic half of Studio's moment-film story and
  craft (one thing, three storylines with world and device, the hook,
  beats, worlds, copy, brand, tells of a generated film). The person picks
  a storyline, or the agent takes its recommendation when asked for the
  file or for no stops.
- The entry's work loop names the storyline step for films over ~15 s; the
  references table routes stories to story.md and maps to film.md.
- film.md points at story.md and gives maps and real places a section.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): story.md follows the entry: no voiceover unless asked

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): bake-basemap starts from a globe and stops waiting out its idle timeout

The Opus 5.5 map run spent 13 of its 38 minutes on parallel bakes that
stalled and 15 more recording 300 frames, after rewriting the script because
it could only start on a flat map.

- PROJECTION=globe with a seeded starfield and atmosphere, and FROM/PAN to
  turn the globe to CENTER before the dive.
- Each frame polls for loaded tiles instead of waiting for an "idle" event
  that had often already fired: 30 frames went from 198-219 s to 10-13 s
  locally with byte-identical frames.
- Progress prints seconds a frame and minutes left; film.md shows the globe
  call and says to run one bake at a time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): render in the foreground; wait on a detached render's log, never pgrep -f

Both Opus 5.5 explainer runs started a detached render, then waited with
`until ! pgrep -f "hyperframes render"`, which matches its own loop: each
render took 88-117 s and the wait sat for the tool's full 10 minutes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): route each kind of film; storylines carry look and sound; add review and sound

The entry routes by request (launch, explainer, music, data, vertical, 3D,
character) to a self-contained file under references/types/, and a film
over ~15 s now gets music and sounds on its visible actions unless the
person asked for silence. Each storyline names its look and its sound, so
picking one picks both; STORYBOARD.md frames carry `reads` and `sound`.

review.md is the whole-film pass: the check warnings, the review sheets,
seven questions to look for, P0/P1/P2 notes and REVIEW.md. motion.md gains
eases by role, six title entrances, a reading-time rule and nine measured
transitions; look.md a design plan before the first scene, with presets
kept for decks and named picks. story.md adds the second default that
generated design falls into, with what to do instead.

The blank template and format.md no longer teach the fade-and-slide-up
entrance on a dark canvas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(cli): check warns on unread lines, dead stretches, small type, empty frame 0 and silence

A coding agent cannot watch its film, so check now measures what a viewer
notices over time. A new `craft` section holds warnings only (exit 0
unless --strict), sampled on the dense 8 fps grid the overlap pass
already seeks, skipped for --range checks:

- line_unread: a reading-size line (>= 30 px tall at 1080) on screen
  under 0.4 s for 1-2 words, else under 3/4 of 0.5 s + 0.3 s a word;
  the film's last line needs 2 s. Short lines are re-measured between
  samples to ~0.01 s before judging. Captions-track and
  data-layout-ignore text are exempt.
- dead_stretch: more than 3 s where the keepsMoving liveness signature
  does not change, outside the final 3 s and any data-hold element;
  quiet when sweep_static fires or a whole-composition keepsMoving
  assertion owns the limit.
- text_below_floor: text under 2.6% of the short side (28 px on 1080)
  held 0.5 s, one finding per file listing every element.
- first_frame_empty: frame 0 paints only the background.
- film_silent: over 15 s with no unmuted audio or audible video.

Each message names the fix and the skill reference to read. keepsMoving
now also leaves the film's last 3 s (a still end card) out of its
static runs, so the two agree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(cli): snapshot --review captures a beat sheet, seam strips and a phone sheet

snapshot --review picks the frames that show a film over time instead of
evenly spaced ones, in the same single browser session:

- review-beats.jpg: one settled frame per scene at its start + 0.3 s,
  plus one per timed data-beat="name" element, labelled time and id.
  Scenes are the composition's top-level timed clips and sub-composition
  hosts, read like `timeline` reads rows (captions and audio excluded)
  and timed by the runtime's start resolver over the live page.
- review-seam-NN.jpg: frames at -0.2, -0.1, 0, +0.1, +0.2 s around
  every cut (each scene start, and any scene end no scene starts at).
- review-phone.jpg: the beat frames at 360 px wide.

--composition scopes the review to one sub-composition; with fewer than
two scenes the beats spread evenly and a note says so. snapshot also
gains --json: one envelope with every frame's path and time and, under
--review, the scenes, beats and seams behind the sheets; progress moves
to stderr.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): craft warnings stay out of --strict; a hold gets a real next action, not a caret

Craft findings (unread lines, dead stretches, small type, an empty first
frame, silence) are judgment calls a film may break on purpose, so
--strict keeps gating defects only. The check reference no longer
suggests a blinking caret to keep a still frame moving.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): review spreads beats when every scene starts together; map credits skip the type floor

A map film whose basemap and overlay both start at 0 s got one beat
frame; the even spread now keys on distinct scene starts. film.md tells
agents to mark the imagery credit data-layout-ignore and back it, since
the new type floor flagged it on the first real run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): review frames each scene at its middle too, not only 0.3 s in

On the first real explainer, 0.3 s into a scene showed a bare colour
field before its title entered. Each scene longer than about a second
and a half now adds a frame at its middle, so the sheet shows what the
scene builds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): a film is built from a numeric SPEC.md, with defaults banned by name and a look test

Studied in full: flxrnc's Motion Lab (vague vs directed prompt), 0xMovez's
course, EianLu's riso skill, agentchengfeng's style samples and the two
Opus 5.5 guides. Their common point: adjectives are where the default look
lives, so the agent now writes SPEC.md before any scene (GOAL, OUTPUT,
TIME, LOOK, MOTION, SHOTS, BANNED, QA), with a number for every adjective.

spec.md bans the defaults by name, unless the brand or reference uses
them (a dark void with small things floating, glow and particles, the
cream-serif-mono-label look, fade-and-slide-up, corner brackets), and
grows that list after the first render. Each storyline gets a 3 s test of
its hero shot before the pick; a look is written as the rules of one
medium. Composition numbers: the hero fills 40% of the frame, one hero
move per shot, the shot's information in within 2.5 s then a held poster.

motion.md adds springs by damping ratio, shorter exits, overlap, stagger
and the beat grid; review.md reviews as a harsh director and grows BANNED;
vertical.md takes the riso skill's measured safe band; explainer.md the
shapes of information.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): story before look; every object reads as itself; posters only for info cards

The first spec-driven C02 swapped the dark default for a flat Swiss
poster, and the story went with it: two black bars nobody read as mirrors,
slogans instead of cause and effect, a storyline picked for its picture.

The spec now serves the story: a storyline is picked for the story a
stranger would follow, the look test asks first whether a stranger
understands what they see, and SPEC.md gains an OBJECTS line giving each
thing the material and details that make it read without a label. The
2.5 s held-poster rule is limited to information cards; in a story film
every shot causes the next. Explainers start from a person or situation
the viewer knows. Review names every object as a stranger would and
treats one that needs explaining as a P0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): storylines are three films in words, each with a device and a form

The three storylines are pitched as text and the person picks from them;
nothing is built for a storyline before the pick, so the 3 s hero-shot
look test is gone. Ideas pair a device with a form, five or more first,
keeping the three that differ most, and the one stop states the rest in
an assumption line that names every paid item beside its free option.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: list main's new embedded-captions hf-cli test among the retired workflow files

The merge with main removed it with the rest of the retired skill, and the
deletion guard rejects any removal it has not been told about.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(core): keep the audio preset comment within core's size budget

The comment's new citation to media-use's mixing reference made the published
package 14 bytes larger than its budget, which sits at main's exact size.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): craft keeps its own 8 fps grid; comments back within the checks

Review on #5651 found that the merge moved craft onto the overlap pass's
times, which main caps at 180, so a brief line in a long film fell between
samples and line_unread missed it. Craft now builds its own grid at 8 fps up to
600 samples, as before the merge, and a 60 s film with a line shown for 0.2 s
pins it.

The Comments check passes again: REVIEW.md and STORYLINES.md join the files a
skill writes into a project, the comment lines this branch added come back
under each file's share at the merge-base, and the audio preset comment drops
a word the check reads as history.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 19:19:31 +00:00
Miguel Ángel b16d5a7063 fix(catalog): token-themed component previews no longer render black (#5995)
* fix(catalog): token-themed component previews no longer render black

Seven components read theme tokens with no fallback, so with no theme
pack they painted dark text on a black canvas, both when installed and
in their live Catalog preview. They now carry exact-value fallbacks from
their own demos, as the theme contract requires, and a test fails any
registry item that reads a contract token bare.

The Catalog's snippet preview wrapper also copies the tokens each demo
sets on its mount, and paints the demo's background, so the preview
wears the demo's theme.

* fix(catalog): trim the preview theme helper comment

* test(catalog): strip HTML comments with the parser in the theme fallback gate
2026-10-21 05:25:09 +00:00
Vance IngallsandClaude Sonnet 5.5 23e7a384b6 feat(registry): add echo-smear component (#5980)
* feat(registry): echo-smear engine and math tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(registry): echo-smear demo

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(registry): register echo-smear

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear copies inherit visibility and record fast

Final review fixes. Copies were forced visible and escaped a hidden scene.

A selector matching earlier copies re-targeted them, and copies kept data-start and data-duration.

A later attach re-drew every earlier copy at each recording sample.

Tests now run the engine through a fake gsap and timeline, and catch five seam mutations.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear skips targets that are not out of flow

Found testing on real blocks: transitions-push and blue-sweater animate static elements.

Their copies stacked in normal flow and pushed the scene 64800px down, rendering black.

The snippet now warns and skips a target that is not absolute or fixed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear group keeps the target's z-index

The wrapper group formed a new stacking context at z-index auto,

so a target that sat above a sibling dropped below it (the cursor went under the card).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear copies live in a sibling group, like motion-blur

The target is no longer wrapped, so it keeps its parent, blend mode and pointer events.

Static targets smear in place; the group mirrors the target's visibility and z-index.

Copies carry resolved styles, media inside is inert, and an element is attached once.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear records resolved transforms and leaves callbacks alone

Poses come from the resolved transform and opacity, so xPercent and any other transform survive.

Travel is measured on the element's own box, so ancestor-only motion draws no copies.

The recording sweep is quiet unless callbacks: true, survives a missing root, and releases its flag.

Copies share one style rule per target and are built to the peak count.

The demo is pinned to the snippet by a test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear copies follow the box, root and look of their target

Copies hidden at a box change never caught up, so a static target drew at the corner.

A shadow-root target threw on every seek; its rules now live in its own root.

Copies show the recorded filter, clip-path and individual transform properties.

No ResizeObserver, so a frame no longer depends on the first frame a worker drew.

Parent perspective, border-box sizing, media children and SVG children are handled.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(registry): echo-smear leaves real GSAP callbacks unfired at attach

Covers the blocker from review: a call or onComplete on the timeline fired during attach.

Runs against real GSAP 3.15 with a removal callback; it fails if the sweep seeks with events on.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear frames do not depend on the seek path

Group visibility and the copies' box are recorded in the sweep, not read live in the update.

A backward seek renders the update before the target, so the live read was stale.

Individual translate, rotate and scale are folded into the measured transform.

An element named twice in one call is attached once.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear resolves a percentage translate against the box

A computed translate of 150% was read as 150px, so count and copy placement were wrong.

Percentages are now a share of the element's own width and height; a scale percentage is a factor.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:00:43 -07:00
Miguel Ángel 434ad8c41c perf(cli): a repeat check starts with the CLI and Chrome already up (#5943)
* fix(cli): the browser the CLI launches exits with it, even on kill -9

* fix(build): the root build runs on Windows

* perf(cli): a repeat check runs in a process that already has the CLI and Chrome up

* fix(cli): a warm check delivers all its output before it exits

* fix(cli): the warm check only talks to a server that proves it is the user's

The warm socket now lives in a private, owner-verified directory (XDG_RUNTIME_DIR,
~/.cache or LOCALAPPDATA) next to a 0600 token. Client and server prove the token to
each other over fresh nonces before any request is sent, the request carries an
HMAC of the environment instead of the environment itself, and frames are capped.

* fix(cli): a warm check prints its stdout even after something went to stderr

The client stopped writing frames once any frame had been written, so a check that
printed a notice to stderr first lost its JSON report.

* test(cli): list the warm-check client fixture as a fallow entry instead of an inline ignore

* fix(cli): a check started while a warm one is busy runs cold at once on Windows

The warm server stopped listening when it accepted a client. On Windows the pipe name stays alive through the served connection, so a second client's connect saw a busy pipe and waited 30 s for a free instance before running cold. It now keeps listening while serving (accept already declines anyone else) and stops when the served check finishes.

* test(cli): the orphan test reads the check's own Chrome and watchdog instead of racing a poll

On Windows each process listing is a PowerShell call of about a second, so a check that finishes quickly could start and close Chrome between two polls, and the test failed its own 'Chrome was seen' precondition. A fixture now records the Chrome tree and the watchdog from inside the check when its first page opens.
2026-10-20 19:09:11 +00:00
Miguel Ángel 5583ae4293 perf(cli): commands run without esbuild, puppeteer-core or sharp until they need them (#5930)
* perf(cli): commands run without esbuild, puppeteer-core or sharp until they need them

A host that bundles the CLI can now leave these packages out: each command
loads them only where it uses them, and sharp installs once on first use when
it is missing. The build check fails on any static import of them again.

* fix(cli): use a host's sharp from NODE_PATH and let hosts turn first-use installs off

* fix(cli): capture fails when sharp is missing, and one install serves every caller

A missing sharp now stops the asset download up front instead of counting every
image as unavailable. Concurrent first uses share one install, a failed install
is not retried in the same run, and preview thumbnails report why. Hosts can turn
off installs for a list of packages, e.g. HYPERFRAMES_NO_OPTIONAL_INSTALL=sharp.

* fix(cli): snapshot --describe still works without sharp, resizing only when it is installed

* style(cli): format the sharp loader and its tests

* build(cli): sharp is an optional dependency, so a host can ship the CLI without it

* build(studio-server): keep sharp out of the bundle now that it is optional
2026-10-20 09:12:47 +00:00
Miguel Ángel aa63a8c70d chore: stop the fallow audit hook from piling up base snapshots (#5882)
* chore: stop the fallow audit hook from piling up base snapshots

Upgrade fallow to 3.34, which reuses one base snapshot per checkout,
and prune snapshots unused for a day with its own audit-cache command.
The hand-written prune script and its test go away. Also drops the
unused concurrently devDependency and moves yaml, imported by a root
script, to dependencies, the two findings 3.x reports on main.

* chore: allow deleting the replaced fallow prune script
2026-10-19 14:12:46 +00:00
Miguel Ángel a403ea1e8b ci(studio): re-run an edit accuracy shard on a fresh runner when its runner is too slow (#5659) 2026-10-15 17:43:09 +00:00
James RussoandClaude Opus 5.5 f55b366175 fix(build): ship NodeNext-resolvable declarations from every package (#5532)
* fix(build): ship NodeNext-resolvable declarations from every package

tsc copies relative specifiers verbatim, so packages whose sources use
bundler-style extensionless imports (core, inspector) published .d.ts files
that a module/moduleResolution nodenext consumer cannot resolve. Core already
rewrote its emitted JavaScript for this, but not its declarations.

Move that rewrite to scripts/rewrite-relative-import-extensions.mjs, make it
cover .d.ts too (adding .js or /index.js, and failing the build when a
declaration import has no declaration), and run it after every tsc emit:
core, engine, sdk, producer, aws-lambda, gcp-cloud-run and inspector.

verify:packed-manifests now typechecks the clean packed consumer with
skipLibCheck false and type-only imports of every typed subpath, so an
unresolvable import inside a published declaration fails the gate and is
attributed to its package.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(ci): allow the moved core rewrite-esm-extensions script deletion

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: rebuild the edit-accuracy bench when the import-extension rewriter changes

The package builds now run scripts/rewrite-relative-import-extensions.mjs, so main's
edit_accuracy paths filter has to include it (check-edit-accuracy-filter test).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 05:59:41 +00:00
James RussoandClaude Opus 5.5 9a2c1c6e80 ci(studio): run the edit accuracy slice on PRs and the full grid in the merge queue (#5566)
* ci(studio): run the edit accuracy slice on PRs and the full grid in the merge queue

The edit accuracy gate used about 83% of CI job-minutes, ran on changes that
cannot reach the bench (encoder, render and CLI command code), and never ran
in the merge queue, so main could drift and fail every later PR.

- Path filter: its own paths-filter step with some-with-excludes, limited to
  what the bench runs: Studio, studio-server, core, parsers, player, sdk, lint,
  the CLI preview server path, and the engine and producer modules in the
  import closure of browserManager, frameCapture and both fileServers. Test
  files, fixtures and docs no longer trigger it. The bench's webmcp-host.mjs,
  which it imports but the old filter missed, now does.
- merge_group: the filter's base is the event's base_sha there. In the queue
  the full grid always runs (20 shards) whenever the filter matches. The queue
  is the last point before main, and nothing else measures the full grid
  now that PRs run a slice. Skipping it would let zoom 50/200 and rotated
  regressions land on main, where they fail every later PR against the
  banked baseline. That drift caused about a fifth of past gate failures.
- PRs: the existing `pr` grid becomes zoom 100 with no rotation in both
  nestings (435 of 2061 cases), in 5 shards. Of the 11 PRs whose real
  regressions the full gate caught, this slice held a regressed case for 10.
- Gate: newly passing cases missing from baseline.json are listed in the
  comment and as a notice, not failed (45% of past gate failures). A partial
  grid compares only the cases it ran. `--shards` fails the gate when a shard
  reported no results, and only a full grid writes a bankable baseline.json.
- The required check "Studio: edit accuracy gate" keeps its name. It is
  skipped when the filter does not match, as before.
- check-test-reachability models negated globs as exclusions under
  some-with-excludes and still fails closed elsewhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(studio): run the edit accuracy gate when its install inputs change

The path filter missed the root package.json, the dependency install script
and the ffmpeg setup action, which Build and every shard run. A change to any
of them could alter the bench while the gate skipped. Add them, and a test that
every local input of the jobs that build and run the bench is in the filter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(edit-accuracy): cover transitive bench inputs in the path filter

The filter test now walks the files the bench actually runs: the bench
entry files, their relative and @hyperframes/* imports, the CLI commands
the bench invokes, and the build scripts, configs and files those builds
read for every package the bench reaches. Globs are matched with
picomatch, the matcher dorny/paths-filter uses.

The walk found 183 inputs the filter missed, including the producer's
frameCapture.ts, build-hf-early-stub.ts and stubs/hf-early-stub.ts, the
engine services behind engine/src/index.ts, and the CLI browser command.
The filter now covers them.

picomatch is not in the no-install Test reachability job, so the filter
test moves to test:scripts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 01:45:40 +00:00
Xuanru Li f31b3c982b build(inspector): publish @hyperframes/inspector to npm (#5530)
* build(inspector): publish @hyperframes/inspector to npm

* fix(inspector): drop prepack so pnpm pack --json stays parseable

* fix(inspector): add .js to published declaration imports for NodeNext

* fix(inspector): declare @hyperframes/parsers, which the published types import

* chore(fallow): ignore @hyperframes/parsers, which inspector's published types import
2026-10-13 04:55:48 +00:00
Vance IngallsandClaude Opus 5.5 071ce8044f feat(registry): add filmed-screen component (#5487)
* feat(registry): add filmed-screen component

Films a live-DOM scene like a camera pointed at a monitor: a tilted screen
plane in perspective, depth of field that follows a named element, chromatic
fringing, LED sub-pixels, per-shot palettes and cut tears. The focused element
stays sharp. Ported from ff-tracking's lens.sksl, with each colour channel
weighted by its own sample in the bokeh average.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(registry): land the filmed-screen catalog poster on the title

The catalog poster is captured at min(3 s, 0.6 × duration), which was the
demo's first cut, so it showed a tear over a blurred title. The first cut
moves to 3.8 s and the first shot racks back to the title before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(registry): filmed-screen glides between shots and racks focus

Shots now glide into each other by default: camera, palette and colour mode
ease over `glide` seconds, and `transition: "cut"` keeps the hard cut with its
tear. Focus racks instead of snapping: the focus box is a Hann-weighted
average of where it was over the last `rack` seconds, so a new target eases in
on an S-curve and every seek order still renders the same frame.

The demo now films three scenes in the style of other compositions, a code
editor, an analytics card and a chat thread, one shot and grade each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(registry): filmed-screen shares the composite slot and films each instance

Review on #5487:

- The composite hooks no longer cycle with data-vfx-chain. Both re-arm every
  seek, and core's resolver reads its prior when called, so a stale wrapper
  of ours is re-entered through it. Each wrapper now keeps its own prior and
  returns at once when re-entered, so the chain stays a constant depth. The
  slot is only claimed once a lens is mounted.
- A second instance films its own canvas. mount() takes the snippet's
  caller-scoped query result and picks the first canvas not already filmed;
  the library's own document belongs to whichever copy loaded first.
- No WebGL2 now steps aside like no HTML-in-Canvas. mount() returns
  { destroy() }, a lens whose canvas leaves the page is dropped, and a lens
  whose composition is not on screen skips its capture instead of logging.
- Docs: fadeOut and zIndex, the rack comment, and the description and tags
  now describe glides instead of cuts.

filmed-screen.test.mjs runs both arm orders against core's arming pattern,
two mounts over two canvases, and that the demo pastes the snippet's library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(registry): filmed-screen test passes oxlint

The vfx stand-in returned `true || prior`, which oxlint rejects as a
constant-truthiness expression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(registry): match script tags case-insensitively in the filmed-screen test

CodeQL reads the extraction regexps as HTML filters (js/bad-tag-filter).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(registry): filmed-screen waits for the scene's first paint instead of erroring

In Studio's preview the first draw can run before Chrome has painted the
layoutsubtree scene once (the paint wait gives up after 250 ms on a busy
machine). drawElementImage then throws "No cached paint record", which was
logged with console.error and surfaced as a preview console error. That
error is now a wait: the lens requests a paint and draws again when it
lands. Other capture failures still log.

Render output is unchanged: the demo renders byte-identical to before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:56:38 -07:00
Miguel Ángel 9825614d91 fix(studio): Studio's own text reads without em or en dashes (#5414)
* fix(studio): text in Studio reads without em or en dashes

* fix(studio): the dash check also catches HTML entities, and peak reads Unavailable

* test(studio): the dash check test covers every entity form

* fix(studio): messages Studio shows from lint, sdk, core and the renderer drop dashes too

* fix(producer): the gate check stops early on the engine's new fallback wording
2026-10-10 19:08:30 +00:00
Miguel Ángel 0ca99b30da fix(ci): skills sync publishes hyperframes-creative past its hidden 1.0.13 and checks it went live (#5081)
* fix(ci): skills sync publishes past a version the registry holds unpublished

* fix(ci): skills sync fails unless a republished skill is the registry's latest

* fix(ci): skills sync republishes one patch past a hidden version and checks it as owner

* test(ci): skills sync fails the job on an error and stops after a clean sync

Two mutants survived review: dropping the line that sets the exit code (the job would print its ::error:: lines and still pass) and dropping the clean-sync return. One test runs the script with a failing fake clawhub on PATH and expects exit 1; another pins that a clean sync makes no further call.

* test(ci): the exit-code test can only reach its fake clawhub

The child's PATH was the fake's folder plus the inherited PATH, so on a machine where the fake cannot execute, a real logged-in clawhub further down would run sync for real. PATH is now the fake's folder alone.

* fix(ci): skills sync skips only hyperframes-creative's hidden 1.0.13

Republishing one patch past any taken version would publish past every future hidden version with no one looking. Only the acknowledged case (hyperframes-creative, 1.0.13 taken) is published at 1.0.14; any other taken version fails the sync as before.

* test(ci): the skip allowlist matches the skill, not just the version

A taken 1.0.13 on any other skill must fail the sync too; without this case, accepting 1.0.13 for every skill passed all tests.

* refactor(ci): skills sync picks the republish version in its own helper

The allowlist check took republishTaken to cyclomatic 5, which the Fallow audit scores at its 30.0 CRAP threshold; deciding the version in versionPastSkippable keeps each function under it. Behaviour unchanged.
2026-10-07 01:46:15 +00:00
Miguel Ángel f835f83164 fix(catalog): 3D previews load their scripts under the docs host's script policy (#5090)
* fix(catalog): 3D previews load their scripts under the docs host's script policy

* fix(catalog): preserve script text through HTML parsing

* docs(catalog): describe the shared script bundle
2026-10-05 15:52:51 -07:00
Miguel Ángel d2398d686a feat(registry): eight ad templates show their video preview in the catalog (#4979)
* feat(registry): eight ad templates show their video preview in the catalog

* refactor(scripts): the preview backfill reuses the shared entrypoint guard
2026-10-04 01:27:10 +00:00
Miguel Ángel d218cec091 ci(studio): gates read the base the run measured, and drag frames on a quiet runner (#4872)
* ci(studio): measure drag smoothness on a quiet runner, with a blank-page control

* ci(studio): gates read the base the run measured, not the base branch tip

* style(ci): format the base-file test
2026-10-01 16:35:49 -07:00
James Russo 0a5e3c7fc4 feat(plugins): ship portable agent bundles and installation guides (#4608)
* feat(plugins): ship portable agent bundles and installation guides

* fix(plugins): align new manifests with current release

* test(plugins): split caption fixture setup and cleanup

* test(plugins): pass render fixture path as an argument
2026-09-27 21:58:51 +00:00
Miguel Ángel 2b1bb8b731 test(cli): CLI tests run with a home folder of their own, never the user's (#4587)
* test(cli): CLI tests run with a home folder of their own, never the user's

* style(cli): format the test home check

* test(cli): the skills test lane also runs with a home folder of its own

* style(cli): format the test home check

* ci: the test reachability map follows the skills lane's new command
2026-09-27 12:30:03 +00:00
Miguel Ángel c238abfe6f fix(media-use): tests never write into the person's media library (#4459)
* fix(media-use): tests never write into the person's media library

* fix(media-use): guard the media home only in this repo's test runs

* test(media-use): probe the media home from a plain child process

* fix(media-use): keep miss logging best-effort when home is unresolvable

* test(media-use): miss logging survives an unresolvable media home

* docs(media-use): describe the media library without naming an app

* chore(media-use): refresh the skills manifest hash
2026-09-26 14:13:56 +00:00
Miguel Ángel 81d8a03d2e fix(repo): keep one fallow audit base cache per checkout instead of one per main commit (#4525) 2026-09-26 07:36:06 +00:00
Miguel Ángel fbaa9f9205 ci: stop running CodeQL on merge queue groups, where its upload races the merge (#4492)
* ci: stop running CodeQL on merge queue groups, where its upload races the merge

The code scanning rule does not apply to merge queue groups, and the queue deletes
a group ref when it merges, so the upload failed on about half the groups.
Pull requests and pushes to main keep scanning.

* ci: catch merge_group in any trigger form and say the upload race is intermittent
2026-09-25 16:50:55 +00:00
Miguel Ángel 0e34d69641 ci: comment checks grade branches cut before their scripts existed (#4466) 2026-09-24 23:28:36 -04:00
Miguel Ángel 3520c8225c ci: a package file's comment share may not rise (#4445) 2026-09-24 22:06:50 -04:00
Miguel Ángel 3194c2ae87 ci: grade the comments each PR touches (#4444) 2026-09-24 20:26:15 -04:00
Miguel Ángel 096a586b41 ci: editing a PR's text no longer cancels its running checks (#4420)
* ci: editing a PR's text no longer cancels its running checks

CI, regression and Windows render re-run on pull_request edited (for base changes) with cancel-in-progress, so a body or title edit cancelled the running checks and the fail-closed Test aggregator went red on the cancelled run. Only a push or a base change cancels now; a text edit queues behind the running run.

* test(ci): read the workflows folder by path, not as a module URL

Fallow treated new URL('../.github/workflows/', import.meta.url) as an unresolvable import. Use import.meta.dirname and join, as scripts/check-skill-mirror.mjs does.
2026-09-24 11:19:00 -04:00
d49e7b95e0 feat(media-use): resolve brand logos from theSVG (#4291)
* feat(media-use): resolve brand logos from theSVG

Replace the svgl and simple-icons tiers of the `logo` cascade with a
single theSVG (thesvg.org) tier. theSVG ships 7,400+ official brand
SVGs with color, mono, light/dark and wordmark variants.

The tier fetches one manifest pinned to a glincker/thesvg commit on
jsDelivr, memoizes it per process, and matches entities locally by
normalized slug, title, or alias. That drops the hand-kept svgl alias
and simple-icons slug maps. GitHub org avatar and favicon stay as the
fallbacks for brands theSVG lacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(media-use): sanitize fetched SVGs and gate theSVG by license

freezeUrl/freezeLocalFile wrote every fetched SVG to disk unsanitized;
the theSVG tier is now the sole SVG-returning logo provider, so an
inline script tag or on-event handler would land verbatim in a user
project. Route .svg destinations through the existing Figma sanitizer
(new @hyperframes/core/sanitize-svg subpath, synced via
package-subpaths).

thesvgMatch never checked theSVG's own license field: entries theSVG
itself marks unresolved (Proprietary, Unknown, Custom, TODO, or an
explicit no-redistribution-license string) could still resolve and
freeze into a project. Gate matches to the ten SPDX ids theSVG's
submission form accepts; a rejected entry falls through to a lower
ranked theSVG match, then to the github/favicon tiers.

Also caps the theSVG manifest fetch at 20MB, streamed, matching
freeze.mjs's existing cap discipline. It had a timeout but no bound.

* chore(skills): regenerate skills-manifest for media-use/motion-graphics

The original submission edited skills/media-use and
skills/motion-graphics docs without regenerating skills-manifest.json;
its own CI never ran (fork approval gate), so the drift went
unnoticed until this pass's first CI run caught it.

* fix(media-use): drop the sanitizer's workspace import, add parity test

freeze.mjs importing @hyperframes/core/sanitize-svg broke the
test-skills CI lane: packages/cli/src/media-use tests run with node
builtins only, zero install, by design (media-use is vendored
standalone into other projects, e.g. packages/desktop). Port the
sanitizer into media-use/lib/svg-sanitize.mjs instead, and drop the
now-unused core export.

Parity between the port and packages/core/src/figma/sanitizeSvg.ts is
enforced by a new test (scripts/check-svg-sanitize-parity.test.mjs,
run in the test:scripts lane, which does have an install) over a
hostile-SVG corpus, so the two cannot drift silently.

* fix(media-use): close three sanitizer bypasses, add fetch timeout

Independent review of the maintainer pass found the SVG sanitizer
(packages/core/src/figma/sanitizeSvg.ts, reused by media-use's own
port) was itself bypassable: an unquoted href/xlink:href survived,
inline style="..." attributes were never checked, and a href bound
through a renamed XML namespace prefix (xmlns:x=".../1999/xlink",
x:href=...) slipped past the literal "xlink:href" match.

Rewrote sanitizeSvg as a tag/attribute tokenizer instead of a set of
literal-spelling regexes: parse each tag's attributes in any quoting
form, resolve the local name after stripping any namespace prefix,
then drop by local name (any on* handler, any *href whose value is
not a #fragment or data:image/ embed, any style attribute containing
url(, @import, expression( or javascript:). Ported the fix into
media-use's svg-sanitize.mjs identically. Legitimate url(#id)
references on non-style attributes (clip-path, fill) are untouched.

Also adds a 10s AbortSignal.timeout to freezeUrl's fetch, matching
every other fetchMedia call in this package - it had a size cap but
no deadline.

New cases for all three bypasses in both sanitizeSvg.test.ts and the
parity corpus, each reproducibly failing against the prior head.

* fix(media-use): replace regex SVG sanitizer with a DOM allowlist

Two rounds of regex/tokenizer patches to sanitizeSvg found new bypasses
in the same shape each time (unquoted href, style= url(), aliased
xlink prefix; then presentation-attribute url(), unquoted '=' handling,
SMIL animate/set href rewrite, self-closing-slash tag corruption,
data:image/ navigation via <a>). Denylisting attribute names against a
hand-rolled tokenizer was the wrong approach regardless of how careful
the regex.

Rewrote sanitizeSvg (packages/core/src/figma/sanitizeSvg.ts) as a real
DOM walk (linkedom, already a dependency of core and cli) with an
allowlist: known-safe elements and attributes only, everything else
dropped by construction rather than pattern-matched as dangerous.
href/xlink:href (any namespace prefix) kept only for a same-document
fragment, or data:image/ off non-<a> elements. Any attribute value or
<style> text containing url() kept only when every url() argument is a
fragment; @import/expression(/javascript: dropped from style content.
Serializing from a real DOM means output is always well-formed, so the
self-closing-slash corruption class of bug is gone by construction.

media-use's own copy (packages/cli/src/media-use/lib/svg-sanitize.mjs)
can't import linkedom directly: its CI test lane runs bare `node --test`
with no install step, so even an external dependency fails to resolve,
not just a workspace import. It's now a generated file instead of a
hand-copy: scripts/generate-media-use-svg-sanitize.mjs bundles the core
source (esbuild, minified, zero remaining imports) and
scripts/check-media-use-svg-sanitize-generated.test.mjs fails the build
on drift. Verified the generated file runs with plain node in an empty
directory with no node_modules on the path.

Every payload from both independent-review reports is now a
table-driven test in sanitizeSvg.test.ts (46 cases) and the same corpus
in check-svg-sanitize-parity.test.mjs (39 cases) asserts core and the
generated media-use copy agree and each independently rejects every
non-local url(), javascript:, data: href on <a>, and on* handler.
Mutation-proved against the real pre-fix blob, not a synthetic case.

Sampled 56 real theSVG icons across all 7 collections to check the
allowlist doesn't over-strip legitimate content: found and fixed three
missing attributes (fx/fy, enable-background, a blanket data-*
allowance) and one real behavioral bug - <style> elements were being
dropped outright, silently unstyling any Illustrator-exported logo that
colors paths via CSS classes. Fixed by keeping <style> on the allowlist
and content-checking its text the same way as an inline style=
attribute, with a regression test using that exact shape.

Full suite after the fix: sanitizeSvg.test.ts 46/46, parity+drift tests
39/39, media-use suite 298/298, tsc/oxlint/oxfmt clean.

* fix(lint): ignore generated svg-sanitize.mjs bundle in oxlint

Minified esbuild output (packages/cli/src/media-use/lib/svg-sanitize.mjs,
generated by scripts/generate-media-use-svg-sanitize.mjs) trips oxlint's
correctness rules 504 times and fails `bun run lint`. Same shape as the
existing registry/blocks/**/*.iife.js exclusions: a generated, minified,
non-hand-edited bundle, not hand-written source.

* fix(format): ignore generated svg-sanitize.mjs bundle in oxfmt

Same generated esbuild bundle that needed an oxlint exclusion in the
previous commit also fails  (minified, no formatting to
check against), cascading Preflight into Player perf, regression,
Windows render verification, and preview-regression on CI. Same
established pattern as the existing registry/blocks/**/*.iife.js and
skills/**/*.min.js entries in .prettierignore.

* refactor(media-use): reduce complexity flagged by fallow audit

isSafeAttribute (10 cyclomatic) and the parity test's aggregate-marker
check (11 cyclomatic, 26 cognitive, CRAP 132) both exceeded fallow's
per-function thresholds after the rebase pulled in a stricter baseline.
Extracted isXmlnsAttribute/isKnownAttribute as named predicates and
survivingUnsafeMarkers as a named per-output helper; no behavior change,
same guard order, same failure strings. Regenerated svg-sanitize.mjs.

* refactor(media-use): make the parity test's marker check table-driven

Fallow gives .mjs scripts no coverage credit, so its CRAP gate needs each
function at cyclomatic 4 or less. The extracted survivingUnsafeMarkers
still scored 9 (CRAP 90). The four regex markers are now a table, and the
data:image-on-<a> and non-local url() checks are small named helpers.
Same checks, same order, same failure strings.

* fix(media-use): close CSS-escape and image-set bypasses in sanitizeSvg

The url()/@import checks scanned raw text for the literal "url(" and
"@import". A browser decodes CSS escapes first, so fill="\75 rl(...)",
url\28...\29 and @\69mport all loaded an external resource, and
image-set("https://...") loads one with no url() at all. Reproduced in
headless Chrome: 8 of 8 payloads fired a request through the old code.

CSS can only load a resource through a function or @import, so CSS text
now passes only if it has no @import, every function is on a compute-only
allowlist (color, transform, filter and math functions), and every url()
is a same-document fragment, all checked on escape-decoded, lowercased
text. Same fix reaches media-use through the regenerated bundle.

Checked against all 13,384 files of the pinned theSVG corpus: 13,383
sanitize byte-identically; one drops an unreferenced id="XRP-(XRP)".
The parity oracle now fails on any surviving attacker host, so it no
longer shares the sanitizer's parsing blind spot.

* refactor(media-use): share one capped body reader for freeze and manifest

fetchJson copied freezeUrl's declared-length check and streaming size cap
line for line. Both now call readCappedBody in media-fetch.mjs, which both
already import; it has its own tests, since freeze's cap had none. The
freeze cap errors now read "freeze failed for <url>: ..." (no caller or
test matches the old wording). Also fixes freeze.mjs's comment, which
still called the sanitizer a lexical pass.

* style(media-use): format the parity test corpus

* fix(media-use): make the sanitizer's CSS scans linear on hostile input

The url() check used /url\(\s*(['"]?)([^)]*)\1\s*\)/ and the function
check /([-\w]*)\(/: on attacker padding these backtracked cubically and
quadratically (an unclosed url( plus 4,000 spaces took 20s; an 80,000
character word took 7.7s), and freeze runs the sanitizer synchronously on
bodies up to 256MB. Both are now single-pass indexOf scans (a million
characters in under 15ms), with the same verdict on ordinary input. A url()
value holding "(" is rejected, so a nested url( can't hide inside one.
A size test holds both payloads under 2s.

* test(media-use): size payloads that fail the old scans fast instead of hanging

* fix(media-use): stop the freeze timeout from aborting long downloads

The 10 s AbortSignal.timeout on freezeUrl also aborted the response body,
so any stock video, generated video or LUT that took longer than 10 s to
download failed. The timer now covers only the wait for response headers
and is cleared once they arrive; the 256 MB cap still bounds the body.

* refactor(media-use): trim logo-provider comments to the repo limit

The tier list and the thesvgMatch doc block ran past the four-line comment
cap that the merge check enforces. Same facts, shorter blocks.

* refactor(media-use): name the header wait and restore the variants note

Renames the freeze timeout controller to headerWait so it no longer reads
as HTTP headers, puts the theSVG variants fact dropped by the comment trim
next to the line that picks the default mark, and keeps thesvgMatch on the
file's JSDoc style.

---------

Co-authored-by: Imad Phelps <imad.phelps@heygen.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-09-23 16:56:16 -04:00
Miguel Ángel ca4af78184 ci: dispatch required checks for catalog publication (#4345) 2026-09-23 13:15:38 -04:00
Miguel Ángel b1cf88aeba ci: generate catalog artifacts through a standing publish PR (#4337)
* ci: generate catalog artifacts through a standing publish PR

* fix(catalog): reconcile obsolete publication snapshots

* refactor(catalog): report failures after staging cleanup

* refactor(catalog): separate generation and publication decisions

* fix(catalog): unpublish deleted items with their sources

* docs(catalog): keep scaffolding guidance concise
2026-09-23 12:09:46 -04:00
Miguel Ángel 2c71a3a112 fix(catalog): registry set-delta check accepts intentional additions (#4287)
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.
2026-09-22 11:24:51 -04:00
Miguel Ángel 05d09a7452 chore(registry): remove low-use catalog items and variant tails (#4274)
* chore(registry): remove low-use catalog items and variant tails

* fix(registry): remove stale references to deleted catalog items

* style: format catalog fixture

* fix(ci): use surviving catalog preview canary

* chore(ci): allow intentional catalog deletions

* fix(ci): scope fallow audit for catalog fixtures

* chore(catalog): regenerate local search vectors

* chore(catalog): update vector revision

* fix(catalog): validate registry set delta
2026-09-22 07:04:54 -04:00
Miguel Ángel cff467a352 ci: select the remaining orphan tests in their owning runners (#4260)
* 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.
2026-09-22 00:54:41 -04:00
Miguel Ángel 86e27b00f6 ci(studio): ratchet colour contrast of timeline tokens (#4257)
* ci(studio): ratchet colour contrast of timeline tokens

* refactor(studio): clarify contrast ratchet branches and state citations

* fix(studio): resolve contrast baseline from the repository root

* test: parse contrast channel tokens without color wrappers

* ci: keep contrast gate artifacts outside runtime sources

* test: honor strict indexed access in contrast scripts

* test: require removal of zero contrast baseline entries
2026-09-21 21:35:23 -04:00
Miguel Ángel 3091dcb787 feat(cli): route media search through media-use (#4230)
* 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.
2026-09-21 18:35:47 -04:00
Miguel Ángel f24ff83834 feat(catalog): add the seven 3D-motion pieces as installable registry blocks (#4056)
* feat(catalog): add the seven 3D-motion pieces as installable registry blocks

Each piece ships as a registry block with a CDN poster and preview, a live
catalog detail page and its source. Scripts travel inside the catalog payload
(shared vendor libraries fetched once, item scripts inline, the one HDR as a
data URI) because the docs host only serves fonts, images and JSON.

NO_ESSAY_COMMENTS_OK: the long comment blocks are in vendored libraries and
the ported frost source under registry/blocks, not hand-written here.
HF_MAX_NONLFS_KB=6000: four hosted texture PNGs under docs/public/catalog/items
are runtime assets the docs host must serve as files.
fallow skipped locally: it audits against origin/main and this branch is
stacked on an older main.

* chore(catalog): regenerate gallery data and nav after rebasing onto main

* fix(catalog): read the install manifest through the bounded project reader

* fix(catalog): a script the inliner missed fails the payload instead of shipping a dead URL

* refactor(catalog): the leftover-script check reuses the reference finder

* fix(catalog): frost and glass textures load from the CDN and the iteration presets are gone

* chore(catalog): regenerate the frost and glass payloads and nav

* fix(catalog): frost and glass textures are hosted through the registry url field, not hardcoded

* chore(catalog): regenerate the frost and glass payloads against the hosted texture URLs

* fix(catalog): the hosted-reference rewrite refuses a bare name it cannot safely replace

* ci(catalog): verify changed catalog payloads in headless Chrome and check the docs catalog

* feat(catalog): every item page follows the shared layout with a Tune panel and tabs

* fix(catalog): let the texture-mask item host its masks so its tile plays live

* chore(catalog): regenerate item pages, gallery data and the texture-mask payload

* chore(catalog): regenerate item pages after merging main

* fix(catalog): declared-only variables get the Tune panel, Code and Docs tabs capped

* feat(catalog): give the blend-difference caption a demo scene so its tile plays live

* chore(catalog): live payload for the blend-difference caption

* perf(catalog): heavy 3d pieces compile and draw before ready, not on the first visible frame

* feat(catalog): the avatar promo card hosts its videos, audio and fonts on the CDN

* feat(catalog): the avatar promo card tile plays live from its CDN-hosted media

* chore(catalog): the avatar promo card plays live, no allowlist left

* chore(catalog): shorten two comment blocks

* chore(catalog): trim first-party comment blocks to the comment-check limits

* chore(catalog): shorten two frost source comments

* fix(catalog): the Docs tab renders as prose again, and the detail page honours reduced motion

Frame-mode pages carry no prose wrapper, and the detail component sat inside
not-prose, so the Docs slot's headings, paragraphs, links and lists had no
styling at all. The slots now carry their own prose classes and the chrome
around them keeps not-prose.

The recorded clip also gets the reduced-motion guard the docs-snippet check
requires, and the generator reads stability from the source metadata type so
the scripts typecheck passes.

* fix(catalog): docs-tab paragraphs render as blocks

Mintlify emits paragraphs as inline spans, so two paragraphs in the Docs slot ran together.

* fix(ci): drop the duplicate docs_catalog job output

git's line-based auto-merge silently duplicated the outputs key when
main and this branch each added it on non-adjacent lines during the
rebase; oxfmt's yaml parser rejects the repeated map key.

* fix(catalog): close four CodeQL alerts in the frost source

* chore(catalog): allowlist the avatar-promo-card CDN asset deletions, format sync-docs-catalog

check-no-main-deletions.mjs was rejecting 11 local mp4/font files this branch
already deleted in favor of CDN hosting; scripts/sync-docs-catalog.mjs had one
line over the 100-col print width from an earlier edit in this branch.

* fix(catalog): detail tabs use docs prose spacing, Install first; previews skip no-WebGPU runners

* fix(catalog): the docs tab scrolls with the page instead of a capped inner box

* refactor(catalog): split the payload asset and verify scripts into single-purpose helpers

* refactor(catalog): split the preview generator into single-purpose helpers

* ci(catalog): fail when the committed catalog differs from generator output

The docs host serves the committed catalog and nothing regenerates it, so a stale
copy shipped silently. The catalog payloads job now generates into a temp dir and
compares it with the committed tree.

* chore(catalog): regenerate docs/public/catalog with the refactored scripts

One full regeneration from an empty output tree with the current scripts. The tree grows
from 58.4 MB to 65.1 MB. Two hosted assets are about 2 MB each (an avatar png and a wav);
the same bytes are already committed under assets/, so HF_MAX_NONLFS_KB is raised for
this commit only.

* fix(catalog): run the catalog script tests under the runner they are written for

The rebase put three node:test files in the vitest half of test:scripts, where they report
"No test suite found". They now sit in the node --test list.

* fix(catalog): a WebGPU item without an adapter shows its recorded clip and says why

* ci(catalog): the drift check runs from a committed fetch mirror with no network

Generation now serves every font and CDN script fetch from scripts/catalog-fetch-mirror and fails
when a fetch is not in it, even if the caller swallowed the error. CATALOG_FETCH_MIRROR=record
refreshes the mirror from the live network. The docs_catalog filter also watches packages/core,
packages/producer and the mirror, and the 13 re-hashed font files are listed in ALLOWED_DELETIONS.

* chore(catalog): clean up the temp font cache and sort the mirror index without a locale

* fix(catalog): hide Tune when the clip stands in, time out the adapter request, share one check

* fix(catalog): the adapter probe survives a sync throw and a hang; test the clip guard

* fix(catalog): keep the adapter probe inside the component, Mintlify evaluates only exports

* fix(catalog): run the detail and page generator tests in CI; no Tune flash while probing WebGPU

* docs(catalog): credit vendored libraries and make each SKILL.md list what the block installs

CREDITS.md names every vendored dependency with its upstream and licence, and flags the two leaf
textures whose origin is still to be confirmed. Each SKILL.md Files table now comes from the block
manifest and its install and render paths point at compositions/<name>/. A test checks that every
path a SKILL.md names is a manifest file that exists.

* fix(catalog): a declared WebGPU item passes verify by its manifest tag, not by console wording

verify-catalog-payloads forgave a missing adapter only by matching the markup and the message, which
library-reported errors and widgets never satisfy. The webgpu tag in the item manifest is now the
owner: a tagged item ignores its page and console errors, an untagged one gets no forgiveness, and a
marker payload with no html is listed instead of crashing the run.

* docs(catalog): correct the credits per block and guard the mount and render paths

CREDITS.md states the three.js version and vendored-or-CDN status per block, adds d3-delaunay,
Clipper on the CDN and the Google Fonts loads, and lists every texture with no known origin.
The SKILL.md test now also checks the mount and render paths against the manifest composition.

* test(catalog): read each block manifest once in the SKILL.md file test

The two checks shared a copied manifest read, which fallow flagged as a clone with a high score.

* fix(catalog): commit the hosted item files a gitignore rule kept out, and check git not the disk

The compositions/ and *-ad/ ignore rules matched folder names under docs/public/catalog/items,
so 33 hosted fonts, textures and marks were generated but never committed and would 404 on
the docs host. The rules now have a negation for that tree and the files are added.

The drift check read the checkout, where git-ignored files still exist, so it passed locally
and failed only in CI. It now unpacks HEAD with git archive and compares that.

* fix(catalog): a tile keeps its poster until the composition has painted

* fix(catalog): a tagged WebGPU item forgives only the adapter-absent error, and test the wiring

The manifest webgpu tag now decides whether an item may lack an adapter, and the error wording
decides which lines are that absence. Any other page or console error on the item, and every
failed request, still fails. The per-item decision is one exported function the tests drive,
marker payloads are parsed by a tested helper, and the SKILL.md variable counts are checked
against their tables (cuboid-carousel said forty-nine for forty).

* fix(catalog): the pinned group's posters load eagerly at high priority

* fix(catalog): type the tile reveal test so the scripts typecheck passes

* fix(catalog): stop the adapter signature matching an unrelated failed adapter request

failed to request .*adapter also matched errors about other adapters. It now matches only a
request for the WebGPU adapter, so a tagged item cannot hide an unrelated failure behind it.

* test(catalog): a tagged item still fails on an error about some other adapter

* feat(catalog): the install command sits above the preview, as on the live catalog

* fix(catalog): find the install slot through Mintlify's child wrapper

* fix(docs): catalog copy buttons keep their size when the label swaps to Copied

* refactor(docs): name the shown copy label once in the catalog copy button

* fix(docs): primary catalog button keeps a readable label on hover and after copy

* fix(docs): inset the catalog tab strip and tab content to match the boxed panels
2026-09-19 16:53:29 -04:00
Miguel Ángel a94466256e ci: require before and after captures on studio and player PRs (#4131) 2026-09-19 05:41:33 -04:00
Miguel Ángel 2db126d4c8 docs(skills): one owner doc for everyday edits and a hyperframes-studio skill (#4125)
* docs(skills): one owner doc for everyday edits and a hyperframes-studio skill

Agents got contradictory advice on volume fades and were blocked from splits
and copies by the Ask-agent guardrails.

- creator-editing-recipes.md is the single owner: adds align-to-event,
  copy-a-group and swap-media recipes and the one volume rule (use the lane)
- variables-and-media, data-attributes (skill and CLI copy), the gsap adapter
  and tracks-and-clips now defer to it
- Ask-agent guardrails allow the neighbour edits a split, retime, copy or swap
  requires and point at the owner doc
- new hyperframes-studio skill: sub-compositions for every scene, one caption
  track, one element kind per track, safe zones
- drift test lints every html example in the owner doc with the real linter
  and fails on any doc teaching a volume tween

* refactor(skills): drop unused tween allowlist from recipes drift test

* docs(skills): name the track kind attribute in the studio skill

* docs(skills): stop claiming lint rules that do not exist yet and fix track numbering

Review fixes:

- studio skill: drop the severity claim and the preview-overlay claim,
  group tracks by data-track-kind instead of numeric ranges that clashed
  with the recipes
- recipes: audio data-start is root time only in the root composition;
  scene-local inside a sub-composition file
- guardrails: timeline edits are exempt from the element-only and timing
  lines; the pointer names the skill reference, not a repo path
- montage doc defers to the volume lane; skill count is 21 everywhere
- drift test also scans the montage doc

* fix(skills): the ducking generator emits a volume lane and captions defer to the studio safe zones

- audio-duck.mjs prints a data-automation lane (clip-local times from the
  target data-start) instead of a timeline volume tween; --json adds the lane
- operations.md shows the generated lane; the drift test scans it
- embedded-captions safe-zone lines point at the studio skill, which owns
  the values

* chore(skills): regenerate the skills manifest after rebase

* docs(skills): remotion translation docs point volume ramps at the automation lane

* docs(skills): studio safe zones use the action-safe 90 and title-safe 80 boxes

* docs(skills): caption docs defer to the studio safe boxes

* chore(skills): regenerate the skills manifest after rebase

* fix(skills): studio skill names the safe margins; guardrail test matches the timeline-edit wording
2026-09-18 23:19:40 -04:00
Miguel Ángel 9bb4f5cd57 feat(catalog): browse the catalog as a live grid, not a static index (#4051)
* feat(catalog): browse the catalog as a live grid, not a static index

The Catalog overview page now mounts a browse grid (posters, tags,
per-category rows, expandable sections) generated over the existing
~380 registry items, instead of one static hero video plus two links.
docs/snippets/catalog-gallery.jsx and docs/catalog-gallery.css are
ported from a reference implementation of this same UX; the CSS keeps
its original comments as-is (NO_ESSAY_COMMENTS_OK: vendored file).

scripts/sync-docs-catalog.mjs and build-docs-gallery-nav.mjs are new,
generating the gallery's data and the Catalog tab's nav directly from
this repo's registry/*/registry-item.json and docs.json, so no sparse
clone or snapshot of another project is needed. scripts/check-docs-catalog.mjs
proves every catalog page from before this change still exists, on
disk and in the sidebar, with an identical page list.

No catalog page or snippet is removed; the previous overview content
(catalog-overview-player.jsx) is unused on this page but stays in the
repo.

* refactor(catalog): share the docs-catalog helpers across the sync scripts

Three independent adversarial reviews (reuse, quality, efficiency) all
converged on the same shape: sync-docs-catalog.mjs, build-docs-gallery-nav.mjs
and check-docs-catalog.mjs each hand-wrote the same docs-root resolution,
the same catalog-gallery-data.mdx parse, and the same Catalog-tab lookup.
Extracted into scripts/docs-catalog-shared.mjs.

sync-docs-catalog.mjs also ran build-docs-gallery-nav.mjs as a dynamic
import purely for its side effect, forcing a same-process re-read and
re-parse of the data file it had just written. build-docs-gallery-nav.mjs
now exports buildNav(docs), called directly with the path already
resolved; its CLI entry is guarded for standalone use.

slug() dropped the /^-+|-+$/ trim in favor of the character-scan trim
packages/core/src/tokenSlug.ts already uses for the same reason (CodeQL
flags that regex as polynomial ReDoS, js/polynomial-redos); not imported
directly since tokenSlug.ts is compiled TS with a different domain
(CSS variable naming) and no existing scripts/*.mjs imports packages/core
source at plain node runtime.

Fixed one bug found while re-verifying after the refactor: the recursive
nav walk wasn't idempotent against its own generated output — re-running
sync-docs-catalog.mjs a second time saw the "Catalog" wrapper buildNav()
adds and misattributed every flat, single-section group under it, 9
groups collapsing to 7. walk() now treats "Catalog" as transparent, the
same way it already treated "Overview". Verified: three consecutive runs
from a fresh origin/main checkout now produce identical 9 groups / 400
items each time, and check-docs-catalog.mjs still passes.

* chore(catalog): scrub an internal decision code from a code comment

* fix(catalog): stop losing a section's label when its nav is flattened

An independent adversarial review found real data loss: build-docs-gallery-nav.mjs
collapsed a group with exactly one section into a flat page list (a clean sidebar
for the common case), but sync-docs-catalog.mjs's walk() derives an item's section
from nav depth, so a second sync run over that flattened nav could no longer
recover the section name and silently fell back to the group's own label. Data &
charts / Data was the one real casualty in this catalog; Carousels and Blocks
happened to already have a section named the same as their group, so the loss
was invisible there.

Fix: only flatten when the section label equals the group label, so nothing
non-recoverable is thrown away. Verified with a deliberate mutation (reverted the
condition, reproduced the exact bug from a clean origin/main checkout, confirmed
the new check-docs-catalog.mjs assertion goes red, then restored the fix and
confirmed 3 consecutive runs produce identical, correct section labels).

check-docs-catalog.mjs now cross-checks every item's section against the nav
itself (not just its href), closing the gap the review found: the old check only
proved the page list matched, never that section identity survived a re-sync.

Also deletes docs/snippets/catalog-overview-player.jsx: unreferenced anywhere in
the repo now that catalog/index.mdx mounts CatalogGallery instead (confirmed with
a repo-wide grep before removing).

* chore(catalog): allowlist the deliberate catalog-overview-player.jsx deletion

* feat(catalog): live-hover preview, fuller sidebar, landing order

Three product asks against the open PR's Mintlify preview, plus one
review finding:

- Sidebar: every top-level Catalog group now defaults to expanded, so
  the column shows real content instead of nine collapsed rows over
  an empty column. Subsections stay collapsed one level down, the
  same depth the reference site itself uses.
- Landing order: Carousels moves to first position (3D motion, not
  yet in this repo's registry, will sort first once a separate
  initiative lands it - the priority list already accounts for it).
  Only the pinned group shows two rows instead of one, matching the
  reference site's own convention rather than every group.
- Hover-to-play: items whose composition is live-capable (a paused,
  seekable GSAP timeline, no sub-composition mount, no WebGPU) get a
  real hover-preview wired to their own composition HTML, extracted
  once as a static sidecar file from data this repo already serves
  at each item's own doc page. Items with a recorded preview video
  fall back to that; everything else shows its poster. Verified no
  WebGL/three.js item is ever marked live (238 live, 30 video, 132
  poster-only of 400).
- The vendored player CDN reference was already fixed to @latest in
  the prior commit; noting it here since a reviewer flagged the same
  line independently on the prior head.

docs/public/catalog/gallery-live/**/*.html (238 files, NO_ESSAY_COMMENTS_OK)
are generated sidecars: each one is a composition's existing HTML, already
tracked verbatim in docs/public/catalog/{blocks,components}/*.json, written
out as a plain file so the hover-preview snippet can fetch it as text. Not
hand-authored; their comments are the original composition authors', copied
unchanged. Regenerated by scripts/sync-docs-catalog.mjs on every run.

* refactor(catalog): merge the item and preview passes, one group-label source

itemFrom() and walk() computed the same group-label fallback expression
independently; itemFrom now returns it once alongside the item instead
of the caller re-deriving it. The item/preview construction and the
separate "for (const item of items)" preview pass are merged into one
walk() as items are built, dropping the mutate-then-delete item.live
scratch field. Byte-identical generated output, confirmed by diff.

* chore(catalog): scrub the private upstream repo name from code comments

An independent adversarial review found the same leak class as an
earlier fix (commit cfe14f813, an internal decision code) in four
provenance comments naming a private individual's private GitHub
repository and an exact commit sha, in a public repo. Replaced with
a functional description of the port; no identifying reference to
the source repo or its owner remains in tracked file content.

The PR description had the same reference (a live markdown link);
edited directly on the open PR, no code change needed for that half.

* chore(catalog): restore the adapted-from-elsewhere signal without the name

* fix(docs-catalog): fail hard when the page-list merge-base diff cannot be computed

* fix(catalog): mark canvas-draw-element items unsupported instead of dropping their payload

* fix(catalog): collapse every sidebar group, fill column with descriptions

Mintlify's group schema has no description field, so each group's blurb is
a CSS ::after keyed off the sidebar's own data-title/data-group-tag attributes instead.

NO_ESSAY_COMMENTS_OK: false positive.
The real /* */ block is 3 lines. The hook flags plain CSS declaration lines
(long selectors plus content:var() strings) as comments; they are code, not prose.

* feat(catalog): fold the 3D-motion group into the browse grid

Regenerated after merging feat/ad93-3d-motion-catalog: 407 items, 10

groups, 3D motion leading and pinned on the landing order.

* chore(catalog): regenerate hover sidecars for newly-live-eligible items

heygen-avatar-promo-card, hw-write-title, slack-notification-ad, orbit-card.

NO_ESSAY_COMMENTS_OK: generated composition HTML, not hand-authored.

* fix(docs-catalog): the page-list check should catch loss, not additions

It was written for a pure refactor (this PR's original scope: same pages,

different nav shape) so identical-set was the right proof. Stacking a

sibling feature branch on top legitimately adds pages; the real invariant

is nothing lost and no duplicates, so only that half stays an assertion.

* fix(catalog): mount the player from each item's own payload, no CDN needed

Root cause of the grey Carousels/3D-motion tiles: those registry items

never had a CDN poster/video, and the .html hover sidecar 404s on the

deployed docs host (Mintlify drops .html/.js/.css from docs/public with

no error). Cards now fetch the same repo-served .json payload the detail

page already uses, mount a player, seek to 3s and rest there, play on

hover -- viewport-gated, capped per tier (DOM vs WebGL) so a heavy row

can't blow the page's GPU budget. Also fixes a real regression: the

canvas-draw-element marker file was making detail pages embed a broken

player (srcdoc=undefined) instead of falling back correctly; it now

renders an honest tile naming the required flag. Deletes the now-dead

.html sidecar mechanism and its vendored gating helper.

* fix(catalog): move player-mount consts inside the component scope

Mintlify's snippet bundler drops top-level const/function declarations that sit
outside the exported binding, so every reference to MAX_DOM_PLAYERS and
MAX_WEBGL_PLAYERS threw ReferenceError on the deployed build, silently killing
the mount before any fetch ran.

Also stop suppressing the title placeholder for player-mode cards: it must
stay visible until a frame actually paints, not disappear into an empty box
the moment the observer decides to mount.

* fix(catalog): make mount failures loud and add a bundler-scope check

Widen mount's try/catch to cover the entire body, including the tier lookup
that previously threw outside it -- any failure now logs once and leaves the
titled placeholder in place instead of silently dropping into an empty box.

Add a check-docs-catalog.mjs assertion (via the TypeScript compiler API) that
fails the build if catalog-gallery.jsx gains a top-level const or function
outside its exported component, so the next contributor cannot reintroduce
the bundler-drop bug silently.

* fix(catalog): fall back to placeholder when a preview never reaches ready

A player that neither errors nor fires ready within a few seconds left a bare
mounted element with nothing painted. Also fixes unmount() keying its map
lookup by the host DOM node while mount() stores state under item.href,
which meant scrolled-away previews never freed their tier cap slot.

* docs(catalog): trim an essay-length comment in docs-catalog-shared.mjs

* fix(catalog): drop the WebGPU tile message, keep the headline mounted

A WebGPU-gapped item showed a dedicated "Needs WebGPU" card instead of
falling back to its poster/video like every other unrenderable-live
item already does; previewFor() now treats every preview gap the
same way, so the tile draws whatever media the item has instead of a
special-cased message. Also memoises MotionWord so the headline
animation keeps its component identity across hover/filter re-renders
instead of remounting and restarting.

* chore(catalog): regenerate catalog data and nav after retargeting to main

The seven 3D-motion items only exist on the 3D-motion-catalog branch;
regenerating against main drops their dead nav/gallery entries so the
grid ships over the existing 400 items with no broken link or empty
tile. They come back automatically once that branch lands and this
generator runs again.

* fix(docs): clear format, lint and complexity gates on the catalog PR

Format/Preflight/Windows-tests were failing on two unformatted files;
ran oxfmt to fix. Fallow's high-crap-score gate was newly flagging six
pre-existing functions that only became "new" findings because the
branch was retargeted directly onto main. Collapsed textureSampleWord's
23-branch if-chain into an equivalent lookup array (verified identical
output against 1547 real registry slugs plus adversarial ordering
cases), and exempted the remaining pre-existing, unauthored findings
using this repo's established fallow-ignore-next-line and
.fallowrc.jsonc health.ignore conventions.

* fix(catalog): drop the stale index prose, give the new item a CDN preview

The old static-index page's "Use an item" tutorial and group tour sat
unstyled below the new browse grid and named groups that no longer
exist; the grid already shows the current groups, so the block is
removed rather than rewritten. Kept the required Related topics
section.

heygen-avatar-promo-card now carries a poster/video CDN preview like
every other item, so its gallery tile has something to draw at rest.

* fix(catalog): correct an inaccurate fallow-exemption justification

A reviewer caught that the generate-catalog-payloads.ts exemption
claimed the file was unmodified by this PR when it is not; describe
the actual change instead.

* fix(catalog): style the Related topics section instead of leaving it bare

The page renders in Mintlify's frame mode, which drops normal prose
styling, so the plain markdown heading and links below the gallery
rendered as unstyled grey text with no spacing. Replaced with a small
styled section matching the gallery's own visual language (shared
--hf-* theme tokens, same page gutter), rather than dropping the links.

* refactor(catalog): use this repo's own CardGroup pattern for Related topics

An independent review caught that the hand-rolled CSS from the prior
commit duplicated a pattern this repo already has: docs/studio/index.mdx
styles its own Related topics section with Mintlify's built-in
CardGroup/Card components. Switched to that instead and dropped the
custom CSS. Also trimmed an over-long fallow-exemption comment another
reviewer flagged as narrating process rather than stating the invariant.

* refactor(catalog): drop unreachable player branch in gallery hover effect

* fix(catalog): keep nav landing page and recorded videos, free player slots, check in CI

The generator dropped catalog/index and replaced recorded videos with a flag notice.
A card scrolled away before ready released its player slot twice.
check-docs-catalog now runs in CI.

* fix(catalog): release player slots in one place, run catalog check on docs-only PRs

Unmount, error, timeout and catch now share one idempotent release.
The Lint job also runs when only docs or registry files change.

* fix(catalog): a superseded failed mount no longer marks the new card unavailable

* revert(catalog): move the CI wiring out of this PR

The CI change ships separately as chore(ci).

* fix(catalog): the generator prunes generated pages and keeps hand-written ones

Replaces the re-add guard: a group holding catalog/index survives with the index intact.
2026-09-18 15:10:35 -04:00
renovate[bot]andJames eae4892ae8 chore(deps): update dependency vitest to v4 [security] (#3789)
* chore(deps): update dependency vitest to v4 [security]

* fix(test): preserve test behavior on Vitest 4

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: James <james.russo@heygen.com>
2026-09-10 13:55:52 -04:00
James Russo cd1a996ba4 fix(catalog): confine hosted preview asset downloads (#3843)
* fix(catalog): confine hosted preview asset downloads

* test(catalog): pin credential and port refusal with finite streams
2026-09-10 08:21:49 -04:00
James Russo 962c95406d fix(registry): verify staged asset content before publishing manifests (#3832)
* fix(registry): verify staged asset content before publishing manifests

* test(registry): run asset staging regression in script CI

* refactor(registry): separate verified manifest publication
2026-09-10 04:28:58 -04:00
Miguel Ángel db51d5479d fix(ci): generated files never read half-written, lint scaling test runner-proof, probe cache test off disk (#3834)
* fix(build): stop generated files from being read half-written during the build

Two unrelated pull requests kept failing CI for reasons that had nothing to do
with their changes.

Typecheck failed with "TS1002: Unterminated string literal" pointing at a
generated file. The root build compiles several packages at the same time, and
more than one of them regenerates files under a package's src/generated while a
sibling's tsc is already reading them. A plain write empties the file and then
refills it, so for a few milliseconds what is on disk is the first half of the
new file. Whichever build read it in that gap saw a truncated file and stopped.
It never happened locally because locally nothing else is reading.

Every generator now writes the new version under a temporary name and then
renames it over the target, which the filesystem does in one step. A reader
either gets the whole previous version or the whole new one; there is no moment
where it can see half of either. Content that has not changed is not rewritten
at all, so repeat builds no longer touch these files. Four generators were
affected and all four now go through one shared helper, which is where the rule
lives from now on.

The build also rebuilt the core package a second time, in parallel with the
packages that read it. That second rebuild was already redundant, and removing
it takes the writers out of the window entirely. Renaming is what makes the file
safe; dropping the duplicate build makes the build shorter as well.

Separately, a linter performance test failed on four of the last ten failed runs
on main. It timed a scan with a stopwatch and demanded the result come in under
two seconds; one run took 3.7s. That is a statement about how busy the shared
runner was, not about the code, because a stopwatch also counts the time the
machine spent running someone else's job. The test now counts only the processor
time this process actually used, and checks that the cost grows in step with the
input rather than against a fixed number of milliseconds. Measured on a machine
under load, the stopwatch ratio for the same input reached 45x while the
processor-time ratio stayed at 20x.

Both fixes come with a check that fails if the fix is removed.

* fix(build): keep the shared write helper inside the core package

The helper the generators call had been placed in the repo-root scripts folder.
Every container image that builds a package copies the packages folders whole
but cherry-picks root scripts one file at a time, so the image builds failed on
a missing module. The repo already shows both halves of that convention: the one
root script a package build imports has a matching copy line in the image, and
cross-package imports into the core package need none.

The helper now lives with the core package's other build scripts, and the one
generator outside that package reaches it the way the engine package already
reaches core.

* test(engine): keep the probe cache bound test off the filesystem

The eviction test wrote, stat'ed and deleted 129 temp files to exercise an
in-memory LRU rule. Its runtime tracked filesystem contention rather than
the code under test, and on a busy Windows runner the file churn alone
pushed a ~300ms test past the 5s timeout, failing unrelated pull requests.

Cache identity comes from stat, so the test now synthesises stat results
and never touches disk. While here, the test also asserts the LRU touch:
re-probing an entry before the bound is hit must keep it resident and
evict the next-oldest one instead. The previous shape never hit the cache
during the fill, so that branch was untested.

* test(lint): keep the scaling check inside the scanner's linear range

The CPU-ratio version sampled 320k characters, where the output string's
own growth dominates and the whole test cost seconds of CPU; on a shared
runner that tripped the default test timeout, the failure this change
exists to remove. Sample 10k and 80k characters instead, where 8x input
measures ~8x and a quadratic scan still measures 60x or more, and give
the test an explicit timeout so a slow runner reports the ratio.

* test(core): snapshot file identity through a descriptor

The before/after inode and mtime checks read the file through a path stat
and then exercised the writer on the same path, which reads as a
check-then-use race to static analysis. Snapshot through an open
descriptor instead; the assertions are unchanged.
2026-09-10 03:08:58 +00:00