Files
hyperframes/skills/media-use/scripts/lib/match.mjs
T
Miguel Angel Simon Sierra 35e54cae19 feat(media-use): agent-driven asset reuse (candidates + reuse)
Reuse now hands the semantic judgment to the coding agent instead of a
string heuristic, while keeping the deterministic normalize-exact match as
an automatic dedup floor. No LLM/embedding call enters resolve; it stays
offline-capable.

- lib/match.mjs: shared matchTokens + typesMatch (extracted from adopt.mjs
  and resolve.mjs so the icon<->image equivalence and token rules can't
  drift between the do-path and the look-path); adds tokenOverlap ranker.
- resolve --candidates: side-effect-free listing of reusable assets across
  the project manifest AND the global ~/.media cache, ranked by lexical
  overlap, capped per scope, --json or human table. Never hard-filters on
  zero overlap (that would pre-empt the agent's judgment); the agent decides.
- resolve --reuse <sha>: import a specific global-cache asset by content
  sha/prefix (from --candidates) into the project via importFromCache,
  marked source=reused-explicit / provenance.reused_by=agent.
- Adherence nudge: on a resolve that misses the exact floor and is about to
  fetch, print a one-line stderr hint when similar cached assets exist,
  pointing at --candidates. Offline, stderr (safe under --json), never
  auto-reuses a fuzzy match.
- cache.mjs: export readGlobalManifest; add findGlobalBySha (prefix resolve
  with ambiguity/miss handling).
- Telemetry: media_use_candidates event + reused-explicit source on
  media_use_resolve (type/scope/counts only, no intent text or paths).
- SKILL.md: 'Reuse before you resolve' guidance + trust guardrail
  (prefer-fresh-when-unsure, entity-exact for brand, cross-project bleed).
- Tests: lib/candidates.test.mjs (ranking, no-hard-filter, cap/truncation,
  icon<->image, sha resolution, formatter); adopt.mjs refactor covered by
  existing lib/adopt.test.mjs.

Full media-use suite green; verified e2e against the live catalog
(cross-project resolve->candidates->reuse; hint fires on miss).
2026-07-07 16:23:06 -04:00

47 lines
1.4 KiB
JavaScript

// Shared lexical-matching helpers used by both the assets/ scan (adopt.mjs) and
// the reuse-candidate ranker (candidates.mjs), and the type-equivalence check
// used by resolve.mjs and candidates.mjs. Kept in one place so the icon<->image
// equivalence and the token rules can't drift between the "do" path (resolve)
// and the "look" path (candidates).
// Common filler words that should never, on their own, make two strings match.
const MATCH_STOPWORDS = new Set([
"the",
"and",
"for",
"with",
"from",
"this",
"that",
"your",
"our",
]);
// Split into lowercased word tokens of length >= 3, minus stopwords.
export function matchTokens(text) {
return new Set(
String(text)
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter((t) => t.length >= 3 && !MATCH_STOPWORDS.has(t)),
);
}
// Count of shared meaningful word tokens between two strings. 0 = no lexical
// overlap (the candidate ranker still surfaces these, ordered after overlaps).
export function tokenOverlap(a, b) {
const ta = matchTokens(a);
const tb = matchTokens(b);
let n = 0;
for (const t of ta) if (tb.has(t)) n++;
return n;
}
// icon and image are interchangeable: both live in images/, and figma-imported
// brand marks are recorded as type image while agents ask for logos as icon.
export function typesMatch(a, b) {
if (a === b) return true;
const visual = new Set(["icon", "image"]);
return visual.has(a) && visual.has(b);
}