Commit Graph
10 Commits
Author SHA1 Message Date
04acbb4d9e feat(registry): add 4 community components (#4906)
* feat(registry): add cinemagraph — a still photograph with one living region

* feat(registry): add ai-answer-rank — a mock AI answer that re-ranks to put your brand in it

A 1080x1080, 9s block for answer-engine and AI-visibility marketing. A prompt
types into a chat card, the assistant answers with a ranked list of three picks,
then re-ranks: the promoted brand slides into its slot (brandRank 1-3), the picks
below step down, the third fades out of the list, and the brand row lands with
the one accent highlight and a drawn check. Monochrome placeholder UI; prompt,
lead-in, pick names, brand, rank, accent, ink and ground are variables.

* feat(registry): add caret swap text effect

* feat(registry): add pendulum-swing component

Add a motion-primitive component that hangs content from a fixed pivot and
lets it settle with a damped oscillation, like a hanging sign coming to rest
on its hook.

The swing rides a single unitless custom property (--hf-swing-angle) that the
timeline eases 1 -> 0, so only rotation animates and it stays sub-pixel under
the seek-by-frame capture engine. The demo drives it with a damped-oscillation
ease (a cosine inside an exponential decay, normalised to land exactly upright)
so the sign passes through center several times, each pass smaller, before it
rests.

Four enum variables: pivot (top-center/left/right), amplitude
(gentle/standard/wide), direction (right/left), tone (ink/paper/accent, each a
bg/fg pair to keep AA contrast on any frame). The snippet is recipe-only and
does not own its timeline; its demo carries the same variable declaration.

Ran npx hyperframes lint/validate on both files, regenerated registry.json via
scripts/generate-registry-items.ts and the docs via
scripts/generate-catalog-pages.ts.

* fix(registry): drop hand-added cinemagraph index entry; catalog-publish adds it

* style(registry): format pendulum-swing with oxfmt

* fix(registry): keep pendulum-swing demo caption clear of the swinging sign

* fix(registry): ship the ad-template editing contract for ai-answer-rank

---------

Co-authored-by: Carlos Miranda Levy <carlosmirandalevy@gmail.com>
Co-authored-by: tugrawork-creator <319556794+tugrawork-creator@users.noreply.github.com>
Co-authored-by: Akshar Patel <aksharcommit@gmail.com>
Co-authored-by: rottenpen <12029924+rottenpen@users.noreply.github.com>
2026-10-02 17:14:52 +00:00
Miguel Ángel 8798e40da8 fix(registry): carousel cards fall back to their sample image inside the assets folder (#4543)
* fix(registry): carousel cards fall back to their sample image inside the assets folder

When an image variable resolves to nothing, the 25 carousel blocks loaded the
sample card by bare file name, which 404s beside the block and draws a white
card. The fallback now keeps assets/carousel-images/, the folder every card
image is installed into. A guard test fails on any block with a bare fallback.

* test(registry): the carousel fallback guard covers every block that uses it

The guard now picks each block that reads an image variable and fails when its
fallback no longer matches, instead of silently skipping it, and ignores stray
files in the blocks folder.
2026-09-26 15:26:19 +00:00
Miguel Ángel 7c634426fd docs(cli): explain how catalog search works and which model it downloads (#4462)
* docs(cli): explain how catalog search works and which model it downloads

* docs(cli): name the model catalog search was chosen against

* docs(cli): keep only live rules in the word-search header comment

* docs(cli): correct the hosted vector size in the local vector build comment
2026-09-25 04:43:54 +00:00
Miguel Ángel 3091dcb787 feat(cli): route media search through media-use (#4230)
* feat(cli): move media-use engine into the CLI

* fix(cli): replace media-use compatibility asset

* fix(cli): replace moved media-use tree

* fix(cli): copy moved media-use files cleanly

* fix(cli): copy media-use engine files individually

* fix(cli): keep audio helper compatibility import

* fix(cli): clear copied skill destinations

* test(cli): preserve moved media-use test paths

* fix(cli): preserve media-use script compatibility imports

* fix(cli): preserve media-use telemetry shim

* fix(cli): resolve packaged media-use LUT assets

* fix(cli): resolve packaged bundled SFX assets

* chore(skills): regenerate media-use manifest

* fix(cli): preserve media-use parity coverage

* fix(cli): cover relocated media-use tests

* chore(skills): regenerate media-use manifest

* fix(cli): format bundled media provider

* fix(ci): run relocated media-use tests with node

* fix(ci): preserve fallow script exemptions

* fix(ci): scope media-use fallow exemption

* refactor(cli): trim media-use essay comments

* feat(cli): index media-use assets for catalog search

* feat(catalog): commit media-use search vectors

* chore(catalog): track media vector metadata

* fix(catalog): validate media vector row order

* fix(catalog): validate media artifact freshness

* fix(catalog): cover media metadata and credits

* fix(cli): reject malformed media rows

* fix(catalog): compare complete media rows

* feat(cli): route media search through media-use

* refactor(cli): trim relocated media-use comments

* fix(cli): make media search runtime-safe

* fix(cli): preserve sfx fallback diagnostics

* fix(cli): probe sfx fallback provider explicitly

* chore(skills): regenerate manifest

* refactor(cli): keep typed media ranker source

* fix(cli): align media search checks with local-first flow

* fix(cli): scope vector audit exceptions

* fix(cli): ship resolver registry beside skill

* fix(cli): keep media-use skill scripts standalone

* test(cli): cover local vector validation

* test(cli): correct media vector fixtures

* fix(cli): prefer exact media keys

* fix(cli): stem exact media keys

* test(cli): skip media lint for snippets without media

* fix(cli): align skills test discovery

Keep the CI skills job's bash discovery as the single source of truth so its empty-list guard covers the exact files Node executes. Make hasMediaVectors private because it has no external callers and needs no Fallow suppression.

* fix(cli): close media-use review gaps

Prioritize literal ids before stemmed ids and cover the collision alongside the plural regression. Add command seam tests for engine discovery, actionable failure, and argv passthrough. Add a deterministic parity check for standalone media-use copies, with documented allowlists for the standalone media-fetch shim and self-contained npx resolver. Keep the bash-discovered skills test list as the CI source of truth.

* fix(cli): lower parity check complexity

Split missing-copy and unallowlisted-drift checks so the parity guard keeps its behavior without a threshold CRAP score.

* fix(cli): close media-use review seams

Run the parity test in the skills job so skills-only edits exercise the same check. Export the verb mapping and inject command dispatch for direct coverage. Keep missing-copy diagnostics separate from drift reads.
2026-09-21 18:35:47 -04:00
Miguel Ángel f24ff83834 feat(catalog): add the seven 3D-motion pieces as installable registry blocks (#4056)
* feat(catalog): add the seven 3D-motion pieces as installable registry blocks

Each piece ships as a registry block with a CDN poster and preview, a live
catalog detail page and its source. Scripts travel inside the catalog payload
(shared vendor libraries fetched once, item scripts inline, the one HDR as a
data URI) because the docs host only serves fonts, images and JSON.

NO_ESSAY_COMMENTS_OK: the long comment blocks are in vendored libraries and
the ported frost source under registry/blocks, not hand-written here.
HF_MAX_NONLFS_KB=6000: four hosted texture PNGs under docs/public/catalog/items
are runtime assets the docs host must serve as files.
fallow skipped locally: it audits against origin/main and this branch is
stacked on an older main.

* chore(catalog): regenerate gallery data and nav after rebasing onto main

* fix(catalog): read the install manifest through the bounded project reader

* fix(catalog): a script the inliner missed fails the payload instead of shipping a dead URL

* refactor(catalog): the leftover-script check reuses the reference finder

* fix(catalog): frost and glass textures load from the CDN and the iteration presets are gone

* chore(catalog): regenerate the frost and glass payloads and nav

* fix(catalog): frost and glass textures are hosted through the registry url field, not hardcoded

* chore(catalog): regenerate the frost and glass payloads against the hosted texture URLs

* fix(catalog): the hosted-reference rewrite refuses a bare name it cannot safely replace

* ci(catalog): verify changed catalog payloads in headless Chrome and check the docs catalog

* feat(catalog): every item page follows the shared layout with a Tune panel and tabs

* fix(catalog): let the texture-mask item host its masks so its tile plays live

* chore(catalog): regenerate item pages, gallery data and the texture-mask payload

* chore(catalog): regenerate item pages after merging main

* fix(catalog): declared-only variables get the Tune panel, Code and Docs tabs capped

* feat(catalog): give the blend-difference caption a demo scene so its tile plays live

* chore(catalog): live payload for the blend-difference caption

* perf(catalog): heavy 3d pieces compile and draw before ready, not on the first visible frame

* feat(catalog): the avatar promo card hosts its videos, audio and fonts on the CDN

* feat(catalog): the avatar promo card tile plays live from its CDN-hosted media

* chore(catalog): the avatar promo card plays live, no allowlist left

* chore(catalog): shorten two comment blocks

* chore(catalog): trim first-party comment blocks to the comment-check limits

* chore(catalog): shorten two frost source comments

* fix(catalog): the Docs tab renders as prose again, and the detail page honours reduced motion

Frame-mode pages carry no prose wrapper, and the detail component sat inside
not-prose, so the Docs slot's headings, paragraphs, links and lists had no
styling at all. The slots now carry their own prose classes and the chrome
around them keeps not-prose.

The recorded clip also gets the reduced-motion guard the docs-snippet check
requires, and the generator reads stability from the source metadata type so
the scripts typecheck passes.

* fix(catalog): docs-tab paragraphs render as blocks

Mintlify emits paragraphs as inline spans, so two paragraphs in the Docs slot ran together.

* fix(ci): drop the duplicate docs_catalog job output

git's line-based auto-merge silently duplicated the outputs key when
main and this branch each added it on non-adjacent lines during the
rebase; oxfmt's yaml parser rejects the repeated map key.

* fix(catalog): close four CodeQL alerts in the frost source

* chore(catalog): allowlist the avatar-promo-card CDN asset deletions, format sync-docs-catalog

check-no-main-deletions.mjs was rejecting 11 local mp4/font files this branch
already deleted in favor of CDN hosting; scripts/sync-docs-catalog.mjs had one
line over the 100-col print width from an earlier edit in this branch.

* fix(catalog): detail tabs use docs prose spacing, Install first; previews skip no-WebGPU runners

* fix(catalog): the docs tab scrolls with the page instead of a capped inner box

* refactor(catalog): split the payload asset and verify scripts into single-purpose helpers

* refactor(catalog): split the preview generator into single-purpose helpers

* ci(catalog): fail when the committed catalog differs from generator output

The docs host serves the committed catalog and nothing regenerates it, so a stale
copy shipped silently. The catalog payloads job now generates into a temp dir and
compares it with the committed tree.

* chore(catalog): regenerate docs/public/catalog with the refactored scripts

One full regeneration from an empty output tree with the current scripts. The tree grows
from 58.4 MB to 65.1 MB. Two hosted assets are about 2 MB each (an avatar png and a wav);
the same bytes are already committed under assets/, so HF_MAX_NONLFS_KB is raised for
this commit only.

* fix(catalog): run the catalog script tests under the runner they are written for

The rebase put three node:test files in the vitest half of test:scripts, where they report
"No test suite found". They now sit in the node --test list.

* fix(catalog): a WebGPU item without an adapter shows its recorded clip and says why

* ci(catalog): the drift check runs from a committed fetch mirror with no network

Generation now serves every font and CDN script fetch from scripts/catalog-fetch-mirror and fails
when a fetch is not in it, even if the caller swallowed the error. CATALOG_FETCH_MIRROR=record
refreshes the mirror from the live network. The docs_catalog filter also watches packages/core,
packages/producer and the mirror, and the 13 re-hashed font files are listed in ALLOWED_DELETIONS.

* chore(catalog): clean up the temp font cache and sort the mirror index without a locale

* fix(catalog): hide Tune when the clip stands in, time out the adapter request, share one check

* fix(catalog): the adapter probe survives a sync throw and a hang; test the clip guard

* fix(catalog): keep the adapter probe inside the component, Mintlify evaluates only exports

* fix(catalog): run the detail and page generator tests in CI; no Tune flash while probing WebGPU

* docs(catalog): credit vendored libraries and make each SKILL.md list what the block installs

CREDITS.md names every vendored dependency with its upstream and licence, and flags the two leaf
textures whose origin is still to be confirmed. Each SKILL.md Files table now comes from the block
manifest and its install and render paths point at compositions/<name>/. A test checks that every
path a SKILL.md names is a manifest file that exists.

* fix(catalog): a declared WebGPU item passes verify by its manifest tag, not by console wording

verify-catalog-payloads forgave a missing adapter only by matching the markup and the message, which
library-reported errors and widgets never satisfy. The webgpu tag in the item manifest is now the
owner: a tagged item ignores its page and console errors, an untagged one gets no forgiveness, and a
marker payload with no html is listed instead of crashing the run.

* docs(catalog): correct the credits per block and guard the mount and render paths

CREDITS.md states the three.js version and vendored-or-CDN status per block, adds d3-delaunay,
Clipper on the CDN and the Google Fonts loads, and lists every texture with no known origin.
The SKILL.md test now also checks the mount and render paths against the manifest composition.

* test(catalog): read each block manifest once in the SKILL.md file test

The two checks shared a copied manifest read, which fallow flagged as a clone with a high score.

* fix(catalog): commit the hosted item files a gitignore rule kept out, and check git not the disk

The compositions/ and *-ad/ ignore rules matched folder names under docs/public/catalog/items,
so 33 hosted fonts, textures and marks were generated but never committed and would 404 on
the docs host. The rules now have a negation for that tree and the files are added.

The drift check read the checkout, where git-ignored files still exist, so it passed locally
and failed only in CI. It now unpacks HEAD with git archive and compares that.

* fix(catalog): a tile keeps its poster until the composition has painted

* fix(catalog): a tagged WebGPU item forgives only the adapter-absent error, and test the wiring

The manifest webgpu tag now decides whether an item may lack an adapter, and the error wording
decides which lines are that absence. Any other page or console error on the item, and every
failed request, still fails. The per-item decision is one exported function the tests drive,
marker payloads are parsed by a tested helper, and the SKILL.md variable counts are checked
against their tables (cuboid-carousel said forty-nine for forty).

* fix(catalog): the pinned group's posters load eagerly at high priority

* fix(catalog): type the tile reveal test so the scripts typecheck passes

* fix(catalog): stop the adapter signature matching an unrelated failed adapter request

failed to request .*adapter also matched errors about other adapters. It now matches only a
request for the WebGPU adapter, so a tagged item cannot hide an unrelated failure behind it.

* test(catalog): a tagged item still fails on an error about some other adapter

* feat(catalog): the install command sits above the preview, as on the live catalog

* fix(catalog): find the install slot through Mintlify's child wrapper

* fix(docs): catalog copy buttons keep their size when the label swaps to Copied

* refactor(docs): name the shown copy label once in the catalog copy button

* fix(docs): primary catalog button keeps a readable label on hover and after copy

* fix(docs): inset the catalog tab strip and tab content to match the boxed panels
2026-09-19 16:53:29 -04:00
Miguel Ángel a0e7c43921 fix(catalog): fall back to recorded video when directory is over budget (#4084)
* fix(catalog): fall back to recorded video when directory is over budget

hostItemDirectory() returned "" for both "not needed" and "over the
2MB host budget", so an item that needed its own hosted directory but
exceeded the cap (texture-mask-text: 5.6MB of mask PNGs referenced via
runtime JS template strings, invisible to the static asset scanner)
silently shipped a payload with no <base href> and dead relative
image references, 404ing on the docs site. It now returns a tagged
result so the caller can fall back to the recorded video on
over-budget, same as the existing unresolved-assets fallback.

* test(catalog): cover the over-budget skip path in the payload generator

* chore(catalog): allowlist the deliberate over-budget payload deletion

* chore(catalog): dedupe hosted-directory assertions, note pre-existing complexity

Fallow's new-only gate flagged two problems this branch introduced: five
near-identical hostItemDirectory assertions in the file-race test collapse
into a shared expectHosted() helper, and the over-budget branch added to
buildPayload pushed it one branch past the complexity threshold on top of
already-high pre-existing debt (CRAP 240 before this branch, unrelated to
the fix). Suppressed with the repo's existing fallow-ignore-next-line
convention rather than refactoring an unrelated 115-line function inside
a bugfix PR.
2026-09-18 13:06:09 -04:00
James Russo d66cd6dcc8 fix(catalog): publish cache assets without overwriting entries (#3739)
* fix(catalog): publish cache assets without overwriting entries

* fix(catalog): publish cache assets without overwriting entries
2026-09-08 14:54:27 -04:00
James Russo 7a2a691736 fix(catalog): bind asset reads to checked project files (#3735)
* fix(catalog): bind asset reads to checked project files

* fix(catalog): budget the exact bytes published

* fix(catalog): bound directory asset reads
2026-09-06 01:18:16 -04:00
Miguel Ángel 406bf316a3 fix(catalog): make component previews answer their variables panel (#3323)
* fix(catalog): make component previews answer their variables panel

Every control on a component's catalog page did nothing. Asking
caption-camera-follow for a violet accent rendered gold, and so did green
and blue, on 166 of the 168 components that declare variables.

A component ships a snippet, which is what the page hands you to paste and
which carries the declaration plus the script that turns a chosen value
into a CSS custom property, and a demo.html which stages and animates it.
The preview is built from the demo, and the demo was authored as a copy of
the snippet rather than a reference to it. The copies drifted until almost
none of them carried the declaration or the reader, so the payload for that
page never contained the word violet at all.

Components come in two shapes, so the repair does too.

123 ship a snippet that registers its own paused timeline. That snippet is
a whole piece, so their preview is now built from it and carries markup,
variables and motion together.

45 are markup plus a commented recipe, where the demo owns the motion.
Those demos now carry the snippet's declaration, reader and var-driven CSS
in the registry itself, written by scripts/catalog/sync-demo-variables.ts.
Nothing is patched in at build time.

A test runs that tool in dry mode and fails when a demo has drifted again,
naming the command that repairs it. It also asserts it inspected more than
a hundred components, because a check that silently matches nothing is how
this rotted in the first place.

Measured by rendering every payload in a real player rather than by reading
markup: payloads declaring their variables go from 2 of 168 to 168 of 168,
previews that animate go from 166 to 167, and nothing that moved stopped
moving. ascii-render-pass and star-rating-fill render a still frame when
built from their snippet, so they keep the demo path as a recorded
exception and stay in the state they were already in.

* refactor(catalog): give the preview pipeline one lookup and one entrypoint guard

Follow-up on the same branch, no behaviour change: 42 tests still pass and
`sync-demo-variables --check` still reports all 168 components clean.

The payload generator and the demo sync had each grown their own copy of
"given a component directory, find the snippet and the demo". Both now call
`componentFiles`, which is the same duplication-by-copying that broke the
previews in the first place.

Both catalog generators also carried a byte-identical 12-line guard for
"only run main() when this file is the entrypoint". That clone was already
in the tree, but nothing had touched both files at once before, so it had
never surfaced. It is now `runAsCommand`, and the sync script's variant of
the same condition is `isEntrypoint`.

The rest is flattening: the layering guards read as a table of conditions
instead of a chain, the reporting splits by what it reports, and the entry
resolution comes out of `buildPayload` rather than being spliced into it.

Also runs the formatter over the demos this branch rewrote. Whitespace only,
and `notes-typing` is the only component demo that renders pre-formatted
text, which this does not touch.
2026-08-18 01:37:46 -04:00
Miguel Ángel 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
2026-08-09 22:59:15 -07:00