Commit Graph
3803 Commits
Author SHA1 Message Date
ukimsanov 199885b85e docs: put the slideshow demo on the slideshow page
Removing the eight-second teaser left this page with nothing at all, and the
replacement had been rendered and published hours earlier without ever being
wired in.

Twenty-seven seconds of a live deck: fragments arriving one key-press at a time,
a hotspot jumping to a branch slide, the back control returning to the exact
fragment it left, and presenter mode with real notes and a running timer. It is a
capture of the deck running, not a video of slides.

Guides with nothing to look at: 5.
2026-08-04 12:15:35 -07:00
ukimsanov b4648e0e5a docs: six pages get a real film instead of a six-second clip
Product launch, colour grading, faceless explainer, captions and recuts, motion
graphics, and voice and audio.

Five of these led with a six-to-eight second teaser. A six-second clip on the
page that teaches you to make explainers does not teach anything — it is a
placeholder that looks like content. Colour grading, the most visual feature in
the product, had no video at all.

The new films are 34 to 41 seconds and each shows a RANGE rather than one look:
four ways to tell the same idea with no footage; five caption treatments on one
untouched clip; correction, presets, curves, selective colour, all four scopes
and a LUT; narration with music ducking under it and effects on the beat. The
launch film is built by running the workflow on huly.io for real, so the page
that sells the workflow is showing its actual output.

They share one visual system, measured from HyperFrames own light-theme launch
films rather than invented: bone field, near-black type, coral accent, and the
product always a dark panel inset on the light field. Every one carries audio at
about -20 dB and is 1-2 MB.

The old teasers are removed from the three pages where they became a duplicate.
They stay in the workflow picker, which is where a six-second tile belongs.

