feat(cli): report which catalog items a render actually used (#3470)

`registry_item_added` fires when a catalog block is installed and
`render_complete` fires when a video is produced, but nothing joined them, so
"did this video use the catalog?" had no answer.

`hyperframes add` now records each installed item in `hyperframes.json`
(installed files are plain composition HTML with no provenance marker, so this
manifest is the only record that a file came from the registry), and
`render_complete` reports both the items the project installed and the blocks
the rendered composition actually reaches. An item installed and then never
mounted was tried and dropped, which no add-time event can express.

The scan answering "which sub-compositions does this file mount" now has one
owner, `collectSubCompositionSrcs` in `@hyperframes/parsers`, shared with
lint's `lintMissingOrEmptySubComposition`. It holds two invariants that were
previously restated per call site and got re-derived wrongly: it is a text scan
rather than a DOM query, because `<template>` content is inert and every
sub-composition except the render entry is wrapped in one; and references
resolve root-relative at every nesting level, matching `parseSubCompositions`.
It walks tag by tag rather than running open-ended spans across the whole file,
so a malformed composition cannot stall the render plan.

Also: `registryItems` is declared in the config schema, which closes with
`additionalProperties: false`, with an ajv-backed test pinning every key the CLI
writes; counts are never truncated by the name cap, and the reported used blocks
stay a subset of the reported installed ones, with `registry_items_truncated`
marking a windowed list; and an unreadable manifest reports itself rather than
posing as a project that never used the catalog.
This commit is contained in:
Miguel Ángel
2026-08-24 19:56:06 -04:00
committed by GitHub
parent b2fc18b2df
commit 045b3a4fd7
18 changed files with 1129 additions and 40 deletions
+4 -8
View File
@@ -7,6 +7,7 @@ import { checkSubCompositionUsability } from "@hyperframes/parsers/sub-compositi
import { parseHTML } from "linkedom";
import {
cleanAssetUrl,
collectSubCompositionSrcs,
isRemoteOrInlineUrl,
isUnresolvedAssetPlaceholder,
isWithinProjectRoot,
@@ -588,14 +589,9 @@ function lintMissingOrEmptySubComposition(
// fallow-ignore-next-line complexity
const walk = (html: string): void => {
const compositionSrcRe = /<[^>]*\bdata-composition-src\s*=\s*["']([^"']+)["'][^>]*>/gi;
const scannable = maskNonScannableRanges(html);
let match: RegExpExecArray | null;
while ((match = compositionSrcRe.exec(scannable)) !== null) {
const srcPath = (match[1] ?? "").trim();
if (!srcPath) continue;
if (isUnresolvedAssetPlaceholder(srcPath)) continue; // __UPPER__ placeholder or late-bound templating token
// Shared scanner — see collectSubCompositionSrcs for why this must be a
// text scan rather than a DOM query (template content is inert).
for (const srcPath of collectSubCompositionSrcs(html)) {
// data-composition-src is always written root-relative (even from a
// nested sub-composition) — matches the resolution the renderer uses
// in packages/producer/src/services/htmlCompiler.ts (parseSubCompositions