mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
693c38550888f6aa4277d4e4fb4bac8c77177316
206
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
693c385508 |
fix(cli): commands exit right away even when the network or DNS is broken (#4604)
* fix(cli): commands exit without waiting on update checks or telemetry The update and skills checks run in a detached child that refreshes the caches for the next run; the parent only reads the caches. finalizeCli no longer awaits the telemetry flush: the exit handler flushSync() delivers what is queued. The events beacon, feedback and figma error paths stop awaiting flush() too. * fix(cli): retry a failed background check at most once an hour Before starting the background worker, the CLI stamps a per-check attempt time and skips a check it attempted within the hour, so a dead network no longer starts a worker on every command. The stale-pin notice writes onto a fresh read, detached spawns set windowsHide, and a test ties the worker path to its build entry. * fix(cli): a failed-check stamp dated in the future no longer silences update checks |
||
|
|
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 |
||
|
|
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 |
||
|
|
e00baefb5c |
fix(studio): reload the preview only when a file it loaded changes (#4570)
* fix(studio): reload the preview only when a file it loaded changes * test(studio): read the owner id from the new change-owner shape * refactor(studio): drop a comment the parser name already says * fix(studio): keep reloading until the preview is built, and count what its document names * style(studio): format the reference pattern and name the kept reload * fix(studio-server): scan preview references in linear time and refresh all thumbnails on a merged reload * fix(studio): keep a waiting film change when a file the preview never loaded arrives behind it * test(studio): name the preview ownership test for what it checks * chore(ci): allow the watchIgnore removals and trim the new comments * fix(studio-server): reload the preview when a folder holding a file it loaded moves in or out * perf(studio-server): check a changed folder in one lookup and cover every reference form * fix(studio-server): record read folders for a project at the filesystem root * fix(studio-server): reload the preview when the file a symlinked asset points at changes * fix(studio-server): resolve symlinks on every preview read and key projects by their real folder * fix(studio-server): record every link a preview read passes through, resolved as the system does |
||
|
|
1a53681de1 |
fix(studio): an automation lane edit is refused on a locked clip and reports how it saved (#4533)
* fix(studio): an automation lane edit is refused on a locked clip and reports how it saved Lane drags, Delete and paste save through the timeline's gated clip or group save instead of the property panel's commit, so the host's canEdit applies and the commit resolves saved, refused or failed. A landed save refreshes the lane store and the panel's selection, as the panel's commit did. * fix(studio): a refused automation save puts the preview and the lane back A save that does not land (recording, no id, a failed write) reverts the live preview it was given, the lane drops a draft whose save came back refused or failed, the no-id and no-project failures toast like the others, and the panel refresh reads the current selection. The panel commit's unused coalesce option goes, and the lane value reuses automationAttrValue. * fix(studio): a failed save restores the preview it was given, not an older one Each timeline save claims the preview's before-value when it starts, so a gesture begun while it is in flight records its own and a later failed save cannot put back a value from before an earlier save landed. * chore(repo): allow the automation gesture key files this branch removes * fix(studio): a refused automation save leaves the lane store and paste mark as saved Reverting a live value re-syncs the lane store from the restored preview, so an overlapping save that landed first cannot leave the store holding a later value that failed. A paste whose save is refused or fails gives back the selection and paste chain it replaced. * fix(studio): a failed save settles on what the file holds and a refused paste marks nothing A save that does not land settles the preview and store on the file's value (the claimed before-value only when the file cannot be read), and leaves a gesture begun meanwhile alone apart from correcting its before-value, so two overlapping failed saves cannot leave either on a value that never landed. A paste selects and marks its span only once it lands and only over the selection it was pasted at. * fix(studio): a failed save reads the file only after writes queued to it land The settle read goes through the same per-path queue as the writes, so a save that fails while another is queued settles the preview, the store and any newer gesture's before-value on what that queued save wrote. * fix(studio): an older failed save never settles over a newer edit on the same lane Live-preview bookkeeping moves to createLiveLanes, one owner for both timeline saves: each lane key carries a generation that every live write and save start advances, and a save settles the preview and store (on its landed value, or on the file after a failure) only while it is still the newest. A paste marks its span only if it is still the latest paste. * test(studio): a newer live drag keeps its preview when an older save recovers * fix(studio): every finished save records what the file holds, even under a newer edit A save's settle now always writes the file's value (landed, or read back after a failure) into the lane store and a live gesture's before-value, and moves the preview only while nothing newer has touched the lane. The clip store is set from that value rather than copied from the preview. A paste marks its span only while its clip is still the selected one. * fix(studio): an older automation save never overrides a newer gesture on its lane A failed save's recovery read could settle after a newer save started, and the save's own optimistic preview patch and failure unwind ran over a newer drag. Every preview write a save makes now goes through the lane's ownership check, only the newest save records its value, and a refused drag hands the lane back to the newest pending save. * fix(studio): timeline attribute saves land in the order they start An attribute save read the file before joining its write queue, so an older save with a slow read could land after a newer one, overwrite it on disk and leave the lane showing the newer value. The save now joins the queue first and reads, patches and writes inside it. A save moves the preview only while it is the newest save and no drag is live, which replaces the per-lane generation. * refactor(studio): name the in-queue history write for the queue it needs * test(studio): pin save start order and a newer release's preview mid-flight * fix(studio): a drag keeps its points when an older save lands under it An older save that landed mid-drag wrote the lane store, the lane dropped its draft, and the release committed the older points. The lane now keeps its draft while a gesture is live. A save that cannot read the file back settles on the last value read or saved on its lane, never on an unsaved one, and an older save that lands records its value there even when a newer save is pending. * fix(studio): a lane's first failed save still puts its drag back With the claimed-value fallback gone, a first save on a lane that failed and could not read the file back settled on nothing, leaving the drag playing. A save begun with no other save pending now records the value under its drag as verified, since that is what the file last settled on. * fix(studio): only a before-value read with no save pending counts as saved The value under a drag was trusted when its save started with nothing pending, but it was read when the drag began, possibly over a paste that later failed. A lane now marks a before-value clean only when no save was pending as the drag began. A group lane's preview carries its store mirror, as its live writes do. * fix(studio): a lane's saved value never crosses into another project The lane bookkeeping outlives a project switch and its keys had no project, so a value saved in one project could settle into another project's clip with the same key when a save there failed and could not read the file back. Lane keys now carry the open project, and a save moves the preview or store only while the project it was made in is still open. * fix(studio): a failed save reads its file back from its own project A save that failed after a project switch read its recovery from the newly opened project and recorded that project's value as its own. Each save now captures its project when it starts and reads back from it. * fix(studio): a failed save never brings back a value an undo removed A lane kept its last verified value after its saves settled, so after an undo a save that failed and could not read the file back settled on the removed value. The value now lives only while a save on the lane is in flight, and each save records what its own in-queue file read found. The group preview's store mirror is no longer needed and is gone. * test(studio): pass the file-read callback in the direct attribute-save tests * fix(studio): a drag whose before-value a save settled counts as clean When a settle rewrites a live drag's before-value to a verified value, that value is known saved, so the drag's own save can fall back on it even when its read and read-back both fail. Adds a test for a failed save reading back from its own project while another save is in flight across a switch. * fix(studio): a save started in one composition never lands in another of the same project Live lanes were scoped to the project only, so a save held across a switch to another composition of that project settled its value into the new composition's preview (clip and group lanes) and store (group lane). Lanes are now scoped to the project and the composition open when the save starts. createLiveLanes moves to its own module for the 600-line cap. * fix(studio): an undo during or after a lane gesture stays undone when the next save fails A live gesture records the lane's value before it starts, and a later save that cannot read the file settles on that value. An undo mid-drag, or after a gesture that ended without saving, left that value stale, so the save brought back what the undo removed. Every undo and redo now re-reads each open lane's value from the files it restored. * fix(studio): undoing group automation clears it from the members' lanes at once The group-info cache only noticed a change when its MutationObserver callback ran, so the soft restore's store sync, which reads it synchronously, mirrored the pre-undo group automation onto the members. A read now counts mutations the observer has not delivered yet. * fix(studio): an undo reaches every lane and pending save it rewrote, in any composition The undo re-read only covered lanes in the open composition and live gestures. History is project-wide, so undoing from another composition left the rewritten composition's lanes on the pre-undo value, and a save released while the undo was in flight kept that value as its fallback. The re-read now covers every lane of the project on a restored path, including saves still pending, and an undone file it cannot read makes the value untrusted instead of dropped. A missing element reads as unreadable, the same as a save's read-back. * fix(studio): a cancelled fade, stretch or preset audition leaves no stale lane value behind A cancel put the gesture's start back through a live write, which left the lane's before-value in place until the next save trusted it, even after the property panel, the source editor or an agent had changed the file. The fade cancel, the edge-stretch cancel and leaving a preset audition now end through the lanes' revert, so a before-value exists only during a live gesture. * test(studio): pin that a group write is read back fresh in the same task * style(studio): drop two comments the names already carry * fix(studio): a failed group save reads its own file back; fade cancels work without revert - The group save's failure read-back used the file of whatever composition was loaded when the write failed. It now reads the file captured when the save started, as the clip save does. - A fade cancel writes its start back live before it reverts, like the other cancel sites, so a host without the optional revert callback still puts the fade back. - The edge-stretch cancel comment says what the code now does. - The save test harness passes the revert callbacks, as the app does; without them the cancelled-gesture tests exercised a host that lacks them. * refactor(studio): trim three long comments on live automation saves |
||
|
|
d290c05b09 |
fix(cli): --yes in an unattended run no longer overrides a no saved by another process (#4547)
* fix(cli): --yes in an unattended run no longer overrides a no saved by another process * fix(cli): every settings writer takes the lock, and --yes needs a real terminal to reverse a no * fix(cli): the thin-results download offer also needs a real terminal * fix(cli): one owner-checked settings lock, and an unreadable settings file never counts as a yes * fix(cli): settings lock removals go through a reaper, and a non-boolean answer counts as a no * fix(cli): a settings lock that cannot be released no longer fails a write that landed * fix(cli): a leftover settings lock is never taken over, and doctor names it * fix(cli): a background update that cannot be recorded is not launched again every run * fix(cli): every on-device install re-reads the saved answer first, and an unsaved yes installs nothing * fix(cli): the thin-results offer stops when a yes cannot be saved * fix(cli): only the consent-checked step installs the on-device runtime, and a leftover lock warns once * fix(cli): the leftover-lock warning follows the attended-terminal rule |
||
|
|
8798e40da8 |
fix(registry): carousel cards fall back to their sample image inside the assets folder (#4543)
* fix(registry): carousel cards fall back to their sample image inside the assets folder When an image variable resolves to nothing, the 25 carousel blocks loaded the sample card by bare file name, which 404s beside the block and draws a white card. The fallback now keeps assets/carousel-images/, the folder every card image is installed into. A guard test fails on any block with a bare fallback. * test(registry): the carousel fallback guard covers every block that uses it The guard now picks each block that reads an image variable and fails when its fallback no longer matches, instead of silently skipping it, and ignores stray files in the blocks folder. |
||
|
|
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 |
||
|
|
81d8a03d2e | fix(repo): keep one fallow audit base cache per checkout instead of one per main commit (#4525) | ||
|
|
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 |
||
|
|
828c434f76 | refactor(studio): drop recordEdit's kind, which the project history never read (#4474) | ||
|
|
36f8bd0e91 |
feat(studio): undo and redo step through the project history, whoever made the change (#4456)
* feat(studio-server): an undo taken from Studio is not mistaken for an outside edit * feat(studio): undo and redo step through the project history, whoever made the change * feat(studio): the dev server keeps each project history, in the same folder as preview * fix(studio-server): an undo that deletes a file reads as Studio's, and dev histories close * fix(studio-server): a claim keeps an agent's earlier write the agent's and takes back Studio's own * chore: allow the removal of Studio's browser edit history files * test(studio): mock the project history in the runtime smoke * fix(studio): step past a jammed undo and say why a step fails * fix(studio-server): an agent turn cut by a claim commits its part before the claim * fix(studio-server): a claim's cut ends a held drag and takes only the cut files out of a turn * fix(studio-server): a held drag ends when someone else changed a file it already holds |
||
|
|
fc8cb86ef6 |
ci: check TODO owners, public URLs and source links in comments (#4453)
* ci: check TODO owners, public URLs and source links in comments * fix(ci): a TODO's issue link must be a github.com issue URL |
||
|
|
7c634426fd |
docs(cli): explain how catalog search works and which model it downloads (#4462)
* docs(cli): explain how catalog search works and which model it downloads * docs(cli): name the model catalog search was chosen against * docs(cli): keep only live rules in the word-search header comment * docs(cli): correct the hosted vector size in the local vector build comment |
||
|
|
53e2e20a15 |
fix(registry): six blocks load their own files and variables when mounted (#4452)
* feat(core): a composition's script can load its own files with __hyperframes.assetUrl * test(core): a template composition inside a mounted file resolves assets from that file * docs: assetUrl is for paths beside the composition, not project-root paths * docs: say assetUrl misses a project-root path * fix(core): a mounted composition's import map and module scripts run as such * refactor(core): scope a mounted module script under the same condition as a classic one * fix(core): a mounted module script loaded by src stays a module * fix(core): a mounted import map keeps the trailing slash of a prefix address * fix(registry): six blocks load their own files and variables when mounted * fix(registry): keep frost's variable declarations on its html element * fix(registry): orbit-card uses its own variables; frost font and catalog maps follow * chore(core): keep the module-script helpers' comment share at the base * fix(catalog): frost's payload runs its driving script, not the asset-base one * test(catalog): check import-map keys by membership, not a built regex |
||
|
|
0e34d69641 | ci: comment checks grade branches cut before their scripts existed (#4466) | ||
|
|
3520c8225c | ci: a package file's comment share may not rise (#4445) | ||
|
|
3194c2ae87 | ci: grade the comments each PR touches (#4444) | ||
|
|
8dabc8af63 |
fix(registry): glass-shard-title renders when mounted, and 4 skipped blocks install again (#4423)
* fix(registry): glass-shard-title renders when mounted, and 4 skipped blocks install again * fix(registry): glass-shard-title loads in Studio and ?play, and its docs payload fails loudly * test(scripts): pin the glass payload's hdr guard for any quote style |
||
|
|
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.
|
||
|
|
d1048cf98f |
fix(studio): ship component styles as their own stylesheet for embedders (#4393)
* fix(studio): ship component styles as their own stylesheet for embedders Studio's component rules (timeline clips, loaders, range inputs, toasts and the rest) lived only in the app sheet, so a page that embeds Studio's components with theme.css got unstyled clips and dark-on-dark labels. They move unchanged into components.css, exported as @hyperframes/studio/components.css; studio.css keeps the app shell and imports it, so Studio's compiled CSS has the same rules. * test(studio): read moved component rules from components.css * test(studio): catch class rules nested in the app sheet's layers and media blocks |
||
|
|
8d8ea810b6 |
fix(scripts): retry a fresh GitHub attachment's transient 404 in the captures gate (#4362)
* fix(scripts): retry a fresh GitHub attachment's transient 404 in the captures gate Reproduced this week: the same attachment URL 404'd, then 200'd minutes later with no edit to the PR in between, and whichever asset had been attached most recently was always the one that failed. The gate already retries 429/5xx with backoff, but treats every other status as final — so a fresh upload lagging GitHub's own read-path failed the check on attempt 1. 404 is now retryable, scoped to a GitHub attachment host (isAttachmentUrl) so a real 404 elsewhere (a typo'd link, a deleted gist) still fails fast. 403 stays non-retryable everywhere, including on an attachment host — it means something else (auth, a private-repo asset another workflow can't see), not propagation lag. Widened the retry window to match: 5 attempts, delays out to 60s, 3-minute deadline (was 3 attempts / 45s) — the observed lag was up to ~3 minutes. Tests: split the old combined 404-and-403 test; added the attachment-host 404 retry case and the non-attachment-host 404 still-final case; updated the backoff test's attempt/delay assertions for the new constants. 73/73 green, oxlint/oxfmt clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(scripts): stop the captures-gate retry tests from sitting through the real backoff Review on #4362: `runCli()`'s no-network preload stubs `fetch` but not time, so the two CLI tests exercising a genuinely-undownloadable capture now sit through the full 5-attempt/110s backoff per asset (two assets each) — 220s+ per test, pushing `bun run test:scripts` from 15s to 446s and `Test (support)` (timeout-minutes: 10) to 10m0s with zero margin. Preload now also collapses every `setTimeout` delay to the next tick, so the retry logic itself still runs for real (same attempt count, same terminal "could not download" outcome) — it just doesn't wait. 73/73 tests, 1.1s total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(scripts): split isRetryableStatus to clear fallow's CRAP-score gate CI's fallow audit flags `isRetryableStatus` at CRAP 30.0 (cyclomatic 5, threshold 30.0) — pre-existing since the original commit, not something the prior test-timing fix touched. Split into `isServerRetryable` (429/5xx) and `isRetryableAttachment404` (404 scoped to the attachment host), each at cyclomatic 2-3, composed by the still-exported `isRetryableStatus`. Same retry decision, no behavior change. 73/73 tests, oxlint/oxfmt clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
29fc95395d |
fix(skills): keep script imports inside installed skill directories (#4356)
* fix(skills): keep script imports inside installed skill directories * refactor(skills): simplify standalone module checks * fix(skills): carry the brand-logo lookup into the media-use copy of media-fetch |
||
|
|
922be55e55 | fix(skills): preserve captured font subsets (#4358) | ||
|
|
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> |
||
|
|
fb0d33c490 |
fix(ci): scope catalog dispatch and fail closed on detection errors (#4347)
* ci: scope catalog dispatch and fail closed on detection errors * fix(ci): distinguish full catalog validation dispatches |
||
|
|
a57e095468 |
feat(studio): panel tabs take the decided Current design (#4266)
Studio's panels become cards on a dark shell, and each panel's tabs take the "Current" design: the open tab is a fill in the card's colour that slides to the tab you pick, only the open tab shows its icon and close glyph, and a strip that is too narrow scrolls with a soft edge instead of hiding tabs behind a menu. This replaces the pill, underline and close treatment that was on this PR before. ## Before   ## After   Switching tabs (the fill slides in about 140 ms and the new icon fades in over 80 ms; the panel body switches at once): https://github.com/user-attachments/assets/8d359ada-c9e1-4425-9c95-61d5d4f5271b ### Every label whole, and the tabs use the whole strip (2x) An earlier head clipped the left strip's fourth tab next to empty strip space. That space was a 72 px slot (plus 4 px) held on every strip for the strip actions, even on strips whose panel is not focused and so draws no actions. Dockview sizes that slot by its content; this PR had pinned it. Now the slot takes room only where the actions are drawn, so an unfocused strip gives its tabs the whole width. Tab padding is 6 px with no gap between tabs, so the tabs also fit next to the actions. Labels, icons and close glyphs sit on the tab's centre in every strip. Left strip at 1440 x 900, tabs width against the room before the action slot: | | macOS | Linux (these captures) | |---|---|---| | Earlier head | 322 in 280, clipped | 312 in 280, clipped | | Now, panel unfocused | 278 in 358 | 268 in 358 | | Now, panel focused (actions shown) | 278 in 282 | 268 in 282 |    At 1280 x 800 the left column is narrower, and the pinned slot clipped Catalog next to the empty space. Room for the tabs there went from 241 px to 317 px:   Captured at the PR head ( |
||
|
|
ca4af78184 | ci: dispatch required checks for catalog publication (#4345) | ||
|
|
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 |
||
|
|
b32b9a7dab |
feat(runtime): data-vfx-chain — a seek-deterministic per-pixel effect element (#4306)
* feat(core): data-vfx-chain defs and chain parsing (mirrors data-fx-chain)
Mirrors audioFx.ts's shape for the per-pixel VFX chain: a versioned
JSON chain attribute (data-vfx-chain), a def registry with typed
params (number/enum/bool/ref), and parse/serialize/normalize helpers.
HF_VFX holds one def, fractal-noise, with a placeholder frag; the
kernel and its consumers (packages/core/src/runtime/vfx.ts) land in
Task 1.2/1.3 of the vfx-chain-runtime plan, so vfx.ts is added to
.fallowrc.jsonc's entry list for now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(runtime): paint data-vfx-chain canvases on every seek
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): fractal-noise kernel (Basic/fBm approximation) with CPU reference
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): self capture through layoutsubtree with the page-composite readiness protocol
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): wave-warp kernel (Sine)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): displacement-map kernel (self-referential map)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(vfx): chain order is nesting order; preceding CSS effects are captured
...and following ones apply outside. The plan's one-line message for Task 2.4
is 105 characters; commitlint caps the header at 100, so the tail moved here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(vfx): seek determinism and CPU-reference PSNR for fractal-noise
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): size the output canvas from the host's layout box, not its transformed AABB
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(telemetry): vfx node count, capture class and types on render_complete
Threads vfxNodeCount, vfxCapture and vfxTypes from a static scan of the
compiled composition's data-vfx-chain hosts (mirrors the existing
compositionElementCount/colorGradingCount scan) through
RenderPerfSummary.drawElement into the render_complete event, next to
de_capture_mode. Exports chainCapture/parseVfxChain/HfVfxCapture from
@hyperframes/core's index for the producer to consume. The PostHog
dashboard tile is not created here — left for the coordinator.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(vfx): data-vfx-chain guide — element shape, capture table, Studio flag, known constraints
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): register chain hosts that arrive with a sub-composition mount
`initVfx(document.body, fps)` ran once, synchronously, three lines after
`void loadExternalCompositions(...)` — so every `data-vfx-chain` host that
enters the DOM only when a `data-composition-src` mount resolves was never
registered. An unregistered host paints nothing and reports nothing: no
`[HyperFrames] composition script error`, the `.hf-vfx-out` canvas keeps its
300x150 default, and `paintVfx` never even arms the page-composite flag.
Measured on a real multi-file export served raw: `#main-l5-text`'s output
canvas stayed 300x150 and `window.__timelines` never gained the sub-comp.
Re-scan in the loader's `.finally`, before `maybePublishRenderReady()`, so
registration precedes readiness. `initVfx` now releases the outgoing
registry's programs, capture textures and ping-pong targets first — the
output canvas hands back the same WebGL2 context on a re-scan, so the
previous pass's GL objects would otherwise leak.
Two browser cases in `vfxDeterminism.test.ts`, both against the real runtime
bundle: a host whose `.hf-vfx-in` IS the mount (already worked — the mount
replaces the wrapper's children, not the wrapper, so the registered source
node survives), and a host that arrives INSIDE a mounted sub-composition
(failed with `armed: false` and no error; passes now).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): runtime tsconfig typecheck passes
Under noUncheckedIndexedAccess, resolveCaptureSource's null return
widened VfxEntry.src past its VfxCaptureSource | undefined type; the
function is internal-only, so its return type moves to
VfxCaptureSource | undefined instead. Indexing PingPong.framebuffers
by a non-literal index produced | undefined against
WebGLFramebuffer | null, coalesced to null. No behavior change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): structural DOM predicates instead of realm-bound instanceof
vfx.ts checked `instanceof HTMLCanvasElement`/`HTMLElement` directly, which
returns false for inner-realm nodes when the Studio previews across an
iframe realm. domRealm.ts had no canvas predicate yet, so this adds
isCanvasElement mirroring the existing isImageElement pattern, then swaps
all three vfx.ts checks to the structural predicates the lint guard requires.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): paint self-capture hosts under hyperframes snapshot, not only the engine path
`paintVfx(t, { engineMode: true })` armed the page-composite protocol and
returned, betting that every capturing chain runs under `frameCapture.ts`.
`hyperframes snapshot` (and check/compare/validate/layout, and Studio's
thumbnail capture) seek through the same `renderSeek`, never read
`__hf_page_composite_pending` and never call `__hf_page_composite_resolve` —
so a `self` chain painted nothing there, in silence, because its content
lives in a `layoutsubtree` canvas the browser never paints directly.
Engine mode now arms the preview-side capture as well and paints on whichever
completes first. A seek token stops a superseded async capture from repainting
a newer frame, and a registry-identity check stops it painting an entry
`initVfx` already released. `resolveVfxCapture` stays non-idempotent on
purpose: the engine resolves only after its own before-capture hooks have
mutated the DOM, so its capture is still the authoritative one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): an exporter-emitted .hf-vfx-out gets the box the spec fixes for it
findOrCreateOut adopted a canvas the exporter had already emitted without
applying the layout cssText, so it kept the default position:static;
display:inline and wrapped to the line AFTER the layer-wide .hf-vfx-src
canvas — painting a second, misplaced copy of the layer one full layer
height below the host. Measured on retro-wave #main-l6-text: .hf-vfx-src
rect top 315.1 h 93.0, .hf-vfx-out rect top 409.2, both style=null,
position static (ae-mcp findings Task 3.5b). Hosts with no emitted
out-canvas were never affected, which is why only this one host moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(telemetry): vfx_node_count counts hosts, not chain nodes — rename to vfx_host_count
The field increments once per `data-vfx-chain` attribute occurrence, never
per node inside a chain's JSON, so the name promised something the scan does
not measure. Renames the internal `vfxNodeCount` and the wire property
`vfx_node_count` to `vfxHostCount`/`vfx_host_count` across the orchestrator
scan, the perf summary, the CLI render path and the telemetry event. No
dashboard consumes the field yet, so the wire rename costs nothing now and
would cost a migration later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): join the preview capture to the seek-completion barrier, not race it
`paintVfx` fired `capturePreviewThenPaint` and forgot it, so nothing the CLI
awaits waited for a `self` chain's paint. Every snapshot-family command
(`snapshot`, `check`, `compare`, `validate`, `layout`) seeks through
`seekCompositionTimeline`, which drains `window.__hfWaitForSeekCompletion`
and then allows one settle race before screenshotting — and only the
`"hf-seek"` event's `waitUntil()` could put anything in that barrier. The
screenshot therefore landed on however many hosts happened to have finished,
which varied with host count and machine speed.
Extracts the "track, untrack on settle, retain the first rejection" block
`dispatch()` built inline into an exported `registerSeekCompletion`, and has
`paintVfx` register the preview capture through it. Also awaits the per-host
paints with `Promise.all` instead of sequentially: N hosts cost about two
animation frames now, not 2N.
Measured on the pre-fix bundle with two `self` hosts, both output canvases
read `[0,0,0,0]` at the moment the barrier returned; they painted only
because the settle race that follows happened to be long enough. Both tests
added here fail on that bundle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): report WebGL context loss loudly instead of silently no-op painting
One WebGL2 context per `data-vfx-chain` host, against a browser cap that is
commonly 16 and shared with `colorGrading.ts`'s per-element contexts. A lost
context makes every later call on it do nothing, per spec — so the chain kept
"painting" a blank canvas with no error, which is exactly what the module's
loud-failure contract exists to prevent.
Adds the `webglcontextlost` listener pair's first half, following
`colorGrading.ts`'s precedent: `preventDefault()`, mark the entry, report
once through `reportVfxError`. The entry is then skipped by `paintVfx`,
`capturePreviewThenPaint` and `resolveVfxCapture` — silently, since the one
loud report already happened and repeating it per frame is spam.
Restore is left unhandled on purpose, with the reasoning in the doc comment:
`initVfx`'s existing re-scan already rebuilds every GL object, and a second
narrower rebuild path for `webglcontextrestored` would duplicate it. The
shared-context redesign the review floated is likewise out of scope here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* perf(vfx): cache uniform locations at pass-build time instead of querying every paint
`setPassUniforms` asked the driver for `u_size`, `u_t`, `u_fps` and every
param's location on every paint of every pass, and `bindPass` did the same
for `u_src` on every bind. `buildPasses` already constructs each `VfxPass`
once at init, right after the program links, and a linked program's
locations never change — so they are resolved there now and read from
`pass.locations` in the hot path.
Same values, same `null`-means-optimised-out semantics (`gl.uniform*(null,
…)` is a defined no-op, which is what the per-frame query produced too), so
the existing vfx.test.ts and the browser determinism suite cover it
unchanged — both still green, the latter against a rebuilt runtime bundle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): bound the paint wait per host so a stalled compositor can't hang the seek barrier
`paintVfx` registers `capturePreviewThenPaint` into the shared seek-completion
barrier (
|
||
|
|
b73df3549e |
refactor(studio): remove unreachable expanded-child timeline code (#4219)
* refactor(studio): keep legacy fixture state out of rendered rows * fix(studio): keep timeline keyboard actor mounted * refactor(studio): remove expansion telemetry and prove stable nested rows * test(studio): type nested timeline fixture broadly * test(studio): remove expansion lane assertions * test(studio): assert nested row identity by key * fix(studio): satisfy removal and format guards * fix(studio): clear fallow findings in timeline cleanup * test(studio): update timeline removal expectations * fix(studio): remove stale timeline test helpers * fix(studio): keep automation lanes mounted without disclosure * refactor(studio): keep audio lanes while removing clip expansion * fix(studio): restore audio disclosure state * test(studio): remove obsolete expanded fixture assertions * fix(studio): resolve provider rebase without clip auto expansion * fix(studio): remove inline keyframe expansion rows * fix(studio): restore automation disclosure state * fix(studio): remove stale timeline model inputs * fix(studio): pass animation state to timeline layout * fix(studio): remove stale expansion tests and helpers * test(studio): cover shared audio lane labels * fix(studio): remove unused keyframe callback plumbing * fix(studio): clear rebase lint leftovers * fix(studio): remove unused timeline header export * style(studio): format timeline cleanup * fix(studio): restore composable timeline exports * fix(studio): keep audio lanes open across selection * fix(studio): align timeline lane test contract * fix(studio): remove stale lane test import * fix(studio): align track header test callback * fix(studio): remove stale track header test prop * test(studio): preserve ruler half pixel alignment * test(studio): keep audio lane fixtures drawable * test(studio): assert shared lane owner label * fix(studio): align audio lane expansion with layout state * test(studio): use the audio lane owner label * fix(studio): unify audio lane expansion state * fix(studio): align automation lane labels * fix(studio): remove stale lane height import * test(studio): assert automation labels against the real curve, not a formula The previous test recomputed the expected top with the same formula the fix uses, so a regression in TimelineAutomationLaneSlot's own offset would still read green. Moved it to TimelineLanes.test.tsx, which mounts both the header and the canvas, and compares their rendered [data-automation-lane-label]/[data-automation-lane] tops directly. * revert(studio): restore the keyframe-row feature from main Miguel: keep the per-property keyframe rows (diamonds, prev/next arrows, disclosure, auto-open) exactly as main. This PR's real scope is removing dead expandedParentStart consumers, not the keyframe-row UX. * refactor(studio): remove dead expanded-child timeline code expandedParentStart is never set anywhere on main (the inline sub-composition child-row expansion that produced it was already removed in #4132/7797c5ead). Delete its now-dead consumers: the TimelinePane move/resize/delete/split rebase wrappers, the ZMirror call site that routed through them, the timelineDragLanding topology guards, isMainTrackElement's expanded-child exclusion, laneGapFloor's host-window-start math, and the paired expanded-clip-edit telemetry. Keeps every keyframe-row feature (diamonds, prev/next arrows, disclosure, auto-open, telemetry) untouched. |
||
|
|
bc1c60ae5b |
fix(catalog): regenerating the texture-mask page no longer duplicates its section (#4302)
* fix(catalog): regenerating the texture-mask page no longer duplicates its Texture masks section The page generator carries forward any section whose heading it does not own, and it emits "## Texture masks" without owning it. Every run kept the old copy and appended a new one; main's page had three. * chore(catalog): drop the two duplicate Texture masks sections from the texture-mask page * chore(catalog): run oxfmt on the texture-masks regression test |
||
|
|
dd97478175 |
fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice (#4179)
* fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice transcript-cut extracts each kept range to its own final-codec file, then concat-copies them. That produces two audible artifacts at every cut: 1. No boundary fades. Concat splices raw segment edges together, so the waveform steps discontinuously and each cut clicks. 2. Encoder priming silence. Each segment is encoded to AAC independently, so the encoder pads every one with priming samples. Stream-copy concat bakes that padding in as a silent gap at each splice. Measured 24.4 ms and 24.1 ms at the two splices of a three-range cut. It accumulates: the nth splice lands roughly 25n ms late, so splice positions drift away from the sum of the preceding segment durations. The second survives the first -- adding fades alone leaves the gap. Fix: intermediates carry PCM (.mkv for video, .wav for audio-only) instead of the final codec, so audio is encoded exactly once, at concat. Boundary fades of 30 ms are applied per segment, scaled down on segments shorter than 4x the ramp and skipped on degenerate ones. Measured on the same three-range cut, widest run of digital silence at each splice: before 24.4 ms / 24.1 ms after 0.1 ms / 0.1 ms --copy is unchanged: stream copy cannot filter, so it keeps producing final-codec segments and a copy concat. Audio-only output is byte-identical in duration before and after. scripts/lib test suite: 34/35, the one failure (lut-preset-provider) pre-exists on main and is unrelated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(skills): regenerate manifest for media-use * fix(media-use): honor the fadeIn/fadeOut flags fadeFilterFor was passed The caller already computed {fadeIn, fadeOut} per segment, but fadeFilterFor still took one parameter and always emitted both afade ramps -- so a single kept segment (no splice at all) and every segment's true start/end got faded regardless. Extracted the pure function into lib/transcriptCutFade.mjs, made it honor both flags, and added its first unit test: first/middle/last/single-segment filter strings, no ffmpeg needed, fails against the previous version. Also installs ffmpeg in the Test: skills CI job (same action the Producer job uses), so transcript-cut.test.mjs's real-tone RED/GREEN proof runs in CI instead of skipping silently for lack of the binary. * fix(media-use): fix the packed-CLI import and lint on transcript-cut.mjs build-copy.mjs replaces dist/skills/media-use/scripts/lib with a copy of packages/cli/src/media-use/lib, so transcriptCutFade.mjs needed a byte-identical twin there (and in MEDIA_USE_COPY_NAMES) or the packed CLI's transcript-cut.mjs imported a file that no longer existed. Moved its test alongside, matching every other lib helper's tests. Also drops three redundant Boolean(args.copy) wrappers (a ternary condition already coerces) that oxlint's no-extra-boolean-cast flagged. * style(media-use): reflow the shortened ext ternary to oxfmt's shape Dropping the Boolean() wrapper shortened the line enough that oxfmt 0.41 now collapses the ternary onto one line; format:check was failing on the stale multi-line version. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
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.
|
||
|
|
94609daf44 |
feat(studio): audio pills show real peaks and loudness (#4282)
* feat(studio): audio clips draw real peaks shaded by loudness The pill fill is the track tint at 70 percent, and each bar's opacity follows the cached server peaks. Muting greys the pill in place. Linked video audio shows a 1px tick. * style(studio): format the audio waveform markup * fix(studio): map audio peaks across the full clip width * fix(studio): keep waveform visible through fade wedges * fix(studio): keep silent waveform bins visible * fix(studio): keep waveform baseline visible edge to edge * fix(studio): keep waveform visible over fades * fix(studio): keep quiet waveform bars visible * fix(studio): paint waveform above fade wedges * fix(studio): fit waveform inside audio clip * fix(studio): preserve waveform contrast tokens * test(studio): ratchet waveform color literals * fix(studio): keep loudness floor internal * chore(studio): document intentional fallow findings * refactor(studio): share fade handle rendering * fix(studio): pass fade handle styles explicitly * refactor(studio): split waveform drawing stages * test(studio): cover waveform canvas painting |
||
|
|
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 |
||
|
|
5203312c34 |
feat(studio): timeline clips take the designed material (#4270)
* feat(studio): timeline clips take the designed material Picture clips use an 8px radius and audio clips a 21px pill. Clips sit one step above the lane. The label is a chip that drops under 60px. Selection keeps the inset outline plus a 1px inner highlight. Hover background is the only transition. * style(studio): format timeline clip shadow * fix(studio): restore handle-bar transition and de-pin box-shadow test |
||
|
|
c029bf4087 |
ci: require zero orphan tests without a baseline (#4271)
* ci: require zero orphan tests without a baseline * fix: keep reachability reports diagnostic-only |
||
|
|
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. |
||
|
|
a80d733219 | ci: run media-use tests when media-use changes (#4259) | ||
|
|
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 |
||
|
|
0be08f34a2 |
ci: ratchet test reachability across workflow filters (#4253)
* ci: ratchet test reachability across workflow filters * fix: fail closed on skipped detection and quoted selectors * refactor: separate reachability parsing decisions * fix: keep comments and runner prefixes out of reachability * refactor: bound reachability parser complexity * fix: reject conditional shell runner blocks * fix: reject conditional shell constructs in test routing |
||
|
|
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. |
||
|
|
5a8ee663f7 |
fix(studio): report a rejected read as a network reason, not an open failure (#4240)
* fix(studio): report a rejected read as a network reason, not an open failure `stage: "open"` is documented as `openComposition` threw — an unparseable composition, or OOM. Every event in that bucket on 0.8.56 and 0.8.57 carries a fetch-rejection message instead: "Failed to fetch" (28 users / 11 users), "Load failed", "NetworkError when attempting to fetch resource.". The cause is one missing catch. `readProjectFileOptional` awaits `fetch` with no try/catch, so a rejected request escapes the function and is caught by the effect's outer `.catch` — the handler written for `openComposition`. The read path has a five-member reason union precisely so this class of failure is diagnosable, and the one case that never produces a response was the case outside it. That made the largest SDK-session failure class in Studio (2.9% of session users on 0.8.56, 2.3% on 0.8.57) unaddressable: you cannot fix a network failure while it is reported as a composition that failed to parse. Now `readProjectFileOptional` catches its own rejection and returns `reason: "network"`, so it lands in `stage: read` with everything else, and `stage: open` means only what its comment says. Telemetry only. Nothing branches on the stage and no user-facing behaviour changes. Fifth split of this shape, after `ineligible_operation` (v0.8.49), `stage: read` (v0.8.51) and `block_detail` (#4214). Co-Authored-By: Claude Code <noreply@anthropic.com> * test(docs): point the catalog tab-row test at the page gutter #4238 moved the inset to `catalog-detail.test.ts` asserted the exact strings that #4238 deleted: `.hf-ve-tabs-row { margin: 20px 0 0 16px; }` and `.hf-ve-body { padding: 2rem 0 0 16px; }`. That PR removed both insets deliberately — its own comment says "the page gutter is the page's job, and a second 16px here put everything below the stage on a different left edge from the title, install block and stage above it" — and restored the gutter on `#content-area` in `docs/custom.css` instead. The test was left asserting the old mechanism, so main has been red on it since, unnoticed because the `Test` workflow does not run for a docs-only change. The invariant did not change: this content is still inset from the page edge. Only the mechanism did, so the test now asserts both halves of it — the two rules carry no inset of their own, and the gutter standing in for them is still present, at both its values. Asserting only the first would keep passing if the gutter were deleted outright, which is the regression the test exists to catch; both mutations were checked and both fail. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> |
||
|
|
41b7202bca |
refactor(studio): make timeline rows use manifest elements (#4213)
* refactor(studio): make timeline rows use manifest elements * fix(studio): preserve timeline consumer prop names * test(studio): update asset row expectation * fix(studio): clear preflight lint nit * chore: allow intentional timeline hook removal * chore: refresh PR capture checks * test(studio): align visibility gate with store rows * test(studio): preserve canEdit literal type |
||
|
|
90e67ff5a8 |
fix(release): a stale local tag can no longer silently widen the changelog (#4208)
* fix(release): a stale local tag can no longer silently widen the changelog `release:prepare` creates `vX.Y.Z` locally at the branch commit, then the publish workflow creates the same tag at the merge SHA. After a squash merge those are different commits and the local one is not an ancestor of main, so `git describe` cannot reach it and falls back to the previous release without saying so. `git fetch --tags` does not help: it refuses to move a tag that already exists locally, and only `--force` does. On the v0.8.52 cut this produced a `v0.8.50...v0.8.52` draft that re-listed about a hundred already-shipped commits. The changelog is user-facing, and nothing in the output said the baseline was wrong. Adds a guard in the drafter: after resolving the baseline, fail if a stable tag exists locally that sits strictly between it and the release. `describe` only ever returns a reachable tag, so a tag in that range is proof it skipped one. The message names the tags and prints `git fetch origin --tags --force`. `--from` still overrides, for a deliberate baseline. Also prints the captures-gate hatch with the release PR instructions. Every release bumps packages/studio and packages/player, which scripts/check-pr-captures.mjs watches, so it demands before/after media for a version bump; its own `## No visible change` section is the intended answer and applies to a 4-line package.json diff. Verified end to end by moving a local tag onto the pre-squash commit of v0.8.51: the drafter fails with the fix, and drafts normally once restored. Six mutants of the range check are all caught by the tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(release): guard the baseline inside its resolver `resolvePreviousTag` only runs when `--from` was not supplied, so testing `!options.from` at the call site repeated a condition the `??` already expressed. Folding the guard into the resolver drops that branch, which had pushed `createDraft` to a CRAP score of exactly 30.0 and failed the audit. The assert sits outside the try/catch on purpose: `fail` exits rather than throwing today, and this keeps a catch from ever swallowing the guard if that changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
98e8372a0a |
feat(studio): all panels move onto the dockable, persisted layout (#4192)
* feat(studio): every Studio panel is a dock panel and the shell moves onto it
EditorShell, App.tsx, the left and right panel columns, and the
inspector hooks now render through Dock.Panel instead of the old
fixed-width sidebars and split-pane inspector. usePanelLayout becomes
a thin facade over the dock store; fitPanels, the resize divider and
the split-inspector hook are dead and removed.
* refactor(studio): drop dead inspector flags and stale fallow entries after dock migration
Collapse inspectorButtonActive into inspectorPanelActive, remove unread
layers/design active fields, un-export unused prop types, flatten a nested
ternary, and point fallow ignores at the renamed/deleted files.
* fix(studio): keep right-panel behaviours the dock migration dropped
Block params dismiss on selection or when Design is hidden, the slideshow
panel can be closed, the selection outline follows the inspector intent,
caption editing focuses Design, and a URL slideshow tab opens once the
panel exists.
* refactor(studio): name the inspector-open state once in the header
* fix(studio): right-panel behaviours survive selection, restore and reopen after the dock migration
Block params stay open over an element that was already selected, caption mode wins over block params, the slideshow panel waits for the dock, the Renders tab title survives a reopen, each side column remembers the tab it last showed, tab switches are reported only when the tab changes, and the e2e thumbnail selector follows the dock.
* docs(studio): describe the dock layout and point comments at the renamed right panels
* fix(studio): block params survive a preview reload of the selected element, and panel titles are covered
The dismissal compares the selected element by its target key instead of object identity, because a preview reload rebuilds the selection for the same element. A closed and reopened panel keeps its custom title, with a test, and the e2e selector is formatted.
* fix(studio): block params leave with any other tab pick, and a restored slideshow placement survives loading
Block params are now dismissed by the tab setter the way their old tab was, including a re-pick of the same element, instead of by comparing selections. The slideshow panel is only closed after a file that was a slideshow stops being one, so a restored layout is not discarded while the file loads.
* refactor(studio): drop the duplicate inspector-open computation
The rebase onto main left two identical inspectorButtonActive/inspectorOpen
constants after a conflict resolution; only the first was still referenced.
* chore(studio): allow the dock migration deletions in the guard
StudioLeftSidebar.tsx, LeftSidebar.tsx and its storage test,
TimelineResizeDivider.tsx, useInspectorSplitResize.ts and fitPanels
are replaced by the dock; the CI deletion guard needs each named
with its reason.
* style(studio): oxfmt the deletion-guard entries
* fix(studio): rename Timeline's aria-label so it does not collide with its dock panel wrapper
Every dock panel wrapper now carries aria-label=<title>, matching the "Timeline"
title for the timeline panel. Timeline.tsx also set aria-label="Timeline" on its
own content div, so `document.querySelector('[aria-label=\"Timeline\"]')` found
the dock wrapper first and the timeline viewport gate CI check timed out waiting
for an attribute the real content div actually carried. Renamed to "Timeline
track view" and updated every consumer: the production diagnostics selector,
its tests, and the e2e viewport-gate script.
* fix(studio): restore inspectorButtonActive as a real prop after a rebase regression
Rebasing PR2 onto main replayed an older local commit whose diff did not
textually overlap main's newer inspectorButtonActive prop, so git applied
it with no conflict marker and silently reverted the header to computing
the value itself instead of receiving it. StudioHeader.dom.test.tsx already
asserts the prop is respected; restoring it makes that test meaningful
again. App.tsx now computes it once (panelLayout.rightCollapsed is already
the dock's effective collapse state, no separate railed-by-width case) and
passes it down.
* chore(studio): wrap a long doc comment in useRightPanelIntents
|
||
|
|
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 |
||
|
|
9bec64a293 |
feat(studio): header and inspector tabs on the shared primitives (#3624)
* feat(studio): header and inspector tabs on the shared primitives The header's view-mode toggle and the inspector's tab strip become real Tabs, so both gain the roving tabindex, arrow keys and Home/End they never had; undo, redo and Inspector become IconButton and Button; Export becomes the primary Button at the medium size. Capture stays an <a download>, wearing Button's recipe, because a button cannot save a file. Colour literals in StudioHeader.tsx drop to zero: the brand gradient moves into theme.css as two tokens and the Export's hand-written text colour becomes text-bg-0. Ratchet total 475 to 470. * docs(studio): shorten the header and inspector tab comments to fit the comment gate * chore(studio): bank the lower colour-literal baseline for StudioHeader * fix(studio): arrow keys only move focus when the inspector strip toggles panes * chore(studio): allow deleting PanelTabButton, replaced by the shared Tabs primitive |
||
|
|
8d07221156 |
feat(scripts): fail a PR whose After capture is a Before capture (#4177)
* feat(scripts): fail a PR whose After capture is a Before capture The captures check now downloads each Before and After asset in the PR body and fails when any After asset has the same content hash as a Before asset, or when an asset cannot be downloaded. A Before clip attached under After passed the old check because it only looked for media, not for what the media was. * refactor(scripts): return the hashed captures directly * fix(scripts): the capture hash check only fetches GitHub attachments The check fetched any URL a PR body named and followed redirects, so a body could make the runner request an internal host. It now downloads only github.com/user-attachments assets and follows redirects only to GitHub and its signed storage. A 429, 5xx or network error is retried with backoff, other statuses fail at once, and unreadable assets and identical assets are reported under separate headings. * fix(scripts): cap capture downloads, match GitHub's asset bucket, pass forks through The redirect allowlist now names GitHub's asset bucket instead of all of amazonaws.com. Each asset and the whole check are capped in bytes while streaming. A fork PR whose capture cannot be downloaded is skipped with a named reason, never failed. * fix(scripts): one download deadline, https only, same link caught before fetching Reviewer findings on the capture hash check: - each asset gets one deadline across its retries - downloads run one at a time so a large asset cannot starve a sibling - a link under both headings is reported without a download - a read error is no longer reported as a download failure - the CLI fork skip and exit code are tested through a preloaded fetch * fix(scripts): a byte-cap refusal blocks even on a fork, retries refund their bytes - our own cap tripping is reported apart from an unreadable download and always fails - a failed attempt gives its bytes back to the shared budget - trailing punctuation and query strings no longer change which asset a link names - more than 12 capture links is refused, and the job timeout is raised to fit * fix(scripts): strip markdown wrappers from capture links, count shared links once The twelve-link limit is tested at its edge. * refactor(scripts): split the capture download code into small functions Fallow scored the download, redirect and reporting functions as untested because the scripts suite runs under node --test. Each function is now small enough to pass without coverage. Links the duplicate check cannot compare are now named: an http attachment link is refused, other media links are listed. * fix(scripts): compare links by identity, name the refusal, pin three behaviours An explicit port or upper-case host no longer reads as an uncomparable link. |