Commit Graph
3612 Commits
Author SHA1 Message Date
ukimsanov 174bd4e2dc docs: add documentation quality gates 2026-08-04 03:06:58 -07:00
ukimsanov 4ef1511b19 ci: re-run catalog previews when the containment module changes
Rames' non-blocking note on #2975. The paths filter listed the renderer but not
scripts/registry-target-paths.mjs, which it imports — so a future change to the
path-traversal defence alone would never re-run the only job that exercises it.

That is the same shape as the bug the module exists to prevent: the check is
present, the thing that would catch a regression in it is not wired to run.
2026-08-04 03:06:32 -07:00
ukimsanov dc2d1697fe fix(scripts): make registry path containment filesystem-aware
Miguel's second P1 on #2975, and he is right that my first fix only closed half
of it. `resolve()` and `relative()` are string operations and do not follow
links. Registry items are copied in recursively with symlinks preserved, so a
PR shipping `escape -> /tmp/outside` and declaring `target: "escape/pwned.txt"`
passed the lexical check, `mkdirSync` followed the link, and `cpSync` wrote
outside the project.

Reproduced before fixing: the old predicate returned one allowed copy and the
file appeared outside the project. Both directions were exposed — a symlinked
`path` reads a runner file in just as readily.

Containment is now filesystem-aware. No existing component of a candidate may
be a symlink, and the candidate's real location — resolved through its deepest
existing ancestor — has to sit under the project's own real path. A symlink is
refused rather than followed, even one pointing back inside the project:
nothing in the registry needs one, and following it would mean trusting the
target not to change between the check and the copy.

The tests are real fixtures now instead of string cases, because a purely
lexical suite is exactly what stayed green through the bypass. Twelve of them,
covering a symlinked target directory, a symlinked source file, a deeper path
through a symlinked component, an inward-pointing symlink, plus the lexical and
absolute cases from before.
2026-08-04 03:06:32 -07:00
ukimsanov bd7ea5d5ce fix(scripts): contain registry manifest paths in the preview renderer
Miguel's P1 on #2975, and it is real. `catalog-previews.yml` triggers on
`pull_request` for anything under `registry/blocks/**` or
`registry/components/**`, so `registry-item.json` arrives from the pull request
and is untrusted. `mirrorRegistryTargets` joined `files[].path` and
`files[].target` under the temp project and called `cpSync` on the result, and
`join()` walks out of its first argument. A `path` of `../../../../etc/passwd`
reads an arbitrary runner file into the project — which the job then uploads as
an artifact — and a `target` of the same shape writes an arbitrary runner path.

Both sides are now resolved and rejected when `relative(projectDir, candidate)`
is absolute or starts with `..`. Traversal that lands back inside the project
still works, so `nested/../demo.html` is unaffected.

Containment lives in `scripts/registry-target-paths.mjs` rather than inline,
because the traversal cases have to be testable and importing
`generate-catalog-previews.ts` drags in the producer. `existsSync` is injected
so the decision cannot depend on whether the target happens to exist on the
runner. Eight tests, covering traversal on each field separately, absolute
paths on each field, the sibling directory that shares the project's prefix,
and traversal that returns inside.

Verified end to end on a real tree, not only in unit tests: a manifest asking
to read `../secret.txt` and write `../pwned.txt` produces neither file, while
the legitimate entry still copies.

I introduced the wrapper when I extracted this block for a complexity finding
earlier in the stack, and did not look at what it was joining.
2026-08-04 03:06:31 -07:00
ukimsanov 0dcd06ff62 refactor(scripts): extract mirrorRegistryTargets from prepareProjectDir
Inlining the registry-target mirroring pushed prepareProjectDir to cyclomatic
24, the one complexity finding fallow attributes to this branch — the other
eight are inherited and out of scope here.

Same behaviour, expressed as a filter chain in its own named function, so
prepareProjectDir returns to what it was and the new helper stays flat.
2026-08-04 03:06:31 -07:00
ukimsanov 79c4056a3f docs: make Catalog visual and reproducible 2026-08-04 03:06:31 -07:00
Ular Kimsanov 7e5a9b3c24 Merge pull request #2974 from heygen-com/docs/developers-reference
docs: rebuild developer and rendering reference
2026-08-04 02:53:18 -07:00
ukimsanov a99ad218f7 docs: fix the types page subtitle to match its softened claim
Rames' residual on #2974. I softened the body sentence last round and left the
frontmatter saying "All exported types from @hyperframes/sdk". Mintlify renders
description as both the page subtitle and the meta description, so the page went
on promising complete coverage in the two places a reader meets first — and the
one a search engine quotes.

