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.
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.
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.
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.
Beat-driven speed ramp, freeze-frame hit, and hard cut for music-led
promos and montages. Contributed as #2957; applied here as content so the
whole catalog set lands together.
Verified by rendering: 6s, clean, deterministic (the one Math.random hit
is a comment above a fixed pattern array). Opaque root, so no demo.html
is needed for its catalog preview.
Co-authored-by: akiyoshisan1218 <akiyoshisan1218@users.noreply.github.com>
Found by stepping through motion contact sheets rather than single frames.
- hw-pipeline: arrowheads were two asymmetric barbs (l -30 -14 / l -26 18)
fixed to horizontal, so they detached from a curve that arrives on a
descending tangent. Barbs now derive from the quadratic's end tangent
(end - control) with symmetric length and spread.
- hw-pipeline, hw-scribble-transition, hw-callout-circle: the family carried
two accents — #ffb020 in hw-frame/hw-title/hw-underline and #6d6dff here.
Unified on the warm marker accent; the blue read as a default UI colour.
- hw-callout-circle: the scribble hatch rendered as stacked bars over the
labelled subject. More rows so the zigzag reads as a scribble, lighter
weight and alpha so it sits behind the label.
- yt-camera-move: the stage grid was too faint to reveal the move, so the
preview of a camera-move component looked static. Stronger grid; zoom,
slide and tilt now read.
Demos resynced from source — demo.html is a plate-swapped copy, so a source
fix does not reach the catalog preview until it is regenerated.
The contributed HTML predates any oxfmt pass, so format:check failed for
42 files in both the Format and Preflight jobs. All 29 items re-rendered
after formatting and verified against frames — no visual change.
Adds three themed registry families contributed by @jbernard077:
- mk-* minimal presentation (7 blocks, 3 components)
- yt-* retro-broadcast creator (5 blocks, 4 components)
- hw-* hand-drawn scribble (6 blocks, 5 components)
Consolidates PRs #1933, #1992, #1993 and #1994, cuts 5 of the original 34
items, and fixes the defects that CI never got to report.
Cut (5), each covered by something we already ship or trivial to inline:
- yt-doc-lower-third — would be the 12th name/role lower third next to the
10 lt-* variants and lower-third-bild
- mk-logo-sting — logo-outro exists, and yt-logo-intro is the richer version
- mk-cta-button, mk-pill-callout, yt-avatar-pip — a styled button, a styled
chip and a masked circle
Fixes:
- hw-path-text rendered as a solid black blob. #hw-pt-path is the textPath
carrier and had no rule, so it inherited SVG's default black fill while
only #hw-pt-guide set fill:none.
- mk-line-graph value labels collided with their own dots and each other.
Series after the first now label downward, and the offset moved off a CSS
transform because the entrance tween animates y and GSAP rewrote it.
- Added demo.html for the 11 components and the 3 transparent-root blocks.
Without it generate-catalog-previews.ts skips components entirely, so the
catalog-previews job failed for every component in the original PRs.
- Tagged hw-title, hw-pipeline and hw-path-text as overlays.
Also registers all catalog pages in docs.json, which the original PRs left
orphaned, and picks up 4 pre-existing blocks that had no catalog page
(camcorder-hud, editorial-flash-overlay, freeze-frame-dressing,
organic-light-leak-overlay) because the regenerated nav now links them.
Every item was verified by rendering it and looking at the frames.
Co-authored-by: jbernard077 <jbernard077@users.noreply.github.com>
- caption-emoji-pop: shadowForColor now builds its glow via color-mix()
instead of hex-pair slicing, so the strict 6-digit brand-color gate is
gone — any CSS color the sibling templates accept (#fff, rgb(), named)
now renders instead of silently falling back to the default palette
- caption-weight-shift: fitFontSize now sizes against the WIDEST split
line rather than the joined group text (two-line groups no longer shrink
unnecessarily), and avoidSingleWordGroups' merges re-check fitsInTwoLines
like makeGroups' first pass does (merged groups can no longer overflow
the split budget)
- all 5: hfApplyStageConfig clamps resolution to the published validator's
<=8192 bound (validator is optional pre-flight; unbounded stages OOM
render workers), and fit floors carry a comment documenting that the
minimum size is returned unverified by design
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two review findings on the caption-data runtime, applied to all 5 templates:
- hfValidate's version gate used Math.floor(Number(v)) > HF_CONTRACT_VERSION,
and Number("v2") is NaN — NaN comparisons are always false, so malformed
versions slid through with no unsupported-version signal. An explicit
Number.isFinite check closes it.
- hfBoot's sibling-fetch .then called hfAttach unconditionally; a manual
window.__HF_CAPTION_ATTACH__ call landing while the fetch or fonts.ready
was still pending got clobbered by the late boot payload. Boot now yields
if a timeline already exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
caption-highlight was already IIFE-wrapped; the other four leaked their
runtime (hfAttach/hfBuild/...) as script-scope globals. Harmless when boot
was synchronous, but the data-driven retrofit defers attach behind
fonts.ready + the sibling fetch — with two caption components pasted into
one composition document, every script finishes before any deferred boot
runs, the last script's definitions win the shared scope, and the first
component never registers its timeline (a renderer waiting on it hangs to
timeout). Wrapping each template's script keeps its internals private so
each boot attaches its own component. window.__HF_CAPTION_ATTACH__ remains
intentionally window-scoped (last-defined-wins).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hfApplyStageConfig (Block B, verbatim across all 5 retrofitted caption
identities) only ever set --hf-caption-primary/--hf-caption-accent when the
corresponding brand.primaryColor/accentColor key was present, with no else
branch to clear it when absent. Re-attaching a brand-less payload after a
branded one left the custom property (and any JS-cached color derived from
it, e.g. caption-pill-karaoke's hfColorActive and caption-emoji-pop's
hfAccentColors) stuck at the stale value instead of reverting to the CSS
fallback, violating idempotent re-attach.
Also removes caption-editorial-emphasis's dead .word--italic CSS rule and
CLASS_MAP.i entry — hfMakeBlocks only ever emits "n"/"e" tags, "i" was only
reachable via the old hand-authored BLOCKS literal this task replaced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both identities looped over every OTHER group/block to force its opacity
to 0 at each group's own start time, in addition to each group already
setting its own opacity to 0 at its own end. Since groups/blocks occupy
non-overlapping time windows and already own their full opacity
lifecycle, that loop was dead weight — but it made timeline construction
O(n^2) in the number of groups/blocks. Under the Task 7 stress transcript
(long, frequent "emphasis" words forcing near single-word blocks/groups),
n reached ~2000 and the page hung well past a 30s test timeout for both
identities.
Verified behavior-preserving: full templates.test.ts (41 tests, incl.
opacity/seek assertions) and the new limits.test.ts stress suite pass
against both identities after the removal.
Replaces the hand-authored BLOCKS literal with hfMakeBlocks, a heuristic that
groups words into blocks/lines from timing (pauses, punctuation, max words per
block) and hfIsEmphasisWord (long, non-stopword content words) to decide which
word gets the large Playfair Display emphasis treatment and its own slide-in
line. computeLineSize/buildBlocks/fitBlocks and timeline construction now live
inside hfBuild, closing over layout-scaled font sizes and widths. Adds the
shared Blocks A-E caption-data runtime (attach/gate/brand config) and the
.word/.word--emphasis CSS brand hook for --hf-caption-primary. This is the
last of the five caption identities to go data-driven.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the hardcoded RAW_GROUPS index-pair array with hfMakeGroups, a
real fitting/pause/punctuation-based grouper, and wires the component
into the shared caption-data runtime (validate/gate/attach). Adds CSS
brand hooks: .hl-word text color from --hf-caption-primary, .hl-word-bg
gradient from --hf-caption-accent (second stop via color-mix()).
Retrofits caption-emoji-pop onto the shared HyperFrames caption-data runtime
(Blocks A-D) and the emphasis heuristic (Block E), replacing the hardcoded
7-word emoji map, fixed KEYWORDS set, and TRANSITION_WORDS set with a
generic ~40-entry lexicon and HF_STOPWORDS/hfIsEmphasisWord. The component
now consumes brand.primaryColor (hfPrimaryColor) and brand.accentColor
(hfAccentColors) and rescales stage/font/emoji sizing to the runtime
resolution via layout.fontScale/scaleX/scaleY.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The flowchart and flowchart-vertical blocks read tl.labels["hold5"] back
from the timeline to schedule the "Pythom" -> "Python" typing correction.
The render compiler executes composition scripts under a GSAP proxy whose
timeline exposes no readable .labels map, so this line throws
("Composition script failed ... Cannot read properties of undefined
(reading 'hold5')") before window.__timelines is assigned. The block is
never registered: renders show only the dotted-grid background and every
render pays the 45s sub_timeline_readiness_timeout.
Use GSAP's own label-relative position syntax ("hold5+=<offset>") instead,
matching every other step in these files. Identical timing, no .labels read.
Verified with hyperframes@0.7.60 render: block was blank before, animates
after; the readiness timeout disappears.
* docs(registry,skills): surface code-highlight 0-based indexing and opacity-reveal sweep guidance
From the 2026-07-14 CLI feedback digest (skills-owner action): a user
building code teaching videos hit two authoring gaps.
1. code-highlight's `line` is intentionally zero-based (`line: 1` =
second displayed line) but the warning lived only in pr-to-video's
code-vocabulary reference — nowhere an author actually touches the
value. Call it out at the block-use sites: the `__BLOCK` declaration
itself, the registry-item description, and the motion-graphics
catalog map.
2. Opacity-only code-typing tripped `sweep_static` for that user, who
worked around it with a slow host y-drift. The sweep fingerprint
does include per-element opacity, so document the actual trap (a
reveal that settles before the sampled window, then holds a static
frame) and the idiomatic fixes (spread the reveal / keep a blinking
caret alive) in the check reference — and pin the fingerprint's
opacity sensitivity with a regression test covering both the
visibility-floor crossing and a mid-fade value change.
* docs(catalog): regenerate code-highlight page from updated registry-item description
Only the code-highlight page is committed: a full generate-catalog-pages
run also surfaces ~34 blocks missing from the git-tracked catalog index
(pre-existing drift on main), which belongs in its own chore PR.
Running the CLI from source (tsx dev script, as CI's smoke job does)
transpiles with keepNames, which rewrites named inner functions in
serialized page closures into __name(...) calls — a helper that exists
in the Node bundle but not in the browser realm. The unified seek
closure was the first validate-path page function with named inner
functions, so 'hyperframes validate' threw '__name is not defined' in
CI while the dist build worked. Every session opener now installs a
no-op __name shim via evaluateOnNewDocument before any page script
runs, immunizing all serialized closures regardless of build mode.
Three filters keep the escalated contrast gate honest, each surfaced by
running check across the registry examples:
- data-layout-ignore (the layout audit's existing decorative opt-out)
now also excludes set-dressing text from the contrast audit — one
vocabulary for 'not copy a viewer must read'.
- Text that has (nearly) left the canvas is skipped: sampling a clamped
off-canvas box reads border pixels and produced the classic false
white-on-white (a cursor exiting the frame).
- Contrast failures follow the same persistence rule as layout findings:
observed at a single sample of a multi-sample sweep demotes to
warning; held failures gate.
Example fixes the sweep exposed: motion-blur's 21 deliberately-dim rail
labels are marked decorative (its vivid labels pass on their own);
nyt-graph's subtitle and source-note adopt the gate's suggested
compliant gray; decision-tree's declared duration drops 15s to 10s —
its content ends at ~9.5s and every render shipped a blank white tail.
The persistence-tier promotion surfaced two held content_overlap errors
on the hero heading; zooming in shows the collaborative-cursor demo
(cursor badge + typed comment bubble) hovering by design. Opt out with
data-layout-allow-overlap per the finding's own fixHint. The example's 4
pre-existing gsap_css_transform_conflict lint errors stay for a separate
example-maintenance pass.