mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
ce18acf075d56e44ebe6382edffc8f3baa96c2c8
119
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ce18acf075 |
feat(catalog): group the sidebar by what you came to make (#3194)
* feat(catalog): group the sidebar by what you came to make Nineteen shelves in one column is a list to read, not a menu to scan, and two of them were oversized: Effects held 54 items and Showcases 38, two thirds of which were near-identical code-snippet themes. The shelves now sit under eight openable sections named for what a reader is making, and the editor and terminal themes get a shelf of their own. Every existing shelf name survives underneath, so a bookmark into the catalog still lands where it did. Two things had to change to support nesting. The summary count walks sections as well as shelves, and the filter that preserves hand-written groups now recurses: a section holds groups rather than pages, so a check that only read `pages` found nothing generated in one, kept it as if a human had written it, and appended a fresh copy on every run. * fix(catalog): type the nested page count * fix(catalog): nest shelves inside pages so the sidebar renders them |
||
|
|
9734578e60 |
feat(registry): bring back the video-primitive moves (#3169)
Restores the 208 catalog items reverted after their previews 404'd in production, this time on the payload mechanism rather than the .html files that caused the outage. The generator no longer writes a preview document to docs/public. That writer, and the machinery under it, existed only to produce files the docs host discards, so it is gone rather than bypassed. Items now embed the composition itself via a payload, which is what the previous change already does for the items that were already in the catalog. The variables explorer is parked, not restored: it drove its preview through the same unpublished .html path, so it would have shown an empty frame. Items that declare variables get the live player plus the static variables table, and reconnecting the explorer to payloads is a follow-up. |
||
|
|
0f76305191 |
fix(catalog): keep the recorded video for canvas drawElement previews (#3171)
Eight blocks paint DOM into a canvas with ctx.drawElementImage(), which sits behind chrome://flags/#canvas-draw-element. A reader without the flag gets a preview that mounts, plays, and shows an empty canvas, which is what the 3D device blocks were doing even after their models were embedded correctly. The recorded video was captured by a renderer that has the capability, so it is the only preview these items can honestly show. All eight have a published mp4 and poster. |
||
|
|
470f802d9f |
fix(catalog): resolve assets a script loads by name (#3170)
The 3D blocks fetch their model with loader.load("models/iphone.glb"), which no
attribute or url() pattern can see, so the path survived into the payload
untouched. Inside a srcdoc iframe it resolved against the docs page and 404'd,
leaving vfx-iphone-device and ios26-liquid-glass mounted and playing with an
empty scene.
Names that resolve to a real file in the item's own directory now count as
references. They are deliberately weaker than markup references: a name that is
not a file is ignored rather than failing the item, so an ordinary string that
happens to look like a filename cannot push an item onto the video fallback.
Also clears an item's payload when a run decides it cannot build one, so the
page generator stops finding a previous run's file on disk and emitting a
player for a preview that no longer exists.
|
||
|
|
536165b6ef |
feat(catalog): play the real composition on catalog pages (#3168)
* feat(catalog): play the real composition on catalog pages Catalog previews were uploaded MP4s, published by hand to the CDN. Each page now embeds the composition itself, running in <hyperframes-player>, so a preview is the block rather than a recording of it. The composition is delivered as JSON under docs/public/catalog, and the player is mounted inside an iframe. Both are forced: the docs host publishes only JSON and images out of docs/public, and its MDX renderer strips unknown custom elements, so a player written into the page never reaches the DOM and an .html payload 404s in production. Assets are inlined as data URIs, which sidesteps the file-type restriction for fonts, scripts and models alike. 164 of 168 items build a payload; the rest keep their MP4 and say so. * chore: rebuild docs preview * chore: route isolation probe * chore: drop the route isolation probe page * chore: probe which asset types the docs host publishes * refactor(catalog): host preview assets instead of inlining them Fonts were being base64'd into every payload that used them, so a handful of files cost tens of megabytes in the repository to say the same thing over and over. Assets are now written once, content-addressed, under docs/public/catalog/assets and linked. Which types can be hosted was settled by fetching one file of each from a deployed preview: woff2, wav, mp4, svg and the image formats are published, glb is not. Types the host drops still travel inside the payload, because a link that 404s is worse than a larger payload. Also fixes the four type errors the scripts typecheck caught, all of them unchecked index access on a split() result. * refactor(catalog): share preview assets instead of repeating them * style: format the asset module |
||
|
|
c86d4013f5 |
Revert "feat(registry): the video-primitive moves, documented and customisable (#3090)" (#3162)
This reverts commit
|
||
|
|
3b53bfd2f7 |
feat(registry): the video-primitive moves, documented and customisable (#3090)
* feat(registry): add the video-primitive moves, and rebuild the catalog around them
Adds the motion primitives: 277 new components and the blocks that go with
them, plus the ui-primitives, themes and generators they are produced by. The
registry index goes from 176 items to 454, and the search catalog is rebuilt so
the set that is ranked is the set that can be installed.
Additive on purpose. An earlier pass of this port used rsync --delete, which
removed 101 files that exist on main because the incoming set is not a superset
of the current one: beat-freeze-cut and camcorder-hud among them. Whether the
re-port replaces those or sits alongside them is a product decision and not one
a sync flag should make, so nothing is removed here. If any of them are meant
to go, that belongs in its own commit where it can be seen.
The generator is ported too. Main's version only scans examples, so running it
without this change silently rewrote the index down to nine items. It also
rewrites example manifests from templates.json and will overwrite hand-edits;
those were reverted here after each run, and the diff is worth reading rather
than trusting.
Not covered. The 445 moves are not individually reviewed in this commit; the
machinery that ranks and installs them landed separately so it could be read on
its own. The internal evaluation corpus is deliberately absent: it is 1,400
files of briefs, gold labels and verdicts, and this repository is public.
* docs(catalog): publish the primitive and component pages
Adds the Mintlify pages for the moves this PR ships: 163 component pages, 13
primitive pages, and the navigation that lists them. Without these the moves
land installable and undocumented, which is the worse half of a catalog.
Three things left out deliberately.
The 78 MB of docs/public. Nothing references it: every page loads its preview
from static.heygen.ai, so those bytes would be weight in a public repo with no
reader. Checked rather than assumed, by grepping the pages for the path.
Pages for the thirteen moves that were specified and never built. They had
documentation but no registry item, so a reader would have followed a page to a
`hyperframes add` that fails. Their nav entries are pruned with them, and every
one of the 309 remaining catalog and primitive nav entries was verified to point
at a page that exists.
Spike and scratch files that sit alongside the real docs on the source branch:
qa-gallery.html, experiment pages, bundled player javascript. They are working
artifacts, not documentation.
Not covered: the pages are generated output and have not been read individually.
The nav is verified to resolve, and the previews load from a CDN this commit
does not control, so a broken image would show up in review rather than here.
* docs: list the primitive and component pages in the site navigation
The pages this PR adds were unreachable: nothing in docs.json pointed at them.
This appends a Motion primitives group to the existing Catalog tab and a
Primitives tab, both built from main's navigation rather than replacing it.
Copying the source branch's docs.json wholesale was the first attempt and was
wrong. That file describes a different site, tabs Documentation / Catalog /
Primitives / Packages / SDK / Reference against main's Guides / Studio /
Catalog / Developers, and it references pages only that branch has, so the
preview server reported six dead links.
Verified by running the preview and resolving every entry: 484 page refs, 0
dead, no warnings. Group-relative refs are why a flat existence check is the
wrong validator here: cursor resolves through catalog/components and mcp
through guides, so checking docs/<ref>.mdx flat pruned 22 entries that were
fine.
* fix(registry): restore what the port took from main's components
Two regressions this branch introduced into items main already ships. Both were
found by the repo's own gates in packages/cli, not by reading the diff, and
neither is visible to the no-deletions check: no file was deleted, the contents
of files were changed.
The four liquid-glass blocks stopped installing their library. main lists
lib/liquid-glass.iife.js as a second file on each; the port wrote the older
manifest over main's and dropped that entry. The file is still in git and still
on disk, it simply stopped being something `hyperframes add` writes, so the
installed composition's <script src="lib/liquid-glass.iife.js"> would have
resolved to nothing. Every one of the 294 registry-item.json files this branch
touches was then audited against main: these four lost a file entry, and no
item lost a top-level key.
Fourteen caption components gained an empty <video>. The port added
`<video id="wp-video" ...></video>` — no src, no <source> — to each component
and its demo. It renders nothing and the registry linter rejects it as
media_missing_src. Removed rather than given a placeholder, because main's
version of each of these composes over whatever the host composition provides,
so the element only ever added a broken node; a made-up src would ship a
reference to footage that does not exist.
The removal is deliberately surgical. Four of the fourteen also carry a
substantial rewrite from the port, and only the media element and the rule that
styled it are touched, so a blunt revert cannot take the rewrite with it.
Verified: 2540 CLI tests pass, `bun run lint` exits 0. Before this, three tests
failed.
* docs(catalog): play the real composition, and show what can be changed
Four changes to generate-catalog-pages.ts, so all 445 pages stay consistent
rather than 445 files being edited by hand.
The preview plays the composition instead of pointing at a video. Every new
page pointed at static.heygen.ai/<name>.mp4 and every one of those answered
403, so the reader got a black box where the whole point of the page is to
show them the thing. The objects were never uploaded and rendering 445 of them
would have to happen again on every change. The player is already the thing
being documented, so the page embeds it: the item's directory is copied under
docs/public and an iframe loads it through a small wrapper. 444 of 445 pages
play; the remaining one is a texture item that uses its own preview panel.
The iframe is not decoration. Compositions set styles on `body`, so dropping
the element straight into the MDX would put a composition's global CSS in the
same document as the documentation around it.
Three things this got wrong first, all found by opening the page rather than
reading the output:
- The wrapper loaded itself. `../<dir>/<name>.html` from inside preview/<dir>/
resolves back into preview/<dir>/. The player embedded the player and the
frame went black with a second set of controls shrinking into the corner.
- Copying only demo.html was not enough. Most demos are a mount shell whose
child carries data-composition-src="./<name>.html", so the sibling has to
come with it. Every URL answered 200 and the frame was still empty.
- `autoplay` and `loop` are not player attributes. Writing them did nothing
and every preview sat paused on frame 0 — which is blank for any
composition that animates in. The wrapper drives play() and loops on
`ended` instead.
The Variables table. generateParams reads `params`; every item ported from the
video-primitives work declares `variables`, a richer schema with a type, a
default and a range. 112 items carry one and not a single page showed it, so
the most useful thing on the page was the one thing missing.
Nav groups. `if (entry.type === "component") return "Effects"` was the
catch-all, so Effects held 267 of 445 pages: an alphabetical wall. Rules keyed
on tags that already exist in the manifests split it; the largest group is now
73.
An install command with a visible copy button. A plain code fence renders one
on hover only, and it was absent from the accessibility tree entirely. This is
the one line every reader comes to take. navigator.clipboard is unavailable on
insecure origins, which is exactly the local preview these pages are written
against, so the fallback path is load-bearing and is what was exercised in
testing.
Verified: 888 preview URLs fetched, 0 failures. Regenerating three times in a
row produces no change, after a first attempt where "Variables" was added to
GENERATED_HEADINGS with a capital V — the set is compared lowercased, so each
run carried the previous section forward and appended a new one.
Not covered: the 445 pages were not read individually. Coverage here is that
every preview resolves and that a page from each of the block and component
paths was opened and watched.
* docs(catalog): put the code on the page
A reviewer with no stake in the work compared these pages against shadcn/ui's
component pages and motion.dev, and returned one gap: the pages carry almost no
code, so they are pointers to a file the reader does not have yet. Its sharpest
example was the Variables table — names, defaults and accepted values, headed
"set the ones you want to change on the element", on a page that never shows an
element or the syntax for setting anything on one.
Two additions, in the generator so all 445 pages get them.
A snippet under the Variables table: the real mount element with
data-variable-values filled in from the item's own defaults, so it is
copy-and-run correct before it is edited. That is the syntax the demos actually
use, not an illustration written for the page.
The item's source, in a collapsed Accordion. These files run 99 to 463 lines,
so inlining them raw would bury everything else; collapsed, the code is on the
page and one click away. Accordion is already what these docs use for this.
A second reviewer, fresh, confirmed the change landed: it called the table and
snippet actionable rather than filler and said the collapsed source earns its
place.
Also here: the preview retries play() until the clock moves. `ready` can flip
before the runtime the player injects for a mounted sub-composition has finished
wiring up, and a play() landing in that window silently does nothing.
Both reviewers additionally reported every preview frozen at 0:00 and called it
fatal. It is not. The player's clock runs on requestAnimationFrame
(direct-timeline-clock.ts), browsers suspend rAF in a hidden tab, and the
reviewing tab was hidden: document.visibilityState read "hidden" while the
player reported ready and not paused, and a one-second rAF loop never completed
a single tick. Seeking the same composition by hand renders it correctly at any
offset. So the retry stops after ~15s instead of spinning forever, and the
comment says why an automated check of a background tab will always read 0.
Not covered: the reviewers' other standing finding, that only some items carry
variables at all, so the pages do not have one shape. 125 of the 206 items
tagged as a primitive declare none, and giving them variables means authoring
them into each composition, not editing metadata.
Verified: lint exits 0, the no-deletions gate passes, nav resolves 598 refs with
0 dead, and regenerating three times running changes nothing.
* feat(registry): give 55 primitives variables that actually do something
The catalog pages listed variables for 112 of 454 items and nothing for the
rest, so most pages could show a reader what a piece looks like but not what
they could change about it. This adds them to 55 more, taking the count to 167.
These are not metadata. A variable is only real if the composition reads it, so
each one is declared on the root, validated in the composition's own script, and
wired to something visible: travel distance, blur radius, direction, density,
accent family, tone, label text. Declaring a knob the code ignores would put a
table in the published docs that lies about the piece, which is worse than
having no table.
Every one falls back to its declared default when the incoming value is missing
or unrecognised, so a bad override degrades to the shipped look rather than to a
broken frame. With no overrides at all, each item renders exactly as it did
before: that was checked per item against `git show HEAD:` in a real browser,
comparing computed styles rather than eyeballing.
Four things this ran into that are worth writing down.
An apostrophe anywhere in a description terminates the single-quoted
data-composition-variables attribute and breaks the HTML parse. Every
declaration in the registry now parses; that is checked, not assumed.
Where a timeline drives GSAP's own y/scale/filter, GSAP writes inline styles
that beat any CSS custom property, so those knobs cannot be won from the
composition. Most of these items keep their motion in a user-owned "Timeline
integration" comment rather than in code, so no timing variables were declared
for them at all. A direction knob on a wipe can still be wired honestly, by
remapping clip-path inset sides through multipliers whose defaults reproduce the
original exactly.
Colour tokens that only reach a :focus-visible outline, or an element sitting at
opacity 0 at rest, render identically in a video. Those were skipped rather than
shipped as knobs that appear to do nothing.
CSS shorthand defaults need care: `border: var(--x, 0 solid transparent)` moves
computed border-color off currentColor even at zero width. Defaults were chosen
to reproduce the original computed style, not merely to look equivalent.
Not covered: 72 primitives still have no variables, and the UI-primitive demos
that scripts/sync-ui-primitives.ts mirrors are now stale for the converted
items. Nothing runs that script in CI today.
Verified: every declaration parses and deep-equals its manifest array, every
declared id is read by the composition, no demo.html changed, and lint exits 0.
* feat(registry): variables for 16 more primitives, and stop the snippet clipping
Takes the count from 167 to 183 of 454. Same contract as the last batch: each
variable is declared on the root, validated in the composition's own script,
and wired to something visible, because a knob the code ignores would put a
table in the published docs that lies about the piece.
The snippet under each Variables table was clipping. Its data-variable-values
payload is one long line and the code block cut it off mid-value, with no wrap
and no scrollbar, so the one line on the page that exists to be copied could not
be read. The fence now carries `wrap`. Worth noting how that survived: the
generated markdown was correct and every mechanical check passed. It only failed
in a browser, which is where it was eventually seen.
Two techniques this round that are worth keeping.
Where an accent has a themed token family, the knob sets a new custom property
consumed by that one surface, with a fallback to the existing token, rather than
overriding the shared accent. Default therefore sets nothing, so an externally
themed accent is not clobbered, and the non-default options still follow the
theme in both light and dark.
Where GSAP owns the property outright and no CSS multiply can win — number-wheel
animates `y` inline — the knob is wired at build time instead: extra revolutions
lengthen the digit strip and move the target, so travel changes while the resting
frame stays identical. That is a real answer rather than a skipped knob.
Motion knobs that multiply a timeline-driven custom property collapse to identity
at rest, so every one of them was verified with that property pinned to a
mid-flight value rather than at t=0, where all options look the same by
construction.
Not covered: 56 primitives still have no variables.
Verified: every declaration parses and deep-equals its manifest array, every
declared id is read by the composition, no demo.html changed, lint exits 0, and
the wrap fix was confirmed on the rendered page rather than in the markdown.
* feat(registry): variables for 21 more primitives
Takes the count from 183 to 204 of 454. Same contract: declared on the root,
validated in the composition script, wired to something visible, defaults
reproducing the pre-edit render exactly.
Three kinds of knob were turned down this round rather than faked, and the
reasons are worth keeping.
A knob that contradicts its own motion. The sheet panel could be moved to the
left, but the recipe drives GSAP x from the right, so the panel would slide in
from the wrong side while the control claimed otherwise.
A knob that needs two defaults. A separator length means width horizontally and
height vertically, so one token would be wrong half the time.
An option that is not an option. Two components were given a green accent that
probed byte-identical to their default, because the theme accent already is that
token. A row in the docs table that does nothing is worse than a missing row, so
it was replaced with one that differs.
Accent knobs set a new property with a fallback to the shared token rather than
overriding it, verified by rendering with an external accent in place and
confirming the default still yields to it. Motion knobs multiply a
timeline-driven property so they never fight the inline styles GSAP writes;
because those collapse to identity at rest, each was checked twice, once at rest
against HEAD and once with the driven property pinned mid-flight.
Verified: every declaration parses and deep-equals its manifest array, every
declared id is read, defaults match HEAD on computed styles and on a pixel hash
of the rendered element, no demo.html changed, lint exits 0, and the
no-deletions gate passes.
* feat(registry): variables for 4 more primitives, and make manifests agree with their compositions
Takes the count to 207 of 454.
Four items carried a different description for their exit variable in
registry-item.json than in their own data-composition-variables. The catalog page
renders the manifest, so the published table described the knob one way while the
composition header described it another. The composition wins: it is the file
that implements the variable and the declaration is what the runtime reads.
The skill docs no longer describe a hosted tier, since the CLI now ships the two
local tiers only, and skills-manifest.json is regenerated to match.
* feat(registry): variables for 4 more primitives
Takes the count to 211 of 454.
Two knobs are worth calling out because they touch things the timeline also
touches. skeleton-block slide multiplies the driven row offset, so it is
identity at rest and only bends the middle of the move. slider value sets the
resting fill together with the readout text, aria-valuenow and aria-valuetext,
so all three agree; a composition that tweens the fill takes over from there and
owns the readout, which the comment header states plainly rather than hiding. A
multiplier was rejected there because a 0 to 1 tween would push the fill past
the end of the track.
Knobs on elements that sit at opacity 0 at rest were kept only where the shipped
recipe reveals them, and verified with the reveal forced on as well as at rest.
Skipped: an aria-label string knob that never renders, and an accent token
declared in one item CSS that nothing consumes.
* refactor(registry): drop the UI primitives, this is a video catalog
Removes 66 items tagged ui-primitive: accordions, buttons, inputs, dialogs, a
calendar. They are a shadcn-style interface component set that happens to be
expressible as HTML. None of them animate anything, so in a catalog whose job is
to offer moves for video they widen the surface without making it more useful,
and each one is a page a reader has to skip past to reach something that moves.
Every one is new on this branch. None exists on main, so nothing main ships is
being taken away; that was confirmed against origin/main before deleting rather
than assumed, and the no-deletions gate still passes.
Removed with them: registry/ui-primitives, the Operator Black token and contract
files only these items consumed, and the tooling that maintained them
(sync-ui-primitives.ts and scripts/lib/ui-primitives). No other registry item
declares a dependency on any of the 66, so nothing else loses a piece. The now
empty UI Primitives navigation rule goes too.
Generated output is pruned with the sources. The page generator writes files but
never removes ones whose source has gone, so a stale page would have survived and
404d its own preview. Verified: 0 orphan pages, 0 orphan previews, and the
navigation resolves 532 references with none dead.
This does discard variables authored for 54 of them earlier on this branch. That
work is in the history if these ever come back, and it is the right trade: they
should not have been in a video catalog to begin with.
The catalog is now 388 items. Lint exits 0 and 2522 CLI tests pass.
* feat(registry): every motion and transition primitive is now customisable
The last 16 primitives get variables, so none is left without them. 173 of 388
items now declare variables; the rest are blocks and showcases, which are whole
scenes rather than parameterised moves.
Same contract throughout: declared on the root, validated in the composition
script, wired to something visible, and falling back to the declared default on
missing or unrecognised input. With no overrides every item renders exactly as
it did before, verified per item against the pre-change render in a real browser
at rest and at pinned mid-flight states, comparing computed styles and rects and
in most cases a screenshot hash.
This round refused several knobs rather than shipping ones that only look real.
A tilt-card depth knob was written, measured, and thrown away: the card sets
overflow hidden, which forces transform-style flat, so the authored translateZ
is already inert and every option probed identical. It ships a glow knob
instead, which drives inset and visibly changes at rest and under the drift.
slot-machine-roll has no free travel knob because the roll is exactly one row
height and any multiplier lands the reel off-register; size scales row height
and roll distance together, which is the only honest version. soft-blur-in
offers up and down but not left and right, because the shipped tween resets y
and not x, so a horizontal offset would never animate away.
Two pre-existing bugs surfaced while checking honestly, both left alone as out
of scope but worth recording. zoom-through-transition and tracking-in each tween
a custom property that is never set, so CSS reads it as zero and the move starts
from zero rather than from its authored value. The depth and tracking knobs are
scoped around that and their headers say so, rather than pretending the tween is
what it appears to be.
Verified: 388 items, 0 primitives without variables, 0 items where the manifest
and the composition disagree, 0 declared-but-unread variables, nav resolves 532
references with none dead, no demo.html changed, lint exits 0, and the
no-deletions gate passes.
* feat(registry): raise the catalog quality bar, and add eight primitives
Cuts 37 components, adds 8, and writes down the standard both decisions were
made against.
The 37 removals are all new on this branch and absent from main, so nothing
shipped is withdrawn. Each was audited with two pieces of evidence: source
identity after name normalisation, and a composition-level contact sheet
showing the members animate identically.
The largest group was 13 files byte-identical apart from an h3 and one
sentence. Nothing marqueed, panned, zoomed, deployed or dragged. An honesty
tiebreak decided survivors: frosted-glass-wipe has no backdrop-filter,
spring-scale-in has no spring, masked-slide-reveal has no mask,
short-slide-right travels up, and three-particle-ribbon differs from
three-orbiting-cards by one number while having neither particles nor a
ribbon.
Two independent audits agreed 10 out of 10 on a shared calibration sample,
in both directions, including three items a first pass wrongly condemned.
The rubric is the durable part. Fatal criteria are separated from fixable
ones, because no-timeline alone hits 97 items including some of the best;
promoting it would have cut 97 and left a worse catalog. It also records the
harness rules that make a verdict reproducible: render from the composition
rather than the demo, since demos carry content the installed item does not,
and mount sub-compositions rather than inlining them, since inlining renders
black frames indistinguishable from a dead item.
The eight additions target measured gaps. Camera language ranked first
because PSNR across 30 reference demos showed the most impressive
environments barely move: they are sets, not shots.
camera-shake carries nine lens-accurate profiles, amplitude scaled by focal
length so a wide lens shakes differently from a telephoto. rack-focus splats
each light through the aperture shape, so a defocused point becomes an image
of the iris, with flux conserved so highlights survive defocus.
camera-dolly-zoom solves focal length from distance, holding subject size to
0.000 percent drift while the background grows 53 percent. Plus
oscilloscope-trace with history-free phosphor persistence, bar-chart-race,
split-flap-board, spiral-galaxy and vfx-anamorphic-flare.
Each is verified by rendered frames and a seek-equals-playback check rather
than by check passing, which is not a visual gate.
* fix(registry): let the split-flap board finish flipping on screen
The board declared 8s but every flap had settled by 3.5s, so more than half the composition was a still frame and the catalog preview opened on it.
* fix(registry): keep the thread-message-stack payload parseable
A line wrap had put literal newlines inside the JSON string literals of the blocks data-hf-primitive-data payload, so JSON.parse threw in the browser and the composition never ran. The preview script hid it: it rewrote the payload in the temporary copy it captured from, so the catalog picture looked right while every installed copy stayed broken. That repair pass is gone and the payload is fixed where it ships.
Its two tests could not have caught this. Both were written against vitest in a directory the repo runs with node:test, so neither was in test:scripts and neither had ever run. They are converted and registered, along with a new one that JSON.parses every payload in the registry, and that one was checked against the re-wrapped shape before being kept.
* fix(registry): close the apostrophe that truncated a variables declaration
chromatic-aberration-wipe described its accent as "the incoming scene's gradient" inside a single-quoted data-composition-variables attribute, so the attribute ended mid-JSON and the tag never closed. The formatter refused to parse the file, which is how it surfaced, but the runtime would have read a truncated declaration.
Also formats the 159 registry and docs files the branch had left unformatted, regenerates the skills manifest, and drops docs/primitives: those 13 pages import /snippets/PrimitivePlayer.jsx and read docs/public/primitives/, neither of which is on this branch, so mint failed the build on them. Nothing links to them and they ship whole on feat-video-primitives.
CI ran test:scripts before building core, so the preview test added here failed on a missing dist rather than on anything it checks. It now runs after the builds.
* fix(registry): make the review findings real fixes
Ten items declared variables on their composition root but had no variables key in the manifest, so their generated pages shipped no explorer at all. Their manifests now mirror the root. Two more disagreed only in description text, and the root was the truthful side: both compositions paint an inset ring, not the slabs or colour pair the manifest described.
The caption <video> removal left 24 CSS rules addressing elements that no longer exist. Removed, excluding the four ids that were already orphaned on main.
thread-message-stack could not stay fixed: oxfmt reflows a divs contents and lands a newline inside a JSON string literal, so the payload broke again on the next format. A script tag is not an option because the runtime strips every script out of the mounted clone. The reader normalizes HTML whitespace instead, which is what makes it survive any reflow, and the guard test now asserts that contract rather than the byte layout.
downloadFile had lost its 30s timeout, DownloadOptions, and the mid-pipeline error plumbing in a rewrite that was only meant to fix redirects. Five callers were left with no stall guard. Restored, redirect handling kept.
warnUnknownEnumValues re-did the parse readDeclaredDefaults had already done. Both now share one readDeclarations, and the rest splits into compositionLabel, declaredOptions and unknownEnumValue. 1909 core tests unchanged.
Deletes build-qa-gallery, theme-gate and generate-primitive-pages: nothing invokes them, two read a coverage map four directories above the repo root, and the pages the third generates are no longer on this branch. Wires check-no-main-deletions, which is the opposite case, real and tested and never run.
* fix(registry): stop shipping a stale copy of the catalog-search work
This branch carried re-authored copies of the CLI search commits rather than the ones on their own PR, so merging it would have rolled back six later fixes: the vector cache that refuses a half download, the 0o700/0o600 modes, the rebuilt-from-registry index generator, the coverage gate and its CI job, and the scripts typecheck. Those files now come from that branch.
registry.json still listed 64 items whose directories the UI-primitive removal deleted, so hyperframes add would resolve a name and then fail on missing files. Regenerated from disk: 358 searchable items, 358 vectors, gate green.
Also drops an internal provenance block from thread-message-stack, along with the type and the two JSON schemas that existed only to describe it. It published an artifact id, a version id and a heygenverse:// URI, none of which mean anything to someone installing a block, and a public registry is the wrong place for them.
Typechecking scripts/ for the first time surfaced 45 errors in this branch. Fixed rather than suppressed: the geometry test reads positions through one accessor that names a missing index instead of letting NaN reach a tolerance compare, and the null-returning shape helper is asserted at its call sites, except in the test whose subject is the null.
* refactor(scripts): split the page builder into its numbered sections
generateItemMdx had grown to cyclomatic 26 across 196 lines while its own comments already named the seams. previewSection, usageSection and footerSection now own one each, taking it to 13. Regenerating all 358 pages afterwards produces a byte-identical tree, which is the check that matters for a generator.
* fix(cli): repair what the cross-branch file take broke
Taking files wholesale from the catalog-search branch reverted the downloadFile timeout restored one commit earlier, so five callers were back to no stall guard at HEAD. Restored on both branches this time, since that branch never had it either.
It also took that branch test:scripts line without the vitest it depends on, so the script exited 127 and the CI Test job would have failed on a missing binary rather than a test. vitest is a root devDependency now, and the run is scoped to scripts/catalog/ with the slash: without it the prefix also matched catalog-preview-temp.test.ts, a node:test file with no vitest suite in it. Both branches had that one.
Four registry items and their docs copies carried absolute paths from a working directory. A public registry is the wrong place for them and history is permanent, so the sentences now name the source without the path.
Skill docs came from before the code they describe: the catalog command reports unindexed and applies installability after ranking, and both SKILL.md files now say so.
Also drops a double type assertion and ten dead ?? NaN coalesces from the geometry test, the second of which reintroduced exactly the NaN-into-a-tolerance-compare that the checked accessor exists to prevent.
* fix(ci): resolve core from source and take only item directories
The scripts typecheck failed on generate-registry-items importing @hyperframes/core by package name. It resolves on a machine with a warm node_modules, which is why it passed locally, and not in CI. Every other script in the directory already imports core from source and says why in a comment.
The preview job derived its item list with a sed that needs a trailing slash, so registry/components/CATALOG.md never matched, survived as a full path, and was handed to the renderer as an item name. The grep now requires a directory component. Simulated against this PR: 219 items, none of them a path.
* refactor(registry): load gsap from the cdn like every other item
store-badge-lockup vendored gsap 3.14.2 as a 4,200 line minified file and installed it into the users project, while 540 other items load that exact version from jsDelivr. Repointed, the copy deleted and the manifest entry with it, so hyperframes add store-badge-lockup no longer writes a second copy of gsap into someone elses compositions directory. Re-rendered and re-generated: the preview still draws.
* feat(registry): swap in the detailed device models
Replaces the iPhone and MacBook models in the three device blocks. The old assets were untitled meshes with no keyboard on the laptop; these name every part and model the keycaps, speaker grilles, antenna bands and camera plateau.
Not a drop-in. The compositions found the screen by side effects, the material that happened to carry an emissiveMap for the phone and a mesh literally called matte for the laptop, and neither exists now. They select front-glass and display instead.
Both panels ship UVs authored for a tiling material, the laptop runs u -6.3 to 6.3, so one screen image clamped and smeared across the panel. Planar UVs are derived from each panel bounds at load.
The old phone display sat at the model minimum Z and the timeline spins assume a screen facing -Z. These face +Z, so the model is aligned by reading which of its own parts is front rather than re-timing the animation.
Removes the hand-drawn Apple logo from two blocks: the replacement ships apple-logo meshes, and the drawn one used coordinates read off the old lid, so it floated beside the device.
The preview copy only took top level files, so models/, lib/ and assets/ never reached docs/public and 38 items rendered there without their assets. That is the source of the non-blocking 404s in the preview job. It recurses now, which also brings vendored bundles across, so the generated tree is out of the lint scope.
The html-in-canvas notice is a Danger callout: without the flag the preview is a black rectangle, which is a prerequisite rather than a caveat.
* chore(registry): rebase onto the merged catalog search
This branch carried its own copy of the catalog-search work so that merging it in either order could not regress the other. That copy is now the older one: main has the consent fix, the contributor path for someone without the embedding model, the restored download test and the corrected gate message. Every file main owns is taken from main, and the three duplicated CLI commits are dropped rather than replayed.
Regenerated afterwards, because the registry it describes has changed: registry.json, the vector index, the catalog pages and the skills manifest.
* fix(scripts): stop the rebase reverting the preview pipeline
Resolving the rebase in favour of this branch took three files whose newer versions had already merged, so the branch quietly reverted them.
generate-catalog-previews.ts lost encodeForWeb, which exists because publishing masters directly put 25 Mbps files on the docs CDN and one 20-second preview was 60 MB. It also lost the ffmpeg transcode, so a jpeg capture was being written straight to a .png path while the comment above still said it transcoded, and it lost openOpaqueCapture, re-creating the second copy of a capture setup that was extracted precisely to stop there being two. This PR renders previews for over 200 items, so all three shipped at scale.
scripts/tsconfig.json regained exclusions that hole the gate, and generate-template-previews.ts went back to importing the producer by package name, which is the CI failure that import was changed to fix.
All three are taken from main. Also drops an alignScreenToMinusZ copied into the laptop block, which has no front and back to compare and never called it, and makes the preview copy lstat so a symlinked directory cannot send it outside the repo.
* fix(registry): clear the five items this PR added that the linter rejects
The registry linter is not wired into CI, so five items this PR adds were shipping with real render defects nobody would have seen fail.
caption-camera-follow and grade-split-reveal styled their root by its own class. Sub-composition CSS is scoped to [data-composition-id=...] <selector>, so a selector whose leftmost part is the root class becomes a descendant selector and stops matching the root: the scene renders unstyled at render time while looking correct in every static check and in preview. Both now key off the attribute the scoper already adds.
logo-brand-close tweened letterSpacing, which the browser snaps to integer device pixels, so the ease-out tail stutters under seek-by-frame capture. It is a scaleX now.
terminal-simulator named SFMono-Regular, which the renderer cannot resolve, so the text silently fell back.
oversized-cursor was a false positive: the rule scans raw source for head tags and a literal one written inside a JS comment paired with the real closing tag. Confirmed against a render, nothing leaks into frame, so the comment says head element rather than the tag.
Also stops generate-registry-items.ts dropping catalogArtifact.revision. build-local-vectors.ts stamps it so the CLI and the coverage gate can tell whether the published vectors still describe this registry; regenerating the item list erased it, and the gate then failed until someone rebuilt the index.
41 items still fail the linter, every one of them pre-existing on main.
|
||
|
|
68205dbbc1 |
feat(cli): search the catalog by meaning, on this machine (#3089)
* feat(cli): search the catalog by meaning, in three named tiers Browsing the registry means matching names and tags, which fails whenever the author's wording differs from yours. "make the pace feel faster" finds nothing when the move is described as "velocity-driven blur". This ranks by meaning instead. Three tiers, and the command always says which one answered: words shared vocabulary, free, offline, no account on-device bge-small, free, offline, one opt-in download hosted Gemini, free for signed-in HeyGen users The tier is stated because a quietly worse answer looks exactly like a good one. --json carries it as a token alongside dropped, shown, total and top_score, so an agent reads provenance as data rather than matching English that is written to be reworded. Two consents, asked once each, and never conflated. Sending a query is a privacy question, so the prompt says the query is sent. Downloading a model is a disk and bandwidth question, so that prompt talks about size. Neither fires without a terminal: an unattended run sends nothing and downloads nothing unless a flag records that a person agreed. The catalog is derived from registry-item.json rather than from a separate document, so the set that is ranked and the set that can be installed are the same object by construction. Only the on-device vectors are committed; the hosted vectors are nine megabytes and belong on the server. top_score is reported and never acted on. A "nothing matched" threshold looked clean on long briefs and collapsed on the short queries people type: "a logo appears" scores 0.6181 and keyboard mash scores 0.6417, so any cut that catches the noise rejects the real query. The measurement is in the evals directory rather than in this branch. Not covered here. The published recall figures were measured against a separate hand-written document, not against registry text, so they should not be quoted for this catalog until re-measured. The offline tier needs a normal install: a single-file build cannot load the native ONNX runtime, which the command now reports instead of silently degrading. And the drop-detection path has never been observed firing outside its author's tests. * fix(cli): make this branch pass the repo's own gates Three things `bun run lint` and `fallow audit --base origin/main` rejected. CI runs both, so none of this branch would have gone green. Found by running them, not by reading the diff. process.exit in catalog.ts, twice: an invalid --type and a cancelled picker. check:cli-process-ownership reserves that for cli.ts, and the rule is not cosmetic — process.exit tears the process down where it stands, so anything cli.ts has queued to run on the way out is dropped. finishCommand throws a CliResultSignal that cli.ts turns into the exit code, which is what init.ts already does for a cancelled prompt. Three exports with no consumers. normalize keeps its body and loses its export; localEmbedder is the only caller. modelsDirectory goes entirely, having no caller inside its file or out. The WordPieceConfig re-export goes, and with it the import it existed to forward: the type is exported from wordpiece.ts, where its consumers already take it from. Complexity. prepareOnDeviceTier is lifted out of run(), which took run from 64 cyclomatic and CRAP 948 to 54 and 684. That block is one decision — can the offline tier run, and if not, why not — and its only product is a list of warnings, so it reads and tests as a unit, which it could not do inline. The rest is suppressed rather than refactored, each with its reason on the line above. Finishing run() means extracting its three output paths, and that is a refactor of a command this branch already changes for other reasons: a separate initiative, not something to absorb here. Every suppression says what shape the function has and why; a bare marker on a function nobody can justify is how a threshold stops meaning anything. Verified: `bun run lint` exits 0, fallow reports no issues across 27 changed files, and 2540 CLI tests pass. * feat(cli): ship the local search tiers only, drop the hosted one Search now has two tiers, both local: shared-vocabulary word matching, and the opt-in on-device model. The hosted tier, which sent the query to a HeyGen endpoint and ranked it with a hosted model, is removed. This is a scope decision, not a defect. The endpoint works and its own change is reviewed and green; it is simply not what we want to ship first. Landing local only means the feature has no backend dependency, no auth requirement, and nothing leaves the machine unless someone opts into downloading a model. Gone: registry/smartSearch.ts and its test, the --smart and --no-smart flags, the outcome plumbing through the command, the remote branch of applySearch, the remote tier, and the hosted-only JSON fields (ranking, catalog_version, top_score). Also the smartSearchEnabled consent field in telemetry config, which was the persisted storage behind the hosted consent and would otherwise have been left as dead configuration surface. Kept exactly as they were: both local tiers, the --on-device and --yes flags, the download consent prompt, and the runtime check that happens before the download rather than after it. The --json envelope still reports query, tier, tier_detail, shown, total, dropped, warnings and results, so an agent can still tell which tier answered and why. tierToken now distinguishes on-device from words. Verified: lint exits 0, fallow reports no issues, 2522 CLI tests pass, and the command was exercised directly. A query answers on the on-device tier where the model is installed and falls back to word matching where it is not, reporting that fallback in warnings rather than silently. An unknown --type still exits 1 with a readable message, and --smart is now rejected as an unknown flag. * fix(cli): count only moves this registry cannot install as dropped The dropped count was computed against the list left after the user's own --type and --tag filters, so every move the user excluded was reported as one the registry is missing. Filtering made the number go up: the same query reported 277 unfiltered and 302 with --type block. The count exists so a caller can tell "nothing matched your words" apart from "the ranker suggested things this project cannot install". Conflating it with user filtering destroys exactly that signal, and worse, genuine index skew and a self-inflicted filter printed a byte-identical line with opposite remedies -- one means refresh the shelf, the other means drop a flag, and refreshing does nothing. Now counted against the registry rather than the filtered view. The manifest is already fetched whole and narrowed in memory, so keeping the unnarrowed name set costs no extra request, and item loading still runs only on the filtered subset. Verified against ground truth rather than by eye: the vector artifact holds 411 names, the registry holds 168 installable items, and 134 of those names exist in both, so 277 are genuinely uninstallable. The count now reads 277 unfiltered, 277 under --type block, 277 under --type component and 277 under --tag, and the skew it reports is real -- the artifact predates dropping the UI primitives and still ranks moves that are no longer on the shelf. Reported by Vance Ingalls, who also noted this closes an item the status doc listed as unverified. Two earlier sweeps could not make the count fire because neither combined a filter with a query. Tests pin the three cases: a genuinely absent name counts, a filter-excluded name does not, and a fully installable ranking reports zero. * fix(cli): tell the user when meaning search cannot see the catalog The on-device index was fetched once and never revalidated: the only freshness check was two existsSync calls. A move added after that fetch was invisible to meaning search permanently, not down-ranked but absent from the candidate set. The registry manifest on the same command carries a 24h TTL, so the two halves of one feature disagreed about staleness. The dropped count reported over-coverage only, names the index has that the registry lacks. Under-coverage was never computed, so the harmless direction was instrumented and the costly one was silent. Reproduced with an index truncated to 120 of 168 moves: dropped read 0, perfect health, while 48 moves were unreachable. Counts under-coverage from the name list the artifact already carries, so no extra request. Warns only when non-zero, and names the remedy. The remedy had to be made true: --on-device could not refresh a stale index because hasLocalVectors short-circuited the fetch. That flag now refetches when the index is absent or no longer covering. Two defects the reproduction surfaced. A failed refresh reported the tier unavailable while the old vectors were still on disk and still ranking. And the fetch wrote its two files one at a time, so failing between them paired a new name list with an old matrix, a hard load error rather than stale data. It now writes both or neither, which matters more once refresh runs on staleness. top_score returns, scoped to the on-device tier and set to the score of the best result actually shown rather than the ranking head, which can describe a row the caller never received. Also: scripts/ is now typechecked. It never was, which is how a build script that crashes after the paid embedding call, and two scripts whose imports do not resolve at all, went unnoticed. 43 errors fixed, no suppressions. And the docs stop describing a --smart hosted tier that was deleted, an item that does not exist, and a registry refresh that cannot fix a stale vector index. * ci: fail when the search index stops covering the registry The catalog vector artifact is regenerated by hand. Nothing in CI, in package.json or in a hook rebuilds it, because embedding needs the 32 MB model. So adding a registry item silently makes it invisible to meaning search until someone remembers to regenerate. The failure is asymmetric, which is what makes it easy to miss. Removing an item is self-healing: the ranker still scores the dead vector, then filters the name before display, so a user is never offered something they cannot install. Adding one is not: the item is absent from the candidate set entirely, not ranked low. Comparing the two name lists needs neither the model nor a network call, so the gate runs in seconds. CI checks rather than fixes, for the same reason it cannot regenerate. Scoped to blocks and components. Examples are starter projects a user scaffolds, never something catalog ranks, and the artifact carries no vector for them, so demanding one would keep this gate permanently red and it would be ignored within a week. Verified in both directions rather than assumed: adding an unindexed item exits 1 and names it, restoring the registry exits 0. * fix(catalog): rebuild the search index from the registry build-local-vectors.ts read registry/catalog-artifact/catalog.json, a file no script in this repo writes and which is not committed, so the documented regeneration command failed on a missing path. That is why the index could drift from the registry with nothing to run to fix it. It now reads registry/blocks/* and registry/components/* through catalogFromRegistry, the existing helper that already produced the right shape but had no caller. Rebuilding reproduces the shipped 168 rows byte for byte. A lefthook catalog-index command regenerates and re-stages both artifact files whenever a staged registry-item.json changes, mirroring the skills-manifest pattern, so adding or removing an item keeps the index in sync without anyone remembering to. Verified end to end: staging a new item took the artifact 168 to 169 rows and staged it in 0.80s. * fix(cli): refuse a half-downloaded vector cache The two artifact files have to agree on how many rows there are, and until now nothing checked that before writing them. A truncated or wrong-model response landed in the cache and only failed at load, on every later search, until someone cleared it by hand. The pair is now checked first and refused as a unit, and the cache is created 0o700 with 0o600 files rather than inheriting the umask of a directory the caller may have pointed anywhere. Also lifts the capture setup the two preview generators had drifted into sharing into scripts/preview-capture.ts, and splits the vector builders batching and packing out of main. Both were findings the audit attributed to this branch. * fix(cli): keep the catalog vitest run with the tests it runs Restacking took the base package.json wholesale, which dropped the vitest dependency and the scripts/catalog run this PR adds. Both belong here rather than under it. * fix(cli): stop the declined model download from happening anyway Answering no to the on-device download offer recorded no and warned, then carried on. The guard below it is localModelConsent() !== false, which the decline had just made false, so it was skipped rather than taken: control reached recordLocalModelConsent(true), overwrote the answer with yes, and fetched the 32 MB model the user had refused. Next run it never asked again. No test could catch it. The stub pinned localModelStatus to ready, so the prompt never fired, and recordLocalModelConsent was a no-op that recorded nothing. Two tests now cover the offer, and they need three things the old stubs did not model: the run has to look like a terminal, because off one the command treats --on-device as the consent and never asks; the ONNX probe has to answer true, or an accepted offer returns at the runtime guard before it can download; and the status has to follow the recorded answer, or the second offer later in the run fires as well. Removing the return makes the decline test fail. * fix(catalog): let someone without the model still add a component The pre-commit hook rebuilds the search index, and rebuilding needs the 32 MB embedding model. An outside contributor adding a registry item does not have it, so their commit died inside the ONNX loader on an ENOENT naming a path they never set, and the CI gate then told them to run the command that had just crashed. The model is an opt-in for search, not a build dependency, so nobody is charged for it to contribute. The builder checks first and explains itself, exiting 3 for cannot as distinct from 1 for failed. The hook treats 3 as skip and lets the commit through. The gate now names both paths: regenerate if you have the model, leave it if you do not and a maintainer will. Verified both ways: with no model the builder explains and the hook exits 0; with the model it still regenerates byte-identically. * docs: say that anyone can add a registry item, and stop hand-editing a generated file Two defects, one of them the reason 64 stale entries survived in registry.json. The checklist told contributors to add their item to registry/registry.json. That file is generated from the item directories, so an entry added by hand survives until the next regeneration and then vanishes, and one left behind for a directory that no longer exists is worse: hyperframes add resolves the name and then fails on missing files. Both CONTRIBUTING.md and the agent-facing skill reference now run the generator instead. Nothing said contribution was maintainer-only, but nothing said it was not either, and two steps do need assets an outside contributor has no reason to install. Those are now named in a table with what happens if you do not have them, matching how the preview image was already handled. The search index is the new one: the model behind it is a 32 MB opt-in for search, not a build dependency. * fix(cli): harden on-device catalog search * fix(cli): refresh stale catalog vectors * test: create catalog vector temp dirs securely |
||
|
|
f28bc80a1d |
feat(scripts): fail a branch that deletes files main still ships (#3150)
* feat(scripts): fail a branch that deletes files main still ships Written after a scare that turned out to be a measurement error, and the error is the reason it exists. Comparing tip to tip on a branch a month behind reports every file main has added since the merge base as a deletion: 1,284 of them, an entire skills tree among them, none of it real. A merge keeps mains side and a pull request shows the three-dot diff, which reported zero. So the gate uses the three-dot form and reports renames separately, because in a name-only diff a rename is indistinguishable from a deletion and treating them alike would either mask real loss or block every legitimate move. * ci: enforce the no-deletions guard |
||
|
|
79dff20516 |
feat(scripts): typecheck the scripts directory (#3149)
* feat(scripts): typecheck the scripts directory scripts/ was the one TypeScript surface nothing typechecked. Adding a project for it surfaced real errors rather than style: a preview generator passing string | undefined where a string was required, a readdir result indexed without a bound, and two non-null assertions standing in for a filter that could have narrowed the type instead. The two preview generators had also drifted into sharing a capture setup, down to the comment explaining why the capture is opaque. That lifts into scripts/preview-capture.ts, so the reason is written once and both callers own the handles they have to close. @hyperframes/core and @hyperframes/producer become dev dependencies because the scripts import them; without that the project resolves on a machine with a warm node_modules and fails in CI. * fix(scripts): use source imports consistently |
||
|
|
0bda6b55b8 |
feat(cli): track which registry items add installs (#3099)
* feat(cli): track which registry items `add` installs `cli_command` records that `add` ran and nothing about what it installed, and the registry is served from raw.githubusercontent.com, which gives no per-item counter either — so there is no way to tell which block or component people actually pull, and no way to know what is worth building more of. Emit one `registry_item_added` event per item written into a project, from `runAdd` after the install succeeds. That is the single choke point: the bulk `add <tag>` path re-enters it per item, and a failed or compatibility-refused install throws before it, so a refused install is never counted as a download. `requested` separates the item the user named from the transitive `registryDependencies` pulled in behind it; without it a popular dependency outranks everything that depends on it. Item names are public registry identifiers, never user content or project data, and the event goes through `trackEvent` — an install that opted out via `hyperframes telemetry disable`, `HYPERFRAMES_NO_TELEMETRY` or `DO_NOT_TRACK` sends nothing. * test(cli): cover `add` telemetry end to end against the built CLI The unit tests assert the emit seam and nothing past it. `shouldTrack()` short-circuits whenever `isDevMode()` is true, and that is true for any `.ts` entry, so under vitest a real event and no event are indistinguishable and the transport is never exercised at all. Drive the built CLI instead and assert on the HTTP body it actually produces: one event per installed item, the dependency reported with `requested: false`, an opted-out install sending no request at all (not merely one without this event), and a refused install counting nothing. Two fixtures, because neither case is reachable through the real registry. The registry origin is a first-class project setting, so a local one supplies the `registryDependencies` edge that no shipped catalog item declares today; and `globalThis.fetch` is wrapped to capture the batch rather than send it. The faked 200 is load-bearing: only a failed flush leaves events queued, and only a non-empty queue spawns the detached `flushSync` child that would bypass the hook and reach production analytics. Verified the check can fail — forcing `requested: true` for every item turns it red on exactly the dependency assertion. |
||
|
|
218eff7d36 |
fix(scripts): render template-only blocks in catalog previews (#3098)
* fix(scripts): render template-only blocks in catalog previews The catalog preview renderer treated any file containing `__timelines` as a standalone composition and rendered it as index.html directly. The 12 VS Code snippet blocks register their timeline inside a `<template>`, which stays inert until a host mounts it, so every one of them failed with "Composition has zero duration" and no preview could be produced from the registry at all. Six of the previews on the docs CDN were hand-made from a project still mounting Monokai, so Dark+, High Contrast, High Contrast Light, Solarized Light, Visual Studio Dark and Visual Studio Light all showed Monokai's video. Detect standalone-ness on the document with template content stripped, mount the mirrored install-layout copy so a block's own `../assets/*` references resolve, and capture posters opaque: `format: "png"` is the engine's transparent mode and forces `background-image: none` on every composition root, which erased the desktop backdrop these blocks paint. Publishing gets the missing half too: preview URLs are stable and the objects are uploaded `immutable` with a one-year max-age, so a re-upload alone never reaches a reader. * fix(scripts): install ffmpeg in the preview job and fix the sibling renderer The canary this PR added caught its own regression: the poster transcode shells out to ffmpeg, which ubuntu-latest does not ship and this job never needed, so both canaries failed with `spawnSync ffmpeg ENOENT`. Install it the way every other render job does. `encodeForWeb` has always shelled out to the same binary; the job only got away with it because `--skip-video` skipped that path. generate-template-previews.ts captures posters through the same transparent `format: "png"` mode, so any template painting its own backdrop loses it exactly as the code snippets did. Fixing one renderer and leaving its sibling on the broken call would just move the bug. Also fold the three separate parses of registry-item.json into one read: they had drifted into three different failure behaviours for the same file. |
||
|
|
aa928f399a |
style(scripts): oxfmt the nav-suppression assertion
The long assert.match line tripped oxfmt --check in CI (Preflight/Format red, which cascaded regression/player-perf/preview-regression into skip-then-fail). The pre-commit format hook doesn't cover scripts/*.mjs, so it slipped through. Wrapped per the formatter. Flagged by Magi and Rames. |
||
|
|
fed2baf278 |
docs: correct composition offset rationale; pin nav fix; label-in-name
- html-schema: the previous clause claimed a composition host's data-media-start is never read. It is: readElementPlaybackStart (media.ts:16) resolves data-playback-start ?? data-media-start and timeline.ts calls it on composition clips. Rewrote to the accurate reason both reviewers gave — composition hosts are only inspected by the playback-start-first readers, media-start works as a fallback, but playback-start is what Studio writes/normalises to. - motion test: pin the HoverVideo click-suppression (preventDefault + stopPropagation). Removing it left the gate 12/12; now it fails. This is the bug that escaped static review and only surfaced by driving the live preview. - replica-compare: fold the visible 'Sound off/on' text into the aria-label so the accessible name contains it (WCAG 2.5.3, Rames). Round-5 findings from Magi and Rames. |
||
|
|
f3d1d531d1 |
docs(examples): reset control state on offscreen release; precise a11y labels
- replica-compare: the offscreen teardown now resets muted (element + React state), matching HoverVideo — a pair unmuted before it scrolled away no longer returns reading 'Sound on' over a paused, sourceless pair (Magi blocker). - Both controls' aria-labels now follow the mode: under reduced motion the button plays/pauses the whole comparison, and when a preview is already autoplaying muted the action is 'unmute', not 'play with sound' (self-review + Rames). - Hardened the motion-suite assertion to scope 'startBoth' to toggleSound's body (a defined-but-unused helper no longer satisfies it) and pin the offscreen muted-reset transition (Rames mutation-test gap). - html-schema: 'hyperframes validate' inspects <audio> only; note that no media-start-only reader inspects a composition host, so the kind rule strands nobody (Rames). |
||
|
|
630c8900f5 |
docs(examples): reduced-motion play starts both films; precise media-start layers
- replica-compare: the voluntary control now starts and pauses BOTH films (not just the reference), and the replica-sync effect attaches in view regardless of the preference, so a reduced-motion visitor who presses play sees the whole synchronized pair. Added a focused source-level assertion to the motion-check suite (the repo has no React runtime harness for snippets). - html-schema: describe each layer precisely instead of grouping the CLI — timing compiler, HTML parser, producer audio, and 'hyperframes validate' read only data-media-start; runtime, Studio and 'hyperframes snapshot' read data-playback-start first (Studio also writes it). Round-3 findings from Magi. |
||
|
|
d373c3f4a0 |
fix(catalog): explicit section ownership, no body-sniffing heuristic
carriedSectionsFrom() decided whether a ## Usage section was generated by matching its first line against a list of historical opener phrases — so a hand-written Usage section that happened to open that way was classified as generated and silently deleted on regeneration. Ownership is now purely set membership: a section is generated iff its heading is one the template emits, and ambiguous 'usage' is no longer in that set (the template never emits it), so any ## Usage is carried. Exported carriedSectionsFrom behind an entrypoint guard and added two executable preservation fixtures. Flagged by Magi (#5). |
||
|
|
aa49f7d924 | Merge remote-tracking branch 'origin/main' into docs/pages-show-not-tell | ||
|
|
8cadefa5d8 |
fix(scripts): import the changelog style note instead of duplicating it
changelog-weekly.ts hard-copied the CHANGELOG_STYLE_NOTE literal that set-version.ts exports; both feed the release gate, so a drift silently weakened it. Import the constant so the two markers cannot diverge. Flagged by Copilot and Rames. |
||
|
|
6ff6601dd0 |
fix(catalog): restore the required Related topics section on generated pages
My generator rebuild replaced the '## Related topics' section (still required by docs/AGENTS.md) with the provenance footer, dropping it from all 168 generated Catalog pages. Emit it again as the final section so pages end with it, and stop carriedSectionsFrom() breaking at the footer marker so a human section appended below the generated tail survives a regeneration. Adds a per-page regeneration assertion so a future drop fails CI. Flagged by Magi (P1) and Rames. |
||
|
|
ddbd547fad |
fix(scripts): restore the texture instruction phrase a core test pins
I misdiagnosed this as a pre-existing Windows CRLF failure. It was not. The core test catalogGeneratorInstructions.test.ts reads scripts/generate-catalog-pages.ts and asserts it contains three exact phrases. My catalog rewrite reworded the texture snippet instruction from "paste the real `<style>` element near the bottom" to "Near the bottom is a real `<style>` element — copy it into", which dropped the third pinned phrase. Restored the wording to contain "paste the real `<style>` element near the bottom". The test now passes (3/3 expects), and it was failing on the Windows runner only because the Ubuntu run tripped an unrelated player port flake first and masked the same core failure there. Not Windows, not pre-existing, not line endings — my text change. Caught because the reviewer questioned the diagnosis. |
||
|
|
b33974cf72 |
chore: quiet fallow complexity on the catalog generator's grown functions
My catalog rebuild grew five functions past fallow's CRAP threshold — carriedSectionsFrom and its flush (the carry-forward parser), plus generateItemMdx, generateParams and main which took on the poster and carry-forward wiring. These are build-time scripts with no unit coverage, and CRAP penalises exactly that: the inherited functions in the same two files score 300-600 for the same reason and pass only because they predate the gate. Refactoring into smaller functions made it worse — more uncovered functions, not fewer findings. So each grown function carries a `// fallow-ignore-next-line complexity`, the tool's documented mechanism, applied consistently with the file's existing shape. Fallow verdict is pass: 0 introduced complexity, 0 introduced duplication. Generator runs clean and emits 168 pages. |
||
|
|
618f73c266 |
Merge remote-tracking branch 'origin/main' into docs/pages-show-not-tell
# Conflicts: # skills-manifest.json |
||
|
|
ebdd1893c4 |
fix(studio): reconcile external edits before reload (#2993)
* fix(studio): reconcile external edits before reload * fix(ci): retry transient workspace installs Make external reload retry behavior honest and isolate reload listeners. Remove the dead SDK timestamp parameter. |
||
|
|
74ba97b7ed |
fix(scripts): the repo already knew which items have no poster
Thirteen manifests declare `preview: { video }` with no `poster` key, and that
omission is deliberate — no .png was ever produced for them. main reads it in one
line: `if (manifest.preview) return manifest.preview.poster`.
I deleted that line as "config nobody varies", having checked whether any poster
DIFFERED from the generated URL (none did, 100 of 100) and never having checked
whether any was ABSENT (thirteen were). Then I replaced it with a filesystem
check against docs/images, which is gitignored, so it was false everywhere and
would have stripped all 168. Then I removed the poster entirely. Three wrong
answers to a question the repo had already answered.
Restored, and the page's video tag now asks the same function rather than a
second source of truth. 155 of 168 carry a poster, matching main exactly: 154
video pages plus texture-mask-text, which renders mask images instead of a video.
The 13 remaining dead URLs are in catalog-index.json only, which no code in
docs/ or packages/ reads, and no rendered page requests them.
The complexity cuts to the encode pass stand: hasAudio deleted (ffmpeg ignores
-c:a with no audio stream), and the spawn+Promise wrapper is execFileSync.
|
||
|
|
592301248e |
fix(scripts): drop the catalog poster instead of guarding it, and cut the encode pass down
**The poster guard I added twice was unworkable and I never ran it.** It called existsSync on docs/images/catalog/<name>.png. That directory is gitignored — previews are generated locally, uploaded to the CDN and never committed — so the check is false on every clean checkout and in CI. It would have stripped the poster from all 168 pages, not the 13 with a missing file. It also referenced REPO_ROOT, which does not exist in that file, so the script crashed on the first item. I described this guard in two commit messages without once executing the generator. The poster is now gone entirely, which is the smaller and more honest fix. These previews are autoPlay muted loop, so the poster is visible for a few hundred milliseconds; 13 of the 168 files do not exist and the browser fetches the poster before the video. Removing the attribute kills 13 x 403 and 168 needless image requests, and there is nothing to keep in sync. Also applied a complexity pass to the delivery encode: - hasAudio() deleted. 17 lines and a spawnSync per item to choose between "-c:a aac" and "-an". ffmpeg ignores -c:a when the input has no audio stream; checked, exit 0, output carries no audio track. - The 40-line spawn + Promise wrapper is execFileSync. Everything around it in that script is already synchronous. - The duplicated poster lookup is gone with the poster itself. Net 76 lines lighter. Generator runs clean, emits 168 pages, carry-forward intact. |
||
|
|
fffc56c33b |
style: format changelog-weekly.ts
Left unformatted by the catalog-generator commit, which is the one file Preflight was failing on. oxfmt --check is clean on it now. Note for anyone reading the earlier claim: I said main fails oxfmt on 30 files. That was my local run picking up no repo config and using its own defaults. CI found exactly one file, and it was mine. |
||
|
|
4f1ace9f47 |
fix(scripts): actually commit the delivery encode
An earlier commit here said generate-catalog-previews.ts "has a web pass now". It did not. I made that edit in a second checkout while investigating and never brought it onto the branch, so the claim shipped in a commit message while the code stayed as it was. The fix itself is unchanged from what was described: the render output is a master, and publishing it directly is what put 25 Mbps files on the docs CDN. generateVideo now renders to <name>.master.mp4, runs one delivery pass at 1280 wide, CRF 28, faststart, keeping audio only when the source has it, then deletes the master and logs the delivered size. Formatted and linted clean. The Format check failing on this PR is pre-existing: origin/main fails oxfmt --check on the same 30 files, none of which this branch touches. |
||
|
|
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. |
||
|
|
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. |
||
|
|
ac6fd5363b |
Merge pull request #3009 from heygen-com/release/v0.7.92
chore: release v0.7.92 |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
79c4056a3f | docs: make Catalog visual and reproducible | ||
|
|
ab00b040f6 |
fix(release): print the release-PR flow for stable versions
set-version printed 'git push origin main; git push origin v<version>' as the way to trigger publish. That is the PRERELEASE flow. publish.yml's push trigger is 'v*-*', so a stable tag push fires nothing, and stable publishes only from a merged release/v* PR. Following the old text put a release commit on main with an unpublishable tag: publish never ran, and the stray tag then fails the next release's verify_remote_tag check. Now prints the branch + PR commands and says not to push the local tag, matching docs/contributing/release-channels.mdx. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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. |
||
|
|
71fd96bbf1 |
Merge pull request #2854 from heygen-com/feat/canary-rollouts
feat(core): percentage-based canary rollouts + calibration experiment |
||
|
|
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. |
||
|
|
d6191965cf | fix: pin release publishing to merge commit (#2959) | ||
|
|
6f0df2640b |
fix(cli,studio,core): close five R5 telemetry and canary findings
- A long-lived preview cached its telemetry posture in two places (readConfig and shouldTrack). Running `telemetry disable` in another terminal left it resolving canaries and injecting the CLI id for hours. Both caches are now dropped together at a request boundary. - Studio minted and shipped a telemetry id for every render regardless of the browser profile's opt-out, and the server emitted the outcome under CLI policy, which cannot see localStorage or DNT. The browser now sends an explicit telemetryOptOut, distinct from an old client's omission. - Any non-empty HYPERFRAMES_PREVIEW_HOST disabled the DNS-rebinding guard, so even a loopback bind accepted a hostile Host. The guard now holds for loopback binds and, on a LAN bind, admits only names this machine answers on. - sunsetAfter had no reader of the current date. A scheduled workflow runs scripts/check-canary-sunset.ts weekly, so a failure lands on the rollout's owner rather than on an unrelated PR author. - The install-state seed memo outlived `rm -rf ~/.hyperframes`, resurrecting a cleared cohort. Removed; it only saved a read on a readConfig cache miss. Docs updated for the Host rule and the 100% exclusion carve-out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c62bd4c454 | fix(studio): harden preview recovery | ||
|
|
3af9a7df2d | refactor(studio): reuse player probe errors | ||
|
|
0faeb42f86 | test(studio): mock composition thumbnails in smoke | ||
|
|
b0d3164ddb | feat(registry): add media treatment overlays | ||
|
|
696cbdbbd0 |
chore(skills): package Codex plugin upload (#2668)
* chore(skills): package Codex plugin upload * chore(skills): harden Codex plugin content * fix(skills): satisfy plugin quality gates * fix(skills): address plugin packaging review * fix(plugin): simplify asset validation * fix(skills): correct embedded-captions catalog count to 35 after nightcity removal The nightcity theme removal left SKILL.md claiming 36 identities in four places, including the frontmatter description the router reads. The catalog now has 35 entries (10 classic + 25 themed). --------- Co-authored-by: Miao Yang <miao.yang@heygen.com> |
||
|
|
ab8b50afd7 | refactor(repo): resolve changed-code audit | ||
|
|
3aa2404747 | refactor(cli): centralize process lifecycle | ||
|
|
942db16fcd | fix(cloudrun): publish adapter contract |