Pre-existing on main, and he did not hold the stamp for it, but leaving it makes
the body fix cosmetic.
2026-08-04 02:45:21 -07:00
ukimsanov 7a91b93dd6 docs: correct four developer-reference claims the source contradicts
Miguel's three P2s and Rames' one finding on #2974, all verified in source
before changing anything.

**`render --json` is not a progress stream.** It prints exactly one
`batch-complete` document at the end (`batchRender.ts:408-418`), asserted as a
single `console.log` in `batchRender.test.ts`. Described as a final result now.

**The iframe drag example never captured the pointer.** `event.target` comes
from `iframe.contentDocument`, so `instanceof Element` against this window's
constructor is always false for a cross-realm node and `setPointerCapture()`
never ran — a pointer leaving the frame then loses `pointerup` and drag state
sticks. Structural feature detection instead, with the reason in a comment so it
does not get "simplified" back.

**The preview adapter example did not compile under strict TypeScript.** `comp`
was captured by the callback before definite assignment (TS2454). Optional, with
`comp?.dispatch(op)`.

**`ORIGIN_APPLY_PATCHES` was imported in a fence that did not use it and used in
fences that did not import it.** Imports do not cross fences, so both examples
were wrong in opposite directions. Rames found the pair in
`open-composition.mdx`; the same shape is in `composition.mdx:630`, which he did
not name. All three fences are self-contained now.

**And `types.mdx` claimed coverage it does not have.** It promised "every type
exported from `@hyperframes/sdk`" while omitting 13 of 42. Eleven are documented
on sibling pages, so the sentence now points at those instead of overclaiming.
The two with no home anywhere — `CompositionVariableType` and
`VariableUsageScan`, both re-exported from the barrel — have entries. The second
is worth having written down: `scanIncomplete` means `usedIds` is a lower bound,
so an id missing from it is unknown rather than unused.
2026-08-04 02:45:21 -07:00
ukimsanov bebaf679d9 docs: rebuild developer and rendering reference 2026-08-04 02:45:21 -07:00
Ular Kimsanov bb416a1413 Merge pull request #2979 from heygen-com/docs/rewrite-guides
docs: rewrite the guides and landing pages
2026-08-04 02:43:37 -07:00
ukimsanov f720eb75be docs: lead the Quickstart with a paste-to-agent block, cut the Examples link wall
Two things a non-technical reader hits that the pages did not help with.

**Quickstart made you do it by hand before you could ask.** Install through an
interactive picker, choose the right group, restart the agent, then type a
prompt — four manual terminal steps before anything happens. The copy-to-agent
affordance existed but sat at the bottom of the page, after the step it would
have replaced, and it copied only the prompt.

It leads now, with the whole thing in one visible block: install, make, open the
preview. It uses `hyperframes skills update` rather than the interactive
`skills add`, which is the command the README already says agents should run —
non-interactive, exactly the core set. The manual steps stay below for anyone
who wants to see them.

Plain code fence rather than the AgentAction component, deliberately: that
component renders a Copy button and never shows the request, so a reader copies
something they cannot read. Mintlify fences already carry a copy button and show
the text.

**Examples had four GitHub cards where one belongs.** Brief, Source, Revision,
Checks — two of them pointing into a gate-output report. That is showing
homework, not helping someone who came to see finished work. The render stays,
with one link into the folder.
2026-08-04 02:16:22 -07:00
ukimsanov b36f69a3c0 docs: drop the live embed from the introduction
Removed on request. The demo let a reader change a headline and an accent on a
ten-second composition, which undersold the thing the page is arguing for — the
Showcase wall above it does more for that in less space.

The component and its build apparatus go with it in #2977; nothing else on the
page referenced them.
2026-08-04 02:16:22 -07:00
ukimsanov 3d8db3f44e docs: drop the Source group to two columns
Rames' second item on #2976, fixed here because this is the line that owns it.
It was the only cols={3} in docs/, and all three cards carry body text — the
case docs/AGENTS.md names as hyphenating titles mid-word at this content width.

The rules file is #2976's deliverable, so it should not ship with the tree
beneath it holding the one counter-example.
2026-08-04 02:16:22 -07:00
ukimsanov ac9486980d docs: make the Reference Project links read as links
The four destinations were a single bordered four-column strip, which reads as
a tab bar — a control that switches the panel below it — when every cell is a
link to GitHub. Replaced with a CardGroup, which is what AGENTS.md prescribes
for choosing between destinations, at the two columns it also prescribes.
2026-08-04 02:16:22 -07:00
ukimsanov e60bef3f57 docs: rewrite the guides and landing pages
Rewrites the pages that survive the restructure so they lead with what a reader
can accomplish, and points them at the sections added in the previous commit.
Page set and navigation are unchanged here; only content moves.

