feat(core,cli): media-use interop — shared index.md regen + description/entity on figma imports (#1927)

Post-release review of media-use ↔ figma coupling (spec §13.1):
- figma asset imports now regenerate .media/index.md, the agent-readable
  inventory media-use maintains — format locked byte-identical via a
  cross-runner parity test against media-use's own index-gen.mjs
- figma asset --description/--entity land in the manifest record, the
  index table, and <img alt>; component rasterize auto-describes with
  the node name. Named brand marks become visible to media-use's
  resolve --entity lookups.
- spec §13.1 records the review verdict (loose coupling correct) and
  the follow-up queue (shared media-ledger module, global cache for
  figma assets, media-use version-keyed idempotency)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-03 22:56:43 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent cf594403ef
commit 3900caaaa9
12 changed files with 418 additions and 33 deletions
@@ -101,4 +101,56 @@ describe("runAssetImport", () => {
/node/i,
);
});
it("records description + entity and regenerates .media/index.md", async () => {
const dir = scratch();
const out = await runAssetImport(
"KEY:1-2",
{ format: "png", description: "hero illustration", entity: "Acme hero" },
deps(dir),
);
expect(out.record.description).toBe("hero illustration");
expect(out.record.entity).toBe("Acme hero");
expect(out.snippet.html).toContain('alt="hero illustration"');
const index = readFileSync(join(dir, ".media", "index.md"), "utf8");
expect(index).toContain("hero illustration");
expect(index).toContain(out.record.id);
});
it("applies --description/--entity on a reuse hit instead of dropping them", async () => {
const dir = scratch();
const first = await runAssetImport("KEY:1-2", { format: "png" }, deps(dir));
expect(first.record.description).toBeUndefined();
const again = await runAssetImport(
"KEY:1-2",
{ format: "png", description: "Acme logo", entity: "Acme logo" },
deps(dir),
);
expect(again.reused).toBe(true);
expect(again.record.description).toBe("Acme logo");
expect(again.snippet.html).toContain('alt="Acme logo"');
const index = readFileSync(join(dir, ".media", "index.md"), "utf8");
expect(index).toContain("Acme logo");
expect(index).not.toContain("image_002");
});
it("reuses against ANY matching tuple, not just the oldest row", async () => {
const dir = scratch();
await runAssetImport("KEY:1-2", { format: "svg" }, deps(dir)); // image_001 (svg)
const png = await runAssetImport("KEY:1-2", { format: "png" }, deps(dir)); // image_002
expect(png.reused).toBe(false);
const pngAgain = await runAssetImport("KEY:1-2", { format: "png" }, deps(dir));
expect(pngAgain.reused).toBe(true);
expect(pngAgain.record.id).toBe(png.record.id);
});
it("flattens whitespace in descriptions so index.md rows stay single-line", async () => {
const dir = scratch();
const out = await runAssetImport(
"KEY:1-2",
{ format: "png", description: "line one\nline two" },
deps(dir),
);
expect(out.record.description).toBe("line one line two");
});
});