Guides with nothing to look at: 15 at the start of this work, 5 now.
2026-08-04 12:14:26 -07:00
Miguel Ángel cf45c98454 fix(studio): respect GSAP transform ownership (#2986)
* fix(studio): respect GSAP transform ownership

* fix(studio): enforce GSAP edit ownership consistently
2026-08-04 19:08:02 +00:00
Miguel Ángel 532f06158b fix(parsers): preserve safe GSAP helper defaults (#2985)
* fix(parsers): preserve safe GSAP helper defaults

* fix(parsers): make helper inlining call-safe
2026-08-04 19:08:02 +00:00
ukimsanov 8dba394d4d docs: make the ten hardest-reading prompting pages readable
Measured across all 136 hand-written pages, the twelve hardest to read were all
in prompting/. These ten were untouched — 29% to 40% of their sentences ran over
28 words, against a site median under 15%.

  media-and-audio           39.7% -> 6.2%    15 semicolons -> 6
  visual-specs              37.1% -> 19.0%   11 -> 9
  weekly-updates            39.1% -> 0.0%
  runtimes-and-3d           34.1% -> 17.5%
  design-systems            34.0% -> 4.3%
  recreating-references     33.3% -> 7.3%    16 -> 1
  storyboards               31.9% -> 5.4%
  variables-and-templating  30.3% -> 7.3%
  generated-artwork         29.4% -> 9.1%
  explainers                29.1% -> 3.3%    12 -> 1

Word counts barely move — 1752 to 1748, 1633 to 1617 — which is the point. The
facts and worked examples all survived; the sentences carrying them got shorter.
weekly-updates is an archive, so no date, version or fact changed there at all.

One anchor came back: capstone#the-full-prompt-verbatim, which these pages link
to and which an earlier commit had already moved to #the-prompt-word-for-word.
Repointed. Branch total: 0 broken pages, 0 broken anchors.
2026-08-04 12:05:32 -07:00
ukimsanov 663f3e8325 fix(docs): the poster guard again, for the catalog index this time
The previous commit claimed all 597 media URLs returned 200. They did not — 13
still 403'd, and I pushed that claim without re-running the check after the fix.
The message was wrong; this is the correction.

Two files carry a poster, not one. Guarding the page generator left
catalog-index.json still pointing at 13 .png files that were never produced,
which is a broken thumbnail in the catalog grid rather than a missing one.
catalogPreviewFor now returns undefined when the file is absent, the same rule
the page preview uses, and the 13 stale entries are dropped from the written
index.

Verified after the change, not before: 584 media URLs on this branch, 0 dead.
2026-08-04 11:53:37 -07:00
ukimsanov a6217f5ed1 fix(docs): stop claiming a poster image that was never generated
My catalog rebuild added `poster="<name>.png"` to every preview. Thirteen items
have an .mp4 but no .png, so the page asked for an image that does not exist —
and a browser fetches the poster before the video, so those were thirteen 403s
on load. The merged version did not have this; I introduced it.

The generator checks the file now, the same way it already checks before telling
a reader to read a comment header that may not be there. Thirteen pages lose the
attribute; the videos are unaffected.

Also uploads the three weekly changelog re-encodes to the path they are actually
served from — hyperframes/changelog-videos/, not the docs image tree — which I
had got wrong, leaving three dead links in the previous commit.

All 597 media URLs on this branch now return 200.
2026-08-04 11:45:41 -07:00
ukimsanov 158d0fea1f docs: stop serving render masters as if they were deliverables
Thirty-two published videos were the raw render output. One 20-second catalog
preview was 60 MB at 25 Mbps — Blu-ray bitrate for a screen capture in a docs
page. Nothing autoplays, so no page shipped that silently, but a reader who
pressed play paid for it.

Re-encoded at 1280 wide, CRF 28, faststart, keeping audio where it existed.
Measured PSNR against the originals at ~38.5 dB, which is visually identical —
the first check I ran returned nothing and I mistook that for a pass, so this
number comes from a filter that actually worked.

Worth correcting one thing I said earlier: the catalog is not systemically
over-encoded. 190 videos, 0.36 GB, about 1.9 MB each. Six were over 10 MB. I
found two monsters and generalised from them.

Pages carrying more than 8 MB of video: 26 before, and the heaviest went from
140 MB to under 15 MB.

The durable fix is in generate-catalog-previews.ts, which had no delivery encode
at all — it published the render master directly. It has a web pass now, so the
next preview is born small instead of being cleaned up later.
2026-08-04 11:33:52 -07:00
Miguel Angel Simon Sierra 6703ea7e04 fix(engine): warn once per process about unverified hardware GPU 2026-08-04 11:04:35 -07:00
Miguel Angel Simon Sierra 131780fe96 fix(engine): verify explicit browserGpuMode=hardware instead of trusting it
Chrome's hardware GL args are advisory: with no usable GPU it silently
falls back to software WebGL and the capture runs at CPU speed. Run the
existing WebGL probe for explicit hardware mode too and warn loudly with
the platform's remediation. The requested mode is still honoured.

Fixes #2967
2026-08-04 11:01:16 -07:00
Vance Ingalls f9ec93459f Merge pull request #2945 from heygen-com/ffprobe-6-argv-sweep
fix(cli,core,lint,producer,studio-server): terminate ffprobe options everywhere, pin the contract
2026-08-04 10:43:14 -07:00
Miguel Ángel ac6fd5363b Merge pull request #3009 from heygen-com/release/v0.7.92
chore: release v0.7.92
v0.7.92
2026-08-04 10:43:02 -07:00
Ular Kimsanov 77f95e46e0 Merge pull request #2976 from heygen-com/docs/quality-system
docs: add documentation quality gates
2026-08-04 04:34:20 -07:00
Ular Kimsanov 836426812a Merge pull request #2975 from heygen-com/docs/catalog-presentation
docs: make Catalog visual and reproducible
2026-08-04 03:41:53 -07:00
ukimsanov 31366d724d docs: draw the project model instead of describing it twice
The page that explains how the whole thing works had one Studio screenshot, 13
table rows, and two ASCII trees doing a diagram's job.

Two mermaid diagrams now. The first is the page's central claim made visible —
agent, Studio, CLI, SDK and Player all reading and writing one project folder,
with the render falling out of it. The second replaces the ASCII composition
tree with the same structure carrying real times and the file each scene lives
in, which the ASCII version could not show.

Verified rendering in the browser, not assumed.
2026-08-04 03:30:16 -07:00
Vance IngallsandClaude Opus 5 1664fe6ad7 fix(core,producer,skills): unicode paths, non-Error rejections, shell callers
Three R3 findings.

The redactor's segment classes were ASCII `\w`, so `/数据/客户/秘密视频.mp4` and
`/data/客户/secret.mp4` went out verbatim — and the generic redactor also feeds
CLI telemetry and producer observation messages, where no known-path list
compensates. Segments are now defined by their delimiters instead of an
alphabet, which is correct for every script by construction rather than
requiring Unicode classes to be kept correct. The bare-relative lookbehind had
the same ASCII assumption and let a match start mid-token, redacting
`客户/秘密/视频.mp4` to `客户[path]`; it is now a token boundary, and
bare-relative runs before absolute so it claims the whole token.

sanitizeProbeFailure cast the rejection reason to Error and read `.message`.
An injected probe can reject with anything, so `Promise.reject("failed")` gave
`undefined` and threw inside the redactor — converting a returned failure
result into a rejected promise. Normalized at the boundary, and
redactKnownPaths no longer throws on a non-string.

The contract only admitted .ts/.js/.mjs/.cjs, so it missed shipped shell and
Python callers. frame_strip.sh passed a user-controlled path as ffprobe's last
positional with no terminator; render-and-composite.sh had four more. Both
fixed, and the sweep now covers .py/.sh. Python list argvs are bracket
literals so they get the same position check; shell command lines get a
separate presence check, because checking position there needs a shell parser
— stated as the weaker guarantee it is rather than implied to be equal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:18:21 -07:00
ukimsanov f595a7d683 docs: the last five blind guides now show the thing
Rendering, Performance, Deploy, Figma and Bring in a design.

**Figma is the one worth reading about.** The first attempt failed honestly — no
token, no file key, so it refused to fake an import and built a generic design
handoff instead, where both panels looked identical and nothing was demonstrated.
A real token and file key were sitting in bug-bash-figma-hf. With genuine access
it makes the actual point: the same cursor and the same clicks land on a PNG export
and on the imported HTML, and only the HTML responds — the field takes text, the
button runs through its states. One side is pixels, the other is a component. It
replaces the placeholder comment on Bring in a design too.

**Performance is measured, not asserted.** 25.0s against 9.8s as the median of
three runs at 1920x1080 over 300 frames, one CSS declaration responsible for the
whole gap, and a pixel comparison at 43.7 dB PSNR confirming the picture did not
change. Real numbers from real runs on this machine.

**Rendering** shows the loop end to end. **Deploy** shows the four places a render
can run and labels anything not executed live.

All four carry their own audio at about -19 dB. Each was checked on a contact
sheet and for dead frames before publishing.

Guides with nothing to look at: 15 at the start of this work, 5 now. The five
left are pages where a video would be decoration — authentication, feedback, a
decision table, a comparison that carries code, and 4K, which is a paragraph
about a flag.
2026-08-04 03:14:43 -07:00
ukimsanov d16f6e69d7 docs: move 19 anchor links onto the headings that now exist
The capstone and determinism rewrites renamed headings that other pages point at.
Eighteen prompting pages linked to capstone#the-full-prompt-verbatim, which is now
'The prompt, word for word', and the rewritten determinism page invented an anchor
on frame-adapters that was never there.

Moved the links rather than restoring the old headings — 'the full prompt,
verbatim' is exactly the register the rewrite was cleaning up.

Broken anchors site-wide: 0. mint broken-links does not check anchors, so none of
this would have failed CI.
2026-08-04 03:09:00 -07:00
ukimsanov eb199bd159 docs: make the two hardest-reading pages readable, and draw the two core concepts
**Prompting.** These were the only two pages that stayed hard to read once the
sentence counter was corrected for bullet lists. Capstone went from 46% of
sentences over 28 words to 20%, and 53 semicolons to 42. Motion went from 37% to
15%, 26 semicolons to 4, and lost six table rows that were prose in a grid. Both
keep every technique and every worked example — the length barely moved, the
sentences did.

**Concepts.** Compositions and Determinism explain how the whole thing works and
showed the reader nothing. Each has one mermaid diagram now: how an HTML file
with data-* timing becomes a numbered sequence of frames, and why frame 90 always
comes out the same — what is locked before frame 0, and the three things that
break it.

Mermaid renders natively in Mintlify, so these cost no asset, no CDN upload and no
regeneration. Verified in the browser rather than assumed; the pattern is the one
proven on concepts/frame-adapters.
2026-08-04 03:08:04 -07:00
ukimsanov 8762887a5b docs: four more guides show the thing instead of describing it
HTML in Canvas, MCP, Skills, and Images and video all had nothing to look at.

**HTML in Canvas** puts a real HTML dashboard beside the same pixels running as a
live WebGL texture, bent by a shader while the DOM underneath keeps updating and
the texture re-captures every frame. That is the whole feature in one shot.

**MCP** is an honest reconstruction and says so on screen for its full length —
"Illustration of the flow". It could not reach a live connector, so it does not
pretend to have one. Describe the video, revise it in plain language, ask for a
format, get the render.

**Skills** shows a real run of the install command, then the router reading two
different requests and picking a different workflow for each.

**Images and video** walks the five basic moves — import, place, crop, trim, swap
the source — adding one attribute at a time on the left with the result on the
right. It serves both /guides/media and /guides/video-components.

All four carry a music bed from HeyGen's own catalogue at about -22 dB. Each was
checked on a contact sheet before publishing, not taken on the session's word.

Blind guides: 15 down to 9.
2026-08-04 03:08:04 -07:00
ukimsanov 951c6ea45c docs: point two prompting links at headings that exist
Both went to /guides/color-grading anchors that the guide rewrite removed. The
content did not disappear — it lives on /reference/color-grading as 'Animate a
supported property' and 'Limit a grade to part of the frame'. A reader clicking
either landed on the right page and scrolled nowhere.

mint broken-links does not check anchors, so CI would never have caught this.
2026-08-04 03:08:04 -07:00
ukimsanov 60f09dfcfd docs: give every click-to-play video an audio track
Ten videos on the docs site played in silence. The player is click-to-play, so a
reader deliberately starts them and gets nothing — which reads as broken rather
than as a stylistic choice.

The music is from HeyGen's own catalogue, pulled through the media-use skill's
resolve path with the account already authenticated on this machine. That matters
more than convenience: the launch projects on disk carry a `launch-music-trimmed.mp3`
with no licence, no attribution and no metadata, and three Pixabay tracks. Neither
belongs on the official docs CDN, where objects cache immutably for a year.

Mixed at roughly -22 to -24 dB mean so it sits under the picture. Getting there
took two corrections worth recording: proportional fades, because a 1.2s fade-out
on a 5s clip eats half the audio; and seeking 11s into the bed, because its first
ten seconds are a quiet intro that dragged every short clip down to -27 dB.

The three Examples tiles are left silent on purpose — they are muted autoplay
loops, so an audio track would only be weight.

Verified: every video referenced anywhere under docs/ that is not a muted
autoplay tile now has an AAC stream, checked against the live CDN URL rather than
the local file.
2026-08-04 03:08:04 -07:00
ukimsanov 61cd3f1da2 docs: keep the audio on the Huly launch film
I stripped the audio track with -an when encoding for the web. The source has a
real mix at -19.2 dB mean / -3.1 dB peak, and the docs player is click-to-play,
so there is no reason for it to be silent. Re-encoded with AAC at 128k; levels
match the source exactly.

The other published films have no audio track at all to preserve — they were
rendered silent at source.
2026-08-04 03:08:04 -07:00
ukimsanov ef99809482 docs: show GSAP keyframes moving, and drop the weakest video on Examples
The animation guide had nothing to look at, which is a strange state for a page
about motion. It now opens with the keyframes launch film — real `gsap.to()`
source with a `keyframes` array on screen, and the same motion being reshaped by
dragging an ease curve in Studio instead of guessing numbers.

Examples loses the ten-second reference-project render. It sat directly beneath
four finished films and lost the comparison every time; the four cards above it
already carry the point that the project is open end to end. The cards stay, the
render goes.
2026-08-04 03:08:04 -07:00
ukimsanov 2a7d9bd0ad docs: rebuild the catalog generator, and show what background removal and HDR do
**The catalog generator destroys hand-written documentation on every run.** It
does `rmSync(dir, { recursive: true })` on both catalog folders before
regenerating, so any section a human added to a generated page is deleted the
next time anyone runs it. There is now a carry-forward pass that reads those
sections off the existing pages first and re-emits any heading the template does
not own. Note this prevents future loss only — the earlier regeneration in this
stack already removed what was there, and it is recoverable from git if wanted.

It was also clobbering `docs.json`: rebuilding the Catalog tab dropped its icon
and unlinked `catalog/index.mdx` from the sidebar entirely.

And 27 of 36 component pages told the reader to "see the comment header in the
file" when no such header exists. The generator reads the file now and only says
it when true.

On presentation: 1,365 table rows across 168 pages became 0. `## Details` was a
table whose rows were "Type: Block" and the duration; `## Files` was three
columns where most items have exactly one file. Both are one sentence now. The
preview leads the page instead of sitting under title, description, tags, a
warning and a credit — four of which Mintlify already renders from frontmatter.

**Two guides that showed nothing now show the thing.** Background removal plays a
real cutout with the matte magnified at the hair edge and at a shoulder a plant
overlapped, because those are where matting fails. HDR is the honest one: you
cannot show HDR brightness on an SDR page, so it shows the `ffprobe` verdict and
a measured round-trip — 4000 nits authored, 4012 back out — and says on screen
for all 26 seconds that it is not simulating anything.

Both films were checked before publishing: no black or frozen stretches, and read
at the real 590px docs column width rather than full size. Nav after regeneration:
307 pages, 0 dangling, 0 redirect collisions.
2026-08-04 03:08:04 -07:00
ukimsanov 72d4016f5f docs: rewrite four pages that told instead of showed
**Color grading** was the worst page in the docs by the measure that matters:
33 table rows against one image, for the most visual feature in the product.
Four of the five tables were restating jargon in a grid — "Waveform | Brightness
by horizontal image position" tells you nothing you could act on. Those are prose
now, and the scopes section explains what each one is *for* (skin tones land on
one known line of a vectorscope, so faces are quick to check). The control lookup
stays a table, because that one is a genuine lookup.

Four factual errors in the old page, each verified at source before changing:
Studio's group is **Grade** in the flat Inspector, not "Color grading" in Design;
the controls are `whites`/`blacks`, not "white point/black point"; LOG and HDR
were conflated into one sentence when they are different cases; and the
project-wide LUT copy refuses a relative path in favour of a URL or data URL, not
a "project-root path".

Two things the page never mentioned and should have: `hyperframes grade-compare`,
which renders every candidate look onto one reference frame, and the fact that
`data-color-grading` nests under adjust/details/effects — a flat object renders
nothing at all.

**vs Remotion** argued for 851 words with nothing to look at and no code. It now
carries the same three-second title card written both ways. The HyperFrames one
is a real composition, not illustrative markup: extracted and run through lint
(0/0) and the full browser gate — runtime, layout, motion and contrast all pass.
It also concedes what Remotion genuinely does better, because a comparison page
that only flatters us is not worth reading.

**Choose how to create** never said what the four options were before comparing
them. It opens with the actual decision now — they all give you a video, the
difference is what you keep — and a four-row table of what you walk away with.

**Bring in a design** loses the 7-row keep-vs-rebuild table for one plain rule,
and the bullet list that used semicolons.

Every internal link on all four checked against the real page set. Where a visual
belongs but no approved asset exists, there is a JSX comment marking the spot
rather than a URL that would 404.
2026-08-04 03:08:03 -07:00
ukimsanov 84b5160b66 docs: show the work on Media effects, Product launch, and Examples
Three pages that described capability instead of showing it.

**Media effects** named eighteen effects as a bulleted list of words and showed
four of them in one small PNG. A reader could not see what Kuwahara Paint,
Ordered Dither or CRT Curvature actually look like. It now leads with the
finished media-treatments showcase film — every effect family on real footage,
with labelled before/afters — and the "Choose the effect" table is gone, because
six rows were saying what four sentences say better.

**Product launch** embedded a 44-second cut of small UI panels on black. Replaced
with the film this workflow's own skill produced from huly.io: the product's real
screens, its type and colour, bold editorial layout. Verified before publishing —
no black stretches over 0.4s, no freezes over 1s, captions clean at full
resolution.

**Examples** gets its four project cards back. Cutting them to one link was my
misreading of the actual complaint, which was about the reference-project video
sitting underneath four finished films and losing to them.

Both new assets are published to the docs CDN under versioned filenames and
return 200.
2026-08-04 03:08:03 -07:00
ukimsanov da35679bf9 docs: widen the accordion exception to the pattern it actually covers
Rames' first item on #2976. The clause named one page while the tree has 48
accordions across eight files: 20 in the verified-example gallery it means, and
28 under per-page `## Variants` headings — which the component table two lines
above routes to `Tabs`.

That contradiction matters more than usual because this file instructs agents.
The next one asked to bring the docs in line reads 28 compliant blocks as
violations and converts them.

Named by pattern now, with the reason the Tabs row does not reach them: Variants
blocks hold long alternative prompts a reader picks one of to read in full, not
parallel ways to perform the same step. Pre-existing and untouched by this PR —
the fix is the sentence, not the tree.
2026-08-04 03:06:59 -07:00
ukimsanov e94a458fed docs: correct two contributor instructions that the source contradicts
Both P1s from Miguel's review on #2976.

**The weekly changelog command has a fourth output, and it is public.**
`changelog-process.mdx` listed three internal drafts. `outputWeeklyDraft` also
calls `prependDocsUpdate` on `--write` (`scripts/changelog-weekly.ts:236`),
which writes straight into `docs/weekly-updates.mdx`. A contributor following
the page would review three files and push a fourth unread. Now called out as a
warning, with the re-run behaviour, since that is what makes editing the entry
afterwards safe.

The same paragraph also said Weekly updates is "unlisted in the sidebar". That
was true when written and I made it false myself, putting the page back under
Explore in #2978 after Rames found it orphaned. Corrected rather than reverted —
the page belongs in the sidebar.

**Bare `hyperframes lint` cannot validate a registry item.** Both
`contributing.mdx` and `contributing/catalog.mdx` told contributors to run it.
The CLI resolves a project by looking for `index.html`
(`packages/cli/src/utils/project.ts`), and registry items ship as `<name>.html`
or `demo.html`, so it fails with "No composition found". This is not
theoretical: `scripts/lint-registry-items.mjs` exists precisely for this shape
and its header records two `gsap_non_transform_motion` errors that reached main
unlinted because of it.

Both pages now document `bun run lint:registry-items`, and the Catalog page
shows the install-into-a-scratch-project route for the full `check` gate rather
than claiming a bare `check` covers a registry item.

Verified by running the documented command, not just by reading the script.
2026-08-04 03:06:59 -07:00
ukimsanov fd90fd3e72 docs: correct the docs rules and label the Prompt Guide previews
Three things the quality layer asserted but the pages did not support.

changelog-process said "do not recreate docs/weekly-updates.mdx; that empty
public page was retired" while the page exists with a real curated feed. It
describes the page as it is: unlisted, reachable by URL and RSS, with versioned
notes still in the Changelog.

The Related-topics rule read as universal, but three different endings are in
use and each is right for its shape — Related topics on task pages, a single
Next line through the Prompt Guide's numbered sequence, and nothing on
reference and concept pages. The rule now says that, and every section matches
it; color-grading gained the Next line it was missing and no longer points at a
page this stack retired.

The vocabulary previews were 18 DocsVideo players in three unlabelled grids, so
a reader saw several near-identical clips with no way to tell smooth from
snappy. They are now native muted loops with visible captions, which is what
AGENTS.md asks for small preview loops in the first place.
2026-08-04 03:06:58 -07:00
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
Vance IngallsandClaude Opus 5 6c5403f7cd fix(producer): drop the ReDoS-prone literal-argv regex for a linear scan
CodeQL flagged js/redos on the all-literal argv matcher. It was right: the
`(?:"[^"]*"\s*,?\s*)+` form nests a quantifier inside a quantifier with an
optional separator, so whitespace can be matched two ways and a long
non-matching argv backtracks exponentially.

Replaced with a linear scan — find the spawn head, slice to the closing
bracket, and check the entries — plus small named helpers. Same behaviour: an
all-literal argv is treated as taking no input, an argv with a bare identifier
still has to be understood (verified by adding one and watching the guard fail).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:41:39 -07:00
Vance IngallsandClaude Opus 5 c81f68b592 test(producer): treat an all-literal probe argv as taking no input
CI runs the PR merged with main, so it saw a caller my branch predated:
`spawnSync("ffprobe", ["-version"])` in engine/src/utils/ffprobe.test.ts. That
is a capability check with no runtime path, so there is nothing to terminate,
but the unclassified guard flagged it as a caller it could not parse.

An argv whose entries are all string literals carries no input by
construction. Those are dropped before the check; an argv with a bare
identifier still has to be understood, verified by adding one and watching the
guard fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:29:19 -07:00
Vance IngallsandClaude Opus 5 255cf92915 fix(skills,producer): terminate ffprobe options in shipped skill scripts
The contract test only walked packages/*/src and only .ts, so it could not see
the shipped agent tools under skills/**, which are .mjs/.cjs. 19 call sites
there and in package tests were still missing `--` immediately before the
input while the suite reported the bug class closed — a dash-prefixed filename
is parsed as an option and fails the same way.

Sweeps packages/, skills/ and scripts/ now, including .mjs/.cjs and test
files (dither.test.mjs was one of the broken sites). Excludes only the
contract test itself, which documents the contract with example argvs
including a deliberately misordered one.

Two guards were fixed while widening: the terminator must never be inserted
after `-i`, which consumes the next token (a blind pass hit an ffmpeg input
and a base64 -i), and comment prose describing a spawn is not a spawn.

Also routes every audioPadTrim probe failure through one sanitizer at the
boundary. runFfprobeJson scrubbed its own stderr, but
defaultProbeVideoFrameInfo threw `no video stream in ${videoPath}` raw into
the public PadTrimAudioResult.error, and an injected probe can throw anything.
The redaction unit tests all passed with the caller wiring deleted; the new
public-path regressions fail without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:35 -07:00
Vance IngallsandClaude Opus 5 e79ab3ab31 fix(core,producer): redact bare relative paths and the known input path
The generic scrub still missed a relative path with no `./` prefix:
`customer/acme-secret/video.mp4` and `assets/bgm.mp3` reached telemetry
completely unredacted, because the absolute rule needs a leading slash and
the `./` rule needs the dot. Adds a rule for them that still leaves `N/A`,
`24/1` and `48000/1001` alone.

Shape matching is a net with holes by construction, so audioPadTrim now
also redacts the exact path it put in the argv, plus its basename, before
the generic scrub runs. It built the argv, so it does not have to guess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:35 -07:00
Vance IngallsandClaude Opus 5 d04569e37f fix(core): redact any path in telemetry, not an allowlist of roots
redactTelemetryString enumerated roots — /Users, /home, /opt, /tmp and a
handful more — so a project on /data, /Volumes, an NFS mount or any root a
user invented reached telemetry verbatim. Relative paths and bare basenames
were never redacted at all, and audioPadTrim routes raw ffprobe stderr
through this on every probe failure.

Now redacts by shape: absolute paths under any root (two or more segments,
so N/A and a 24/1 frame rate are not mistaken for one), relative paths
including dash-prefixed ones, and bare basenames with an asset extension.
URLs still keep their host and drop only the query.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:35 -07:00
Vance IngallsandClaude Opus 5 91a7cb1f5b test(producer): discover ffprobe callers and pin terminator position
The prior contract test scanned a hardcoded file list for a format flag
followed by a bare identifier, so it only matched the shape it was written
against. Mutation testing showed removing `--` from engine/utils/ffprobe.ts
and cli/commands/init.ts did not fail it.

Now walks packages/*/src and finds callers itself, checks that `--` is
immediately BEFORE the input rather than merely present, and compares
discovery against a manifest so a regex regression cannot silently stop
checking a known caller. A separate guard fails on any file that spawns a
probe binary but builds an argv this test cannot parse.

All 11 seams mutation-tested for both removal and misordering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:34 -07:00
Vance IngallsandClaude Opus 5 d0dbf11ef5 fix(producer,studio-server): finish the ffprobe argv sweep, pin the contract
The previous commit claimed "all nine now terminate their options". That
was false: `producer/src/utils/audioRegression.ts:307` still passed the
path bare, and it is production source used by the regression harness.
A repo-wide audit found two more in studio-server
(`mediaValidation.ts`, `mediaMetadata.ts`) — their current callers pass
absolute paths, so they were defence-in-depth rather than live bugs, but
the exhaustiveness claim should be true rather than narrowed.

Eleven sites total, all terminated.

Adds a SOURCE-level contract test, which is the gap that let this
happen twice. #2740 fixed one of ten sites and shipped a regression
asserting the argv of that single site, so CI reported the class closed
while nine invocations still parsed `-intro.mp4` as an option. A
per-site unit test has the same blind spot for site twelve; scanning the
tree does not. The test also asserts its own coverage list has not
shrunk.

Verification: engine 1300, lint 511, core 1431, studio-server 398, cli
init/webmAlphaCheck/whisper 146, producer utils 51, audioPadTrim 18.
Removing any single terminator fails the contract test by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:34 -07:00
Vance IngallsandClaude Opus 5 47564ab94c fix(cli,core,lint,producer): terminate ffprobe options at every call site
#2740 added `--` to one of nine independent ffprobe invocations, so the
bug class it closed stayed open everywhere else while CI reported it
fixed — the regression test asserts the argv of that single site.

Reproduced on ffprobe 8.1.1: an asset named `-intro.mp4` probes fine
through extractMediaMetadata but fails with "Missing argument for option
'intro.mp4'" in audio pad/trim (mid-render), `hyperframes init`, whisper
duration probing and webmAlphaCheck. hevcPreviewLint catches and returns
false, so a dash-prefixed HEVC preview silently passes the lint rule.

Terminated at all of them:
  producer/services/render/audioPadTrim.ts (x2)
  producer/plan-parity-analysis.ts
  cli/commands/init.ts
  cli/utils/webmAlphaCheck.ts
  cli/whisper/transcribe.ts (x2)
  core/mediaGradeAnalyzer.ts
  lint/hevcPreviewLint.ts

audioPadTrim's runFfprobeJson is a near-verbatim clone of the engine's
runFfprobe and structurally cannot add the terminator itself, because
callers bake the input path into `args`. It now asserts the terminator
is present rather than letting a dash-prefixed path through, takes the
same stdio ["ignore", ...] as the engine helper, and redacts its stderr
— it was throwing raw ffprobe output, which echoes the input path, into
logs and telemetry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:34 -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