Keeps the skill count in README. CLAUDE.md's catalog-maintenance rule requires
the count to live in README and CLAUDE.md, and both now agree with the 19
directories under skills/.
2026-08-04 02:16:22 -07:00
Ular Kimsanov 8e41fa17b9 Merge pull request #2978 from heygen-com/docs/site-structure
docs: rebuild the site structure — add sections, retire superseded pages
2026-08-04 02:11:01 -07:00
ukimsanov 1a82615f64 fix(docs): drop the nav entry that redirects away from itself
My rebase of this branch onto the squashed main resolved a docs.json conflict by
keeping both sides. One of those sides was a deliberate deletion: this PR removed
"contributing/studio-manual-dom-editing" from the sidebar precisely because it
also became a redirect source pointing at /studio/canvas. Re-inserting it left a
Contributing entry that bounces the reader somewhere else when clicked.

Nav collision count is back to 0. The redirect and the page file are untouched;
contributing/canary-rollouts stays, since that one genuinely arrived from main.
2026-08-04 01:47:10 -07:00
ukimsanov 8b98b41eed fix(docs): keep the changelog and weekly archive reachable
Rames' review on #2978. Two pages left the sidebar without a redirect and
without being deleted, so they survived only as direct URLs: `docs/changelog.mdx`
and `docs/weekly-updates.mdx`.

Not deliberate, and the stack says so — #2979 upgrades `weekly-updates.mdx`,
importing DocsVideo and converting four raw <video> tags. You do not invest in
a page you meant to retire, and it carries `rss: true`, so it is a subscribable
feed. `product-updates.mdx`, which this stack adds to both the nav and the
footer, links to `/changelog` three times and `/weekly-updates` once. One of
those is advice to read the release archive before upgrading a production
workflow.

Both are back in the Explore group next to Product updates, which is where a
reader looking for "what changed" would go.

Worth naming why the verification missed it: the checker walks navigation → file,
which is why it correctly reported zero dangling entries. The file → navigation
direction — a page that exists, is not in the sidebar, and has no redirect — was
never checked, and that is exactly where these two sat.

`--check-redirects` on the existing `mint broken-links` step closes the adjacent
gap: it resolves every redirect destination, so a future restructure cannot
leave a redirect pointing at a page it removed. It does not catch the orphan
case above.

Also retargets `/guides/pipeline`. It pointed at `/concepts`, which explains how
a project is put together; the retired page was a seven-step process. `/workflows`
is the closer intent. The old step 3, "Strategy & Messaging", has no successor
anywhere in the docs — worth deciding deliberately rather than routing around.
2026-08-04 00:39:31 -07:00
ukimsanov 52f3eb5b27 docs: retire superseded pages behind redirects
Removes the thirteen pages the rebuilt sections replaced, and claims a redirect
for each one in the same commit so no published URL starts returning a 404.

Each destination is the page that now answers the question the retired page was
answering — the Studio pages for editor topics, the workflow guides for the
per-source how-tos, Troubleshooting for the mistakes list.
2026-08-04 00:39:31 -07:00
ukimsanov 6fe5b4f82a docs: add the Studio section, workflow guides, and section hubs
Adds the pages the rebuilt navigation is organised around, and moves the sidebar
onto that structure. Every entry points at a file that exists after this commit;
nothing is rewritten or removed yet, and no redirect is claimed yet.

- Studio (13 pages) — the editor had no section of its own
- One guide per creation workflow, matching the shipped agent skills
- Section hubs: Concepts, Catalog, Developers, Deploy, Workflows, Help,
  Go further, Product updates, 30 Days of HyperFrames

Pages a later commit retires drop out of the sidebar here but still exist and
still resolve, so the site stays navigable at this commit.

Incidentally fixes a link to /concepts from the Lambda migration guide that is
broken on main today — the destination did not exist.
2026-08-04 00:39:31 -07:00
Ular Kimsanov edfe66a953 docs: add the shared page components and the Reference Project (#2977)
* docs: add the shared page components

Adds the six React snippets the rebuilt documentation pages compose against,
plus the styles they need. Nothing imports them yet, so this lands with no
user-visible change and no navigation churn.

- DocsVideo / ShowcaseWall — the film player and the Showcase grid
- LiveReferenceProject — embeds the Reference Project via <hyperframes-player>
- WorkflowChooser, AgentAction, and the two grid snippets

The scrub indicator is a timecode bubble rather than a thumbnail. Mounting a
second <video> with the same src to drive a preview frame made every page
carrying a film download the whole file twice, which is not worth a thumbnail.

* docs: add the Reference Project example

One real 10-second project the documentation can point at instead of describing
a hypothetical one: a live capture of example.com, synthesised narration, and
caption timings measured from that narration. It passes its own gates —
`hyperframes lint` clean, `hyperframes check` passed, 28/28 text checks WCAG AA.

No page imports it yet, so this lands without touching navigation.

Only the two WAV masters exceed the repository's 500 KB non-LFS limit, so only
those go through LFS. The MP3 stings and the capture PNG stay plain, which keeps
the example usable after a clone without `git lfs pull`.

`bun run docs:bundle-reference` regenerates the single-file embed the
Introduction page loads from the CDN.

* docs: keep the Reference Project verification report

The Examples page links this file twice — as "What changed after review" and
as "The real verification report" — in the section that makes the project's
brief, source, revision notes, and checks public end to end. It is a published
artifact, not leftover scaffolding.

* docs: state the Reference Project embed's isolation contract

The composition is fetched from the CDN and handed to the player as a blob:
URL, which inherits the docs origin, and <hyperframes-player> sandboxes its
iframe with allow-scripts + allow-same-origin. So the embedded composition runs
with script access to this origin.

That is a consequence of how the player works — it drives seeking through the
iframe's document, which a cross-origin frame does not expose — not something
this component can fix. Serving the CDN URL directly would isolate the frame
and break playback.

The guard is therefore the source, so the comment says so out loud: src must
stay a first-party path we publish, never user- or community-supplied HTML.

* fix(docs): resolve reduced-motion on the first render, and the embed's dep gap

Both defects from Rames Jusso's review on #2977. Neither is visible today
because nothing imports these files yet, which is what makes them cheap now.

**Reduced motion resolved one paint too late, in all three grids.**
`useState(false)` plus a `matchMedia` read in an effect meant the first
committed render always emitted `<video src autoPlay loop>`; a reduce-motion
visitor had 6 + 8 + 4 tiles already fetching before the attributes came off.
`autoPlay` also overrides `preload="metadata"`, so those were the files, not
metadata probes — and dropping `src` with no following `load()` is not a
reliable abort. A lazy initializer knows the answer on the first render.

**LiveReferenceProject never sent the initial variables.** The sending effect
read `playerRef.current`, assigned by the effect above it on the commit where
`compositionSrc` lands — a commit with nothing in the sending effect's dep
array. So it ran once against a null ref and never again. It looked correct
only because the three defaults match what the composition already renders.

Also from the same review:

- The object URL could outlive its revoke: once the body resolves, `abort()`
  no longer stops the chain, so the blob could be minted after cleanup ran with
  `objectUrl` still undefined. Same `cancelled` guard the effect above uses.
- `postMessage` targeted `"*"` while the isolation comment argues the frame is
  same-origin. Naming `window.location.origin` turns that prose guard into an
  enforced one.
- Nothing reached a terminal state when the player script never arrived:
  `whenDefined()` does not reject, and a later mount reuses the tag without its
  error listener. A CSP rule or content blocker never fires `error` at all.
  A deadline covers every path instead of sitting on "Loading…" forever.
- `loadFailed` was never cleared, so one transient failure stuck.
- The README claimed a clone works without `git lfs pull`. It does for the
  visuals; both WAVs are pointers and they are the bed and the voiceover, so
  the captions would play over silence. Says so now.
- The bundler stripped trailing whitespace document-wide while inlining the
  runtime, which reaches inside script template literals where those spaces are
  data. It also assumed a literal `<head>` and would silently ship an embed with
  no `<base>`. Strip removed, anchor asserted.

Copilot's five "missing hook imports" comments are wrong — Mintlify pre-injects
the hooks, and `TemplateCard.jsx`, cited as the counter-example, uses the
`export function` form the same page says is unsupported.

* fix(docs): stop preview loops when Reduce Motion is turned on mid-session

Miguel's changes-requested on #2977. He is right about the mechanism: dropping
`src` and `autoPlay` through React props neither pauses a playing element nor
aborts its selected resource, so a visitor who turned Reduce Motion on with the
page already open kept every tile running.

Measured in a browser rather than argued from the spec, same clip, same
sequence:

  playing                 paused=false  t=2.90  readyState=4  networkState=1
  React props only        paused=false  t=3.90  readyState=4  networkState=1
  + pause/removeAttr/load paused=true   t=0     readyState=0  networkState=0

The middle row is the bug: time still advancing, resource still held.

Rames' follow-up asked for a remount-to-poster instead, because a video that
ends with `src` removed holds its last frame and `poster` only paints before
playback begins. `load()` covers that too — it drops readyState to
HAVE_NOTHING, which is precisely the state that paints the poster. Confirmed
side by side on screen: the React-props-only tile sits on an arbitrary mid-clip
frame, the pause/load tile shows the poster again. So no remount is needed.

The guard cannot be shared as code — Mintlify compiles each snippet in
isolation and forbids one importing another — so it is copy-pasted into all
three grids. A duplicated invariant is the kind that rots, and a rendering test
would mean adding React to a repo that only carries it inside packages/studio,
plus mocking Mintlify's hook-injection contract with a mock that can stay green
while the page breaks. `scripts/check-docs-snippet-motion.mjs` asserts the
source instead, wired into `bun run lint`, with unit tests covering both edges.

That gate immediately found `docs/snippets/TemplateCard.jsx`: autoplays with no
reduced-motion handling at all. It is imported by zero pages, and it uses the
`export function` form Mintlify's constraints page says is unsupported, so it
would not work if it were. Deleted rather than fixed.

* refactor(scripts): split the motion guard into named predicates

fallow flagged findMotionGuardViolations at CRAP 42 — a finding this branch
introduced, so it gets fixed rather than suppressed, same as the catalog
generator earlier in the stack.

The two conditions are now their own predicates behind a small requirements
table, which drops the branch count under the threshold and makes each rule
readable on its own line. Same output, same tests.

* fix(docs): move the stop effect above ShowcaseWall's early return

Rames' changes-requested on `e1a03c63`. The effect I added in the previous
commit landed below `if (open) return`, so `ShowcaseWall` called five hooks on
the grid render and four once a tile was open. That is a conditional hook:
clicking a tile — the component's primary interaction — threw "Rendered fewer
hooks than expected".

Worth naming why it landed in one of three. `workflow-chooser` and
`advanced-path-grid` have no early return, so the same paste position was fine
there. `ShowcaseWall` is the only one with a conditional return and it got the
same copy. That is the duplication cost this script's own header warns about,
showing up in the commit that added the script.

**The bespoke gate could not have caught it, and now the generic one does.**
`.oxlintrc.json` already loaded the `react` plugin and never excluded `docs/`
— only `.prettierignore` does, which is why formatting is not a finding here
but linting reaches these files. Naming the two hook rules in an override
scoped to `docs/snippets/**` reports this bug directly, and also reports the
`compositionSrc` dependency gap from round one that was found by reading.
Verified both ways: reintroducing the conditional hook produces
`react-hooks(rules-of-hooks)`, and `bunx oxlint .` is clean repo-wide, so
nothing lit up in `packages/studio`.

**Two holes in the script itself, both from the same review.**

It matched whole files while the invariant is per component, so a second
unguarded grid in `docs-video.jsx` would have ridden in on `ShowcaseWall`'s
guard. It now splits by component. That immediately surfaced the distinction
between a component that decides to autoplay and one that forwards its caller's
`autoPlay` prop — `DocsVideo` only ever plays because a reader clicked, so it
does not owe a preference check.

And `readsPreferenceLazily` never tied its halves: any lazy initializer plus
the media-query string anywhere in the file passed, which is the original bug
satisfying the check written to prevent it. The query now has to sit inside the
initializer's own expression.

Both holes have tests. fallow is clean at 0 introduced.

* fix(scripts): close the two silent gaps in the motion gate

Both from Rames' approval pass on #2977, and both found by running these
functions rather than reading them. Both fail the same quiet way: a component
`autoplays` misses is filtered out before any requirement runs, so the gate
reports zero problems instead of a violation.

`autoplays` had become narrower than the version it replaced. Excluding the
`autoPlay={autoPlay}` passthrough was right, but the replacement only matched
`autoPlay={` or `autoPlay` alone on a line, so `<video autoPlay muted />` on one
line slipped through. Restored the old breadth. Two things are stripped first
rather than one — the passthrough, and the prop's own default in the signature,
which is a declaration and not a use. Without the second strip, `DocsVideo` is
asked to own a decision it only forwards.

`splitComponents` anchored on `^export`, so anything not exported folded into
the previous exported component and inherited its guard. Same hole as the
whole-file match, narrowed from file scope to non-export scope. The anchor no
longer requires `export`.

Ten tests now, including his exact examples for both.

* docs: remove the live-composition embed and its build apparatus

The Introduction no longer carries the embed (removed in #2979), and nothing
else used any of this: the 200-line snippet, 26 CSS rules, the bundler that
built the single-file HTML for the CDN, its npm script, and the README section
explaining how to regenerate it.

The Reference Project itself stays — Examples, Developers, and Go further all
link to it as the worked example; only the interactive embed of it is gone.

This also retires the isolation contract I documented two rounds ago. That
comment existed because the embed handed CDN HTML to a same-origin blob; with
the embed gone there is no such surface to reason about, which is a better
outcome than a comment explaining why it was acceptable.

* docs: remove the AgentAction snippet

Its only consumer is gone. The Quickstart now shows the agent instruction in a
plain fence instead, because this component rendered a Copy button and never
displayed the request — a reader copied text they could not read, which is the
wrong shape for the one affordance a non-technical visitor depends on.

Mintlify fences already carry a copy button and show their contents.
2026-08-04 00:37:48 -07:00
Miguel Ángel 0a70ba6717 fix(studio): scope timeline ease focus lifecycle (#2710) 2026-08-03 23:06:46 -07:00
Vance Ingalls 8c6cf90ff9 chore: release v0.7.91 2026-08-03 22:48:00 -07:00
Vance Ingalls 71fd96bbf1 Merge pull request #2854 from heygen-com/feat/canary-rollouts
feat(core): percentage-based canary rollouts + calibration experiment
2026-08-03 22:35:37 -07:00
Miguel Ángel 677fd31e9a perf(studio): stabilize virtualized beat gestures (#2709) 2026-08-03 21:35:40 -07:00
Miguel Ángel 423c5ffadb fix(studio): scope timeline context targets (#2708) 2026-08-03 21:19:49 -07:00
Miguel Ángel 4e7fcf7f2a fix(core): resolve sub-composition sibling asset paths everywhere (#2994)
Extends the studio-preview fix to the render path and the asset-discovery
utilities, which share the same resolver and had the same defect.

`rewriteAssetPath` takes an optional `assetExists` probe. A plain relative ref
authored in a sub-composition (`_shared.css`, `clip.mp4`) is re-pointed at the
composition's own directory when that sibling exists on disk; project-root refs
with no sibling (the registry's `assets/logo.png` convention) stay as authored.
Callers that can see the filesystem supply the probe, so the module stays free
of node:fs.

Also fixes a second defect in the inliner: `<head>` <link> hrefs and external
script srcs are hoisted into the root document but never went through the
rewrite at all, so even the documented `../` form escaped the project and 404'd
at render time.

Wired into the preview bundler, the producer compiler, the studio preview
builder, the HEVC preview lint, the project lint's asset scans, publish proxy
baking, and media-treatment source resolution.
2026-08-03 21:13:35 -07:00
Miguel Ángel 91d14744a0 perf(studio): virtualize timeline marquee selection (#2707) 2026-08-04 06:03:21 +02:00
Miguel Ángel bb5c603870 perf(studio): stabilize virtualized timeline drops (#2706) 2026-08-04 05:45:45 +02:00
Miguel Ángel 44bee4c3cf perf(studio): stabilize virtualized keyframe retiming (#2705) 2026-08-04 05:14:09 +02:00
Miguel Ángel f52398ceef perf(studio): stabilize virtualized clip gestures (#2704) 2026-08-04 04:58:15 +02:00
Miguel Ángel cbd8a77d16 fix(studio): resolve sibling asset paths in sub-composition previews (#2983)
Fixes #2956

## What

A composition in a subdirectory that references a **sibling** file (`<link rel="stylesheet" href="_shared.css">`, not `../_shared.css`) is now resolved against the composition's own directory when building the standalone sub-composition preview page.

## Why

The preview page borrows the project-root `<base href="/api/projects/:id/preview/">`, but the path rewriter only rewrote `../`-prefixed paths. So `design/styleframes/frame-01.html` referencing `_shared.css` was served unrewritten and the browser requested `/preview/_shared.css` → **404**.

With its stylesheet missing, the frame renders unstyled: `body` has no background, and the thumbnail generator's transparent-body fallback paints it `#1c2028`. Result: dark navy thumbnail with unreadable dark text on every styleframe in the Board view.

The report attributed this to project scale (~21 sibling files). It is not scale-related: a 2-file project reproduces identically, and the same file moved to the project root renders correctly. The trigger is **composition-in-a-subdirectory + relative sibling asset ref**.

Reproduced before the fix (single `curl` against the thumbnail endpoint, plus a direct headless capture of the preview URL):

```
HTTP 404 http://localhost:5190/api/projects/big/preview/_shared.css
body bg: rgb(28, 32, 40)
```

## How

`resolvePreviewAssetPath` in `packages/studio-server/src/helpers/subComposition.ts`, applied through the single rewrite pass all three dispatch branches (template / full-doc / fragment) already share, so `src`, `href`, inline `style` urls, and `<style>` blocks all get the same rule:

1. `../` paths keep resolving against the composition dir (unchanged, shared with the producer's inliner so preview and render agree).
2. Any other relative path is re-pointed at the composition's directory **only when that sibling file exists on disk**.

The disk check is what keeps the two conventions apart: registry blocks are installed into a subdirectory but reference project-root assets (`assets/logo.png`), which are already correct under the root base and have no sibling on disk, so they are left untouched.

Not changed: the `#1c2028` transparent-body fallback in the thumbnail generator. It is correct for genuinely transparent compositions; the illegibility was a downstream symptom of the 404.

## Test plan

- [x] Unit tests added/updated — two tests in `subComposition.test.ts`: a red-first regression guard for the sibling `<link>` / `<img>` / `url()` case, and a guard that project-root-relative refs with no sibling on disk stay untouched.
- [x] Manual testing performed — reproduced the dark thumbnail on a generated project (21-file and 2-file variants both reproduce), then confirmed the same URL renders the white-to-lavender gradient with legible text after the fix, with no 404 in the network log.
- [x] `packages/studio-server` suite green: 29 files / 402 tests. Lint, format, typecheck clean.
- [ ] Documentation updated — n/a
2026-08-04 04:09:50 +02:00
Xinyu YANG 70cc4f59a1 fix(cli): stop bare --frame-check from swallowing the next flag (#2966)
citty parses string options greedily, so a bare --frame-check consumed
the following flag as its value (--caption-zone silently dropped,
--json disabled) and failed with an error blaming --caption-zone.

Normalize rawArgs at the check command boundary (bare --frame-check
followed by a flag or at end becomes --frame-check=), attribute
frame-check spec errors to --frame-check, and detect dash-prefixed
values with corrective guidance.

Fixes #2965
2026-08-04 04:03:04 +02:00
James Russo 127eb19371 fix(producer): correct short VFR frame coverage (#2936)
* fix(producer): correct short VFR frame coverage

* fix(engine): keep VFR extraction seek-local

* fix(engine): match ffmpeg decimal frame boundaries

* fix(engine): preserve exact extraction frame rates

* fix(engine): key frame cache by exact rate
2026-08-03 18:42:03 -07:00
James Russo 9792c32950 fix(producer): reject asset media type mismatches (#2937)
* fix(producer): reject asset media type mismatches

* fix(engine): document read-only AVIF probe

* fix(engine): bound read-only AVIF brand probe

* fix(producer): make media preflight lifecycle-safe

* fix(producer): reconcile runtime media before preflight

* fix(engine): avoid writable file-open detection

* fix(producer): close runtime media preflight gaps
2026-08-03 18:16:41 -07:00
James Russo 1d0d4d8939 docs(changelog): weekly digest 2026-07-27–2026-08-03 (#2969)
* docs(changelog): weekly digest 2026-07-27–2026-08-03

* docs(changelog): embed weekly video for 2026-07-27 to 2026-08-03
2026-08-03 15:08:01 -04:00
WaterrrForever bc6dbc7e21 fix(cli): stop dropping queued telemetry when process.exit races the final flush (#2970)
* fix(cli): stop dropping queued telemetry when process.exit races the final flush

Two exit-path defects introduced by the 0.7.65 process-lifecycle refactor:

1. The 'exit' handler returned early once finalizeCli had started, which
   also skipped the flushSync() fallback. When an agent-pipe EPIPE killed
   the process mid-flush (the NORMAL teardown under Claude Code / Codex),
   the still-queued render_complete was silently dropped — fleet delivery
   fell from ~90% (0.7.55-0.7.64) to ~35%. flushSync() is now
   unconditional: empty queue is a no-op, event uuids dedupe re-sends.

2. The EPIPE handlers set commandFailed unconditionally, so every piped
   successful render scored success:false in cli_command_result (fleet
   success rate collapsed 89% -> 5-25%). EPIPE now only marks failure
   when the pipe died before the render artifact was validated, matching
   the existing isRenderSucceeded() exemption on the uncaughtException
   path.

Regression tests cover both: flushSync-after-finalize, and EPIPE
before/after artifact validation.

* fix(cli): don't score a validated render as failed due to pre-artifact noise

Review follow-up: commandFailed can be set by noise that precedes artifact
validation — a stray unhandledRejection mid-render, or an EPIPE firing
before markRenderSucceeded on a run that still completes. Once the
artifact validates, that earlier noise must not flip the run's
cli_command_result to success:false. Genuine failures keep a non-zero
exit code and are still caught by the exitCode check.

Extracted commandSucceededForTelemetry() and applied it at both tracking
sites (finalizeCli and the exit handler), with a regression test.

* test(cli): pin the production-reachable producer of the stale-failure override

Review note: the pre-artifact-noise test drives the scenario with an
EPIPE, which only reaches 'render validates afterwards' because
process.exit is mocked — that sequence can't occur in production. Add a
test for the reachable producer: an unhandledRejection before validation
(the handler deliberately does not exit), followed by a validated render,
must score success:true at exit code 0. Verified red on the pre-override
cli.ts.
2026-08-04 02:02:55 +08:00
Miguel Ángel 3cf268e1e5 Merge pull request #2960 from heygen-com/fix/registry-transform-motion
fix(registry): animate mk card offsets with transforms, not top/left
2026-08-03 10:16:02 +02:00
Miguel Angel Simon Sierra ce7d75dbaa fix(registry): animate mk card offsets with transforms, not top/left
mk-background and mk-clone-wall-transition tween the card's `top`/`left`.
Layout properties snap to integer device pixels, so the move stutters under
the seek-by-frame capture engine (lint: gsap_non_transform_motion). Both
cards sit at top:0/left:0 in CSS, so the values carry straight over to x/y,
and in clone-wall the later scale composes cleanly with the translate.
Re-rendered both and diffed frames against the previous output — identical,
as intended: this changes how the motion is computed, not how it looks.

Adds scripts/lint-registry-items.mjs (bun run lint:registry-items), which
mounts each item into a throwaway project and lints it. Registry items ship
as `<name>.html`, so `hyperframes lint <dir>` fails with "No composition
found" and these items had never actually been linted — which is how both
errors reached main. Verified the script reproduces the original failure on
the pre-fix source.

Left as a local command rather than a CI gate for now; wiring it up needs
two prior fixes, noted in the PR.
2026-08-03 10:14:37 +02:00
Miguel Ángel 7e95630060 Merge pull request #2939 from heygen-com/feat/registry-theme-families
feat(registry): mk / yt / hw theme families + beat-freeze-cut — 30 items
2026-08-03 09:53:54 +02:00
Miguel Angel Simon Sierraandakiyoshisan1218 d3d286a1a5 feat(registry): add beat-freeze-cut
Beat-driven speed ramp, freeze-frame hit, and hard cut for music-led
promos and montages. Contributed as #2957; applied here as content so the
whole catalog set lands together.

Verified by rendering: 6s, clean, deterministic (the one Math.random hit
is a comment above a fixed pattern array). Opaque root, so no demo.html
is needed for its catalog preview.

Co-authored-by: akiyoshisan1218 <akiyoshisan1218@users.noreply.github.com>
2026-08-03 09:32:12 +02:00
James Russo 1e51eaec2c chore: release v0.7.90 (#2958) v0.7.90 2026-08-02 21:57:19 -07:00
James Russo d6191965cf fix: pin release publishing to merge commit (#2959) 2026-08-02 21:32:08 -07:00
James Russo 2339757377 fix: bound HDR and video extraction resources (#2955)
* fix: bound HDR and video extraction resources

* fix: trim negative video extraction preroll

* fix: skip invisible video extraction windows

* fix: preserve negative-start loop and held tails

* fix: cap finite video slots to source duration

* fix: bound held-tail frame extraction

* fix: plan from playable video duration

* fix: preserve open-ended held video tails

* fix: resolve held tails from decoded frames

* fix: normalize final-frame probe timestamps

* fix: handle unseekable final-frame sources

* fix: dedupe final-frame probes per render

* refactor: clarify output dynamic range contract
2026-08-02 20:21:54 -07:00
Miguel Ángel 67ffafb11c docs(contributing): refresh the catalog contribution guide (#2954)
The guide still described a 52-block registry, told contributors to run the
deprecated `validate` command, and listed gaps that have since shipped.

- Correct the counts: 113 blocks, 25 components
- `validate` -> `check` in the quick version
- Document the `demo.html` requirement for components (CI fails without it)
- Document `params` (drives the Studio customization panel) and the other
  optional registry-item fields
- Add the monospace caption floor and `fitTextFontSize()` to the quality bar
- Add a motion-review checklist: rules paired with a self-check question
- Replace the manual preview-MP4 step with what catalog-previews CI does
- Rewrite "What's Needed Right Now" by the job a shot does in a video, and
  drop the gaps that have shipped (karaoke, lower thirds, maps, news ticker)
2026-08-02 22:02:55 +02:00
Miguel Ángel 411ada0d90 chore: release v0.7.89 (#2953) v0.7.89 2026-08-02 20:25:14 +02:00
Miguel Ángel 59fe32b633 Merge pull request #2952 from heygen-com/fix/2951-ignore-generated-cache-watcher
fix(cli): ignore generated caches in project watcher
2026-08-02 19:51:43 +02:00
Miguel Ángel 1d01b9f2cf fix(studio): exclude generated caches from project metadata 2026-08-02 17:37:51 +00:00
Miguel Ángel 3eb7b1ffd5 test(cli): clarify watcher exclusion coverage 2026-08-02 17:25:17 +00:00