@tendrilapp/cli 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tendril.js CHANGED
@@ -1083,14 +1083,54 @@ function planSet(setDir, component, symbols, opts = {}) {
1083
1083
  writeManifest(setDir, manifest);
1084
1084
  return { manifest, plan, resumed: false };
1085
1085
  }
1086
+ function usableEnvelope(file, tool) {
1087
+ let payload;
1088
+ try {
1089
+ payload = JSON.parse(readFileSync(file, "utf8"));
1090
+ } catch {
1091
+ return { ok: false, reason: "the recorded file is not JSON" };
1092
+ }
1093
+ const schema = tool === "get_screenshot" ? ImageEnvelopeSchema : TextEnvelopeSchema;
1094
+ if (!schema.safeParse(payload).success) {
1095
+ return { ok: false, reason: "not a verbatim tool envelope (structure the ingest boundary would refuse)" };
1096
+ }
1097
+ const content = checkEnvelopeContent(tool, payload);
1098
+ if (!content.ok) return { ok: false, reason: content.reason };
1099
+ if (tool === "get_metadata" && !/name="/.test(envelopeTextContent(payload))) {
1100
+ return { ok: false, reason: "node markup carries no name attribute \u2014 task authoring cannot derive the pose" };
1101
+ }
1102
+ return { ok: true };
1103
+ }
1086
1104
  function sessionStatus(setDir) {
1087
1105
  const manifest = loadManifest(setDir);
1088
1106
  const reps = manifest.reps.map((rep) => {
1089
- const recorded = RECORD_TOOLS.filter((t) => existsSync(path.join(setDir, rep.slug, `${t}.json`)));
1090
- const missing = byProtocolOrder(requiredToolsFor(manifest, rep.slug).filter((t) => !recorded.includes(t)));
1091
- return { slug: rep.slug, nodeId: rep.nodeId, recorded, missing };
1107
+ const required = requiredToolsFor(manifest, rep.slug);
1108
+ const invalid = [];
1109
+ const recorded = RECORD_TOOLS.filter((t) => {
1110
+ const file = path.join(setDir, rep.slug, `${t}.json`);
1111
+ if (!existsSync(file)) return false;
1112
+ const usable = usableEnvelope(file, t);
1113
+ if (!usable.ok) {
1114
+ invalid.push({
1115
+ tool: t,
1116
+ reason: required.includes(t) ? usable.reason : `${usable.reason} (optional for this rep \u2014 not counted against completeness; re-record only if you need this evidence)`
1117
+ });
1118
+ return false;
1119
+ }
1120
+ return true;
1121
+ });
1122
+ const missing = byProtocolOrder(required.filter((t) => !recorded.includes(t)));
1123
+ return { slug: rep.slug, nodeId: rep.nodeId, recorded, missing, ...invalid.length > 0 ? { invalid } : {} };
1092
1124
  });
1093
- return { reps, complete: reps.every((r) => r.missing.length === 0) };
1125
+ const setDefsFile = path.join(setDir, "get_variable_defs.json");
1126
+ const setDefs = existsSync(setDefsFile) ? usableEnvelope(setDefsFile, "get_variable_defs") : { ok: false, reason: "never recorded" };
1127
+ const setTokenMapRecorded = setDefs.ok;
1128
+ return {
1129
+ reps,
1130
+ setTokenMapRecorded,
1131
+ ...!setDefs.ok && setDefs.reason !== "never recorded" ? { setTokenMapInvalid: setDefs.reason } : {},
1132
+ complete: reps.every((r) => r.missing.length === 0) && setTokenMapRecorded
1133
+ };
1094
1134
  }
1095
1135
  function nextInstruction(setDir) {
1096
1136
  const { reps } = sessionStatus(setDir);
@@ -1337,8 +1377,8 @@ var init_src = __esm({
1337
1377
  function variableNameToPath(name) {
1338
1378
  return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
1339
1379
  }
1340
- function tokenPathToCssVar(path40) {
1341
- return `--${path40.join("-")}`;
1380
+ function tokenPathToCssVar(path41) {
1381
+ return `--${path41.join("-")}`;
1342
1382
  }
1343
1383
  function toDtcgToken(variable, defaultMode) {
1344
1384
  const modes = Object.keys(variable.valuesByMode);
@@ -1382,11 +1422,11 @@ function toDtcgToken(variable, defaultMode) {
1382
1422
  }
1383
1423
  function mapVariablesToDtcg(variables, defaultMode = "light") {
1384
1424
  const entries = variables.map((variable) => {
1385
- const path40 = variableNameToPath(variable.name);
1386
- if (path40.length === 0) {
1425
+ const path41 = variableNameToPath(variable.name);
1426
+ if (path41.length === 0) {
1387
1427
  throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
1388
1428
  }
1389
- return { variable, path: path40 };
1429
+ return { variable, path: path41 };
1390
1430
  });
1391
1431
  const groupPrefixes = /* @__PURE__ */ new Set();
1392
1432
  for (const e of entries) {
@@ -1407,21 +1447,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
1407
1447
  }
1408
1448
  const tokens = {};
1409
1449
  const flat = [];
1410
- for (const { variable, path: path40 } of entries) {
1450
+ for (const { variable, path: path41 } of entries) {
1411
1451
  const token = toDtcgToken(variable, defaultMode);
1412
1452
  let group = tokens;
1413
- for (const segment of path40.slice(0, -1)) {
1453
+ for (const segment of path41.slice(0, -1)) {
1414
1454
  const existing = group[segment];
1415
1455
  group = existing ?? (group[segment] = {});
1416
1456
  }
1417
- const leaf = path40[path40.length - 1];
1457
+ const leaf = path41[path41.length - 1];
1418
1458
  if (group[leaf] !== void 0) {
1419
- throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path40.join(".")}" (variable ${variable.id})`);
1459
+ throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path41.join(".")}" (variable ${variable.id})`);
1420
1460
  }
1421
1461
  group[leaf] = token;
1422
1462
  flat.push({
1423
- path: path40.join("."),
1424
- cssVar: tokenPathToCssVar(path40),
1463
+ path: path41.join("."),
1464
+ cssVar: tokenPathToCssVar(path41),
1425
1465
  type: token.$type,
1426
1466
  value: token.$value
1427
1467
  });
@@ -1610,9 +1650,9 @@ function boundId(value) {
1610
1650
  return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
1611
1651
  }
1612
1652
  function resolveBinding(ctx, id) {
1613
- const path40 = ctx.pathById.get(id);
1614
- if (path40 === void 0) ctx.unresolved.add(id);
1615
- return path40;
1653
+ const path41 = ctx.pathById.get(id);
1654
+ if (path41 === void 0) ctx.unresolved.add(id);
1655
+ return path41;
1616
1656
  }
1617
1657
  function parseVariantProps(name) {
1618
1658
  if (!name.includes("=")) return void 0;
@@ -1647,8 +1687,8 @@ function walk(ctx, raw) {
1647
1687
  if (!isObject(paint) || paint["visible"] === false) continue;
1648
1688
  const id = boundId(paint);
1649
1689
  if (id !== void 0) {
1650
- const path40 = resolveBinding(ctx, id);
1651
- if (path40 !== void 0) tokens.add(path40);
1690
+ const path41 = resolveBinding(ctx, id);
1691
+ if (path41 !== void 0) tokens.add(path41);
1652
1692
  } else if (typeof paint["color"] === "string") {
1653
1693
  ctx.hardcoded.push({ node: name, property, value: paint["color"] });
1654
1694
  }
@@ -1656,8 +1696,8 @@ function walk(ctx, raw) {
1656
1696
  }
1657
1697
  const radiusId = boundId(raw["cornerRadius"]);
1658
1698
  if (radiusId !== void 0) {
1659
- const path40 = resolveBinding(ctx, radiusId);
1660
- if (path40 !== void 0) tokens.add(path40);
1699
+ const path41 = resolveBinding(ctx, radiusId);
1700
+ if (path41 !== void 0) tokens.add(path41);
1661
1701
  } else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
1662
1702
  ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
1663
1703
  }
@@ -1667,10 +1707,10 @@ function walk(ctx, raw) {
1667
1707
  layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
1668
1708
  const gapId = boundId(raw["itemSpacing"]);
1669
1709
  if (gapId !== void 0) {
1670
- const path40 = resolveBinding(ctx, gapId);
1671
- if (path40 !== void 0) {
1672
- layout.gap = path40;
1673
- tokens.add(path40);
1710
+ const path41 = resolveBinding(ctx, gapId);
1711
+ if (path41 !== void 0) {
1712
+ layout.gap = path41;
1713
+ tokens.add(path41);
1674
1714
  }
1675
1715
  } else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
1676
1716
  ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
@@ -1679,10 +1719,10 @@ function walk(ctx, raw) {
1679
1719
  for (const field of PADDING_FIELDS) {
1680
1720
  const id = boundId(raw[field]);
1681
1721
  if (id !== void 0) {
1682
- const path40 = resolveBinding(ctx, id);
1683
- if (path40 !== void 0) {
1684
- paddingPaths.push(path40);
1685
- tokens.add(path40);
1722
+ const path41 = resolveBinding(ctx, id);
1723
+ if (path41 !== void 0) {
1724
+ paddingPaths.push(path41);
1725
+ tokens.add(path41);
1686
1726
  }
1687
1727
  } else if (typeof raw[field] === "number" && raw[field] !== 0) {
1688
1728
  ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
@@ -3498,6 +3538,7 @@ function isCollection(bytes) {
3498
3538
  }
3499
3539
  function nameTableStrings(view, bytes, nameOffset, nameLength) {
3500
3540
  const out = /* @__PURE__ */ new Map();
3541
+ const rank = /* @__PURE__ */ new Map();
3501
3542
  if (nameOffset + 6 > bytes.length) return out;
3502
3543
  const count = view.getUint16(nameOffset + 2);
3503
3544
  const stringOffset = nameOffset + view.getUint16(nameOffset + 4);
@@ -3511,14 +3552,21 @@ function nameTableStrings(view, bytes, nameOffset, nameLength) {
3511
3552
  const offset = stringOffset + view.getUint16(rec + 10);
3512
3553
  if (offset + length > bytes.length || offset + length > nameOffset + nameLength) continue;
3513
3554
  const slice = bytes.subarray(offset, offset + length);
3514
- const isUtf16 = platformId === 3 || platformId === 0 || encodingId === 1;
3555
+ const languageId = view.getUint16(rec + 4);
3515
3556
  let value = "";
3516
- if (isUtf16) {
3557
+ if (platformId === 3 || platformId === 0) {
3517
3558
  for (let j = 0; j + 1 < slice.length; j += 2) value += String.fromCharCode(slice[j] << 8 | slice[j + 1]);
3518
- } else {
3559
+ } else if (platformId === 1 && encodingId === 0) {
3519
3560
  for (const b of slice) value += String.fromCharCode(b);
3561
+ } else {
3562
+ continue;
3563
+ }
3564
+ if (value === "") continue;
3565
+ const score = platformId === 3 && languageId === 1033 ? 5 : platformId === 0 ? 4 : platformId === 1 && languageId === 0 ? 3 : platformId === 3 ? 2 : 1;
3566
+ if (score > (rank.get(nameId) ?? 0)) {
3567
+ rank.set(nameId, score);
3568
+ out.set(nameId, value);
3520
3569
  }
3521
- if (value !== "" && !out.has(nameId)) out.set(nameId, value);
3522
3570
  }
3523
3571
  return out;
3524
3572
  }
@@ -3598,15 +3646,154 @@ var init_font_collection = __esm({
3598
3646
  }
3599
3647
  });
3600
3648
 
3601
- // packages/verify/src/font-resolve.ts
3602
- import { createHash } from "node:crypto";
3603
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
3649
+ // packages/verify/src/font-discovery.ts
3650
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
3604
3651
  import os2 from "node:os";
3605
3652
  import path10 from "node:path";
3653
+ function weightFromSubfamily(subfamily) {
3654
+ for (const [re, w] of WEIGHT_TOKENS) if (re.test(subfamily)) return w;
3655
+ return void 0;
3656
+ }
3657
+ function systemFaceRefusal(family) {
3658
+ if (/^\.?SF(NS|[ -]|$)/i.test(family) || /^San Francisco/i.test(family) || /^\.?Apple/i.test(family) || /^\.?New York\b/i.test(family) || /^\.?System Font\b/i.test(family)) {
3659
+ return "Apple's bundled system faces (San Francisco, New York, the Apple-named families) are licensed only for mock-ups of interfaces running on Apple operating systems, by registered Apple developers \u2014 a grant Tendril cannot verify, so these faces are never cached (ROADMAP 0i).";
3660
+ }
3661
+ if (/^\./.test(family)) {
3662
+ return "dot-prefixed families are the operating system's own hidden UI faces (macOS marks internal faces with a leading dot) \u2014 not user-licensable, so they are never cached (ROADMAP 0i).";
3663
+ }
3664
+ if (/^Segoe UI Variable/i.test(family)) {
3665
+ return "Segoe UI Variable cannot be licensed for use outside Microsoft products at all \u2014 an 'I have the rights' assertion has nothing it could attach to, so it is never cached (ROADMAP 0i).";
3666
+ }
3667
+ return void 0;
3668
+ }
3669
+ function faceAt(bytes, view, dirOffset, file, faceIndex) {
3670
+ if (dirOffset + 12 > bytes.length || !SFNT_VERSIONS.has(view.getUint32(dirOffset))) return null;
3671
+ const numTables = view.getUint16(dirOffset + 4);
3672
+ let names = /* @__PURE__ */ new Map();
3673
+ let os2Weight;
3674
+ let os2Italic = false;
3675
+ let variable = false;
3676
+ for (let t = 0; t < numTables; t++) {
3677
+ const rec = dirOffset + 12 + t * 16;
3678
+ if (rec + 16 > bytes.length) break;
3679
+ const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
3680
+ const offset = view.getUint32(rec + 8);
3681
+ const length = view.getUint32(rec + 12);
3682
+ if (tag === "name") names = nameTableStrings(view, bytes, offset, length);
3683
+ else if (tag === "OS/2" && offset + 64 <= bytes.length) {
3684
+ const w = view.getUint16(offset + 4);
3685
+ if (w >= 1 && w <= 1e3) os2Weight = w;
3686
+ os2Italic = (view.getUint16(offset + 62) & 1) !== 0;
3687
+ } else if (tag === "fvar") variable = true;
3688
+ }
3689
+ const family = names.get(16) ?? names.get(1);
3690
+ if (family === void 0 || family === "") return null;
3691
+ const subfamily = names.get(17) ?? names.get(2) ?? "";
3692
+ const italic = /italic|oblique|inclined|slanted/i.test(subfamily) || os2Italic;
3693
+ const weight = weightFromSubfamily(subfamily) ?? os2Weight ?? 400;
3694
+ return { family, subfamily, weight, italic, variable, file, ...faceIndex !== void 0 ? { faceIndex } : {} };
3695
+ }
3696
+ function facesInFile(file) {
3697
+ try {
3698
+ const bytes = new Uint8Array(readFileSync3(file));
3699
+ if (bytes.length < 12) return [];
3700
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
3701
+ if (isCollection(bytes)) {
3702
+ const numFonts = view.getUint32(8);
3703
+ const faces = [];
3704
+ for (let i = 0; i < numFonts; i++) {
3705
+ const f = faceAt(bytes, view, view.getUint32(12 + i * 4), file, i);
3706
+ if (f !== null) faces.push(f);
3707
+ }
3708
+ return faces;
3709
+ }
3710
+ const single = faceAt(bytes, view, 0, file);
3711
+ return single === null ? [] : [single];
3712
+ } catch {
3713
+ return [];
3714
+ }
3715
+ }
3716
+ function systemFontDirs() {
3717
+ const env = process.env["TENDRIL_SYSTEM_FONT_DIRS"];
3718
+ if (env !== void 0 && env !== "") return env.split(path10.delimiter).filter((d) => d !== "" && existsSync6(d));
3719
+ const home = os2.homedir();
3720
+ const dirs = process.platform === "darwin" ? ["/System/Library/Fonts", "/Library/Fonts", path10.join(home, "Library", "Fonts")] : process.platform === "win32" ? [
3721
+ path10.join(process.env["WINDIR"] ?? "C:\\Windows", "Fonts"),
3722
+ ...process.env["LOCALAPPDATA"] !== void 0 ? [path10.join(process.env["LOCALAPPDATA"], "Microsoft", "Windows", "Fonts")] : []
3723
+ ] : ["/usr/share/fonts", "/usr/local/share/fonts", path10.join(home, ".fonts"), path10.join(home, ".local", "share", "fonts")];
3724
+ return dirs.filter((d) => existsSync6(d));
3725
+ }
3726
+ function discoverSystemFaces(dirs = systemFontDirs(), depth = 3) {
3727
+ const seen = /* @__PURE__ */ new Set();
3728
+ const walk2 = (dirList, remaining) => {
3729
+ const faces = [];
3730
+ for (const dir of dirList) {
3731
+ let entries;
3732
+ try {
3733
+ entries = readdirSync2(dir, { withFileTypes: true });
3734
+ } catch {
3735
+ continue;
3736
+ }
3737
+ for (const e of entries) {
3738
+ const full = path10.join(dir, e.name);
3739
+ if (e.isDirectory()) {
3740
+ if (remaining > 1) faces.push(...walk2([full], remaining - 1));
3741
+ } else if (FONT_EXTENSIONS.has(path10.extname(e.name).toLowerCase())) {
3742
+ let key = full;
3743
+ try {
3744
+ key = realpathSync2(full);
3745
+ } catch {
3746
+ }
3747
+ if (seen.has(key)) continue;
3748
+ seen.add(key);
3749
+ faces.push(...facesInFile(full));
3750
+ }
3751
+ }
3752
+ }
3753
+ return faces;
3754
+ };
3755
+ return walk2(dirs, depth);
3756
+ }
3757
+ function systemFacesForFamily(family, dirs) {
3758
+ const want = normalize(family);
3759
+ return discoverSystemFaces(dirs).filter((f) => normalize(f.family) === want);
3760
+ }
3761
+ var WEIGHT_TOKENS, SFNT_VERSIONS, FONT_EXTENSIONS, normalize;
3762
+ var init_font_discovery = __esm({
3763
+ "packages/verify/src/font-discovery.ts"() {
3764
+ "use strict";
3765
+ init_font_collection();
3766
+ WEIGHT_TOKENS = [
3767
+ [/extra\s*light|ultra\s*light/i, 200],
3768
+ [/extra\s*bold|ultra\s*bold/i, 800],
3769
+ [/semi\s*bold|demi\s*bold|demi\b/i, 600],
3770
+ [/\bthin\b|\bhairline\b/i, 100],
3771
+ [/\blight\b/i, 300],
3772
+ [/\bmedium\b/i, 500],
3773
+ [/\bbold\b/i, 700],
3774
+ [/\bblack\b|\bheavy\b/i, 900],
3775
+ [/\bregular\b|\bnormal\b|\bbook\b|\broman\b/i, 400]
3776
+ ];
3777
+ SFNT_VERSIONS = /* @__PURE__ */ new Set([
3778
+ 65536,
3779
+ 1330926671,
3780
+ 1953658213
3781
+ /* true */
3782
+ ]);
3783
+ FONT_EXTENSIONS = /* @__PURE__ */ new Set([".ttf", ".otf", ".ttc"]);
3784
+ normalize = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
3785
+ }
3786
+ });
3787
+
3788
+ // packages/verify/src/font-resolve.ts
3789
+ import { createHash } from "node:crypto";
3790
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
3791
+ import os3 from "node:os";
3792
+ import path11 from "node:path";
3606
3793
  function fontCacheDir() {
3607
3794
  const env = process.env["TENDRIL_FONT_CACHE"];
3608
- if (env !== void 0 && env !== "") return path10.resolve(env);
3609
- return path10.join(os2.homedir(), ".tendril", "fonts");
3795
+ if (env !== void 0 && env !== "") return path11.resolve(env);
3796
+ return path11.join(os3.homedir(), ".tendril", "fonts");
3610
3797
  }
3611
3798
  function normalizeFontLicense(value) {
3612
3799
  return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
@@ -3671,29 +3858,29 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
3671
3858
  }
3672
3859
  const bytes = new Uint8Array(await fileRes.arrayBuffer());
3673
3860
  const sha256 = createHash("sha256").update(bytes).digest("hex");
3674
- const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
3861
+ const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
3675
3862
  writeFileSync3(file, bytes);
3676
3863
  resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
3677
3864
  } catch (err) {
3678
3865
  failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
3679
3866
  }
3680
3867
  }
3681
- const mPath = path10.join(cacheDir, "manifest.json");
3682
- const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3683
- const portable2 = resolved.map((m) => ({ ...m, file: path10.basename(m.file) }));
3868
+ const mPath = path11.join(cacheDir, "manifest.json");
3869
+ const prior = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
3870
+ const portable2 = resolved.map((m) => ({ ...m, file: path11.basename(m.file) }));
3684
3871
  const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
3685
3872
  if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
3686
3873
  `);
3687
3874
  return { resolved, failures };
3688
3875
  }
3689
- function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex) {
3690
- const src = path10.resolve(filePath);
3691
- if (!existsSync6(src)) throw new Error(`font file not found: ${src}`);
3692
- const ext = path10.extname(src).toLowerCase();
3876
+ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex, provenance = "local") {
3877
+ const src = path11.resolve(filePath);
3878
+ if (!existsSync7(src)) throw new Error(`font file not found: ${src}`);
3879
+ const ext = path11.extname(src).toLowerCase();
3693
3880
  if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
3694
3881
  throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
3695
3882
  }
3696
- let bytes = new Uint8Array(readFileSync3(src));
3883
+ let bytes = new Uint8Array(readFileSync4(src));
3697
3884
  if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
3698
3885
  let storedExt = ext;
3699
3886
  if (isCollection(bytes)) {
@@ -3703,7 +3890,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
3703
3890
  const all = listCollectionFaces(bytes);
3704
3891
  const shown = (candidates.length > 0 ? candidates : all).map((f) => ` --face ${f.index} ${f.family ?? "(unnamed)"}${f.subfamily !== void 0 ? ` ${f.subfamily}` : ""}`).join("\n");
3705
3892
  throw new Error(
3706
- `${path10.basename(src)} is a collection of ${all.length} faces and ${candidates.length === 0 ? `none is named "${family}"` : `${candidates.length} match "${family}"`} \u2014 name the one you mean with --face <index>:
3893
+ `${path11.basename(src)} is a collection of ${all.length} faces and ${candidates.length === 0 ? `none is named "${family}"` : `${candidates.length} match "${family}"`} \u2014 name the one you mean with --face <index>:
3707
3894
  ${shown}`
3708
3895
  );
3709
3896
  }
@@ -3712,20 +3899,20 @@ ${shown}`
3712
3899
  }
3713
3900
  mkdirSync2(cacheDir, { recursive: true });
3714
3901
  const sha256 = createHash("sha256").update(bytes).digest("hex");
3715
- const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
3902
+ const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
3716
3903
  writeFileSync3(file, bytes);
3717
- const face = { family, weight, source: `local:${path10.basename(src)}`, sha256, file, license: "unknown" };
3718
- const mPath = path10.join(cacheDir, "manifest.json");
3719
- const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3720
- const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path10.basename(file) }];
3904
+ const face = { family, weight, source: `${provenance}:${path11.basename(src)}`, sha256, file, license: "unknown" };
3905
+ const mPath = path11.join(cacheDir, "manifest.json");
3906
+ const prior = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
3907
+ const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path11.basename(file) }];
3721
3908
  writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
3722
3909
  `);
3723
3910
  return face;
3724
3911
  }
3725
3912
  function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
3726
- const lock = JSON.parse(readFileSync3(lockPath, "utf8"));
3727
- const mPath = path10.join(cacheDir, "manifest.json");
3728
- const manifest = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3913
+ const lock = JSON.parse(readFileSync4(lockPath, "utf8"));
3914
+ const mPath = path11.join(cacheDir, "manifest.json");
3915
+ const manifest = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
3729
3916
  return lock.map((l) => {
3730
3917
  const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
3731
3918
  if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
@@ -3733,11 +3920,11 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
3733
3920
  });
3734
3921
  }
3735
3922
  function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3736
- const mPath = path10.join(cacheDir, "manifest.json");
3737
- if (!existsSync6(mPath)) return [];
3923
+ const mPath = path11.join(cacheDir, "manifest.json");
3924
+ if (!existsSync7(mPath)) return [];
3738
3925
  let entries;
3739
3926
  try {
3740
- entries = JSON.parse(readFileSync3(mPath, "utf8"));
3927
+ entries = JSON.parse(readFileSync4(mPath, "utf8"));
3741
3928
  } catch {
3742
3929
  return [];
3743
3930
  }
@@ -3751,8 +3938,8 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3751
3938
  return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
3752
3939
  }
3753
3940
  function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
3754
- const mPath = path10.join(cacheDir, "manifest.json");
3755
- const manifest = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3941
+ const mPath = path11.join(cacheDir, "manifest.json");
3942
+ const manifest = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
3756
3943
  const wanted = new Set(families.map((f) => f.toLowerCase()));
3757
3944
  return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
3758
3945
  family: f.family,
@@ -3765,11 +3952,11 @@ function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
3765
3952
  }));
3766
3953
  }
3767
3954
  function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
3768
- const mPath = path10.join(cacheDir, "manifest.json");
3769
- if (!existsSync6(mPath)) return [];
3955
+ const mPath = path11.join(cacheDir, "manifest.json");
3956
+ if (!existsSync7(mPath)) return [];
3770
3957
  let entries;
3771
3958
  try {
3772
- entries = JSON.parse(readFileSync3(mPath, "utf8"));
3959
+ entries = JSON.parse(readFileSync4(mPath, "utf8"));
3773
3960
  } catch {
3774
3961
  return [];
3775
3962
  }
@@ -3778,31 +3965,88 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
3778
3965
  ).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
3779
3966
  }
3780
3967
  function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3781
- const mPath = path10.join(cacheDir, "manifest.json");
3782
- if (!existsSync6(mPath)) return [];
3968
+ const mPath = path11.join(cacheDir, "manifest.json");
3969
+ if (!existsSync7(mPath)) return [];
3783
3970
  let entries;
3784
3971
  try {
3785
- entries = JSON.parse(readFileSync3(mPath, "utf8"));
3972
+ entries = JSON.parse(readFileSync4(mPath, "utf8"));
3786
3973
  } catch {
3787
3974
  return [];
3788
3975
  }
3789
3976
  const byFamily = /* @__PURE__ */ new Map();
3790
3977
  for (const e of entries) {
3791
3978
  if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
3792
- const file = path10.isAbsolute(e.file) && existsSync6(e.file) ? e.file : path10.resolve(cacheDir, path10.basename(e.file));
3793
- if (!existsSync6(file)) continue;
3794
- if (createHash("sha256").update(readFileSync3(file)).digest("hex") !== e.sha256) continue;
3979
+ const file = path11.isAbsolute(e.file) && existsSync7(e.file) ? e.file : path11.resolve(cacheDir, path11.basename(e.file));
3980
+ if (!existsSync7(file)) continue;
3981
+ if (createHash("sha256").update(readFileSync4(file)).digest("hex") !== e.sha256) continue;
3795
3982
  const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
3796
3983
  set.add(e.weight);
3797
3984
  byFamily.set(e.family, set);
3798
3985
  }
3799
3986
  return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
3800
3987
  }
3988
+ function addSystemFamily(family, opts = {}) {
3989
+ const refusal = systemFaceRefusal(family.trim());
3990
+ if (refusal !== void 0) return { added: [], skipped: [], overwrote: [], refusal };
3991
+ const plainStyle = (sub) => weightFromSubfamily(sub) !== void 0 || /^(regular|plain|)$/i.test(sub.trim());
3992
+ const faces = [...systemFacesForFamily(family, opts.dirs)].sort((a, b) => {
3993
+ const pa = plainStyle(a.subfamily) ? 0 : 1;
3994
+ const pb = plainStyle(b.subfamily) ? 0 : 1;
3995
+ if (pa !== pb) return pa - pb;
3996
+ return a.subfamily.localeCompare(b.subfamily) || a.file.localeCompare(b.file) || (a.faceIndex ?? 0) - (b.faceIndex ?? 0);
3997
+ });
3998
+ const added = [];
3999
+ const skipped = [];
4000
+ const overwrote = [];
4001
+ const cacheDir = opts.cacheDir ?? DEFAULT_FONT_CACHE;
4002
+ const manifestFile = path11.join(cacheDir, "manifest.json");
4003
+ const prior = existsSync7(manifestFile) ? JSON.parse(readFileSync4(manifestFile, "utf8")) : [];
4004
+ const taken = /* @__PURE__ */ new Set();
4005
+ for (const face of faces) {
4006
+ const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
4007
+ const faceRefusal = systemFaceRefusal(face.family.trim());
4008
+ if (faceRefusal !== void 0) {
4009
+ skip(`refusal-class family \u2014 ${faceRefusal}`);
4010
+ continue;
4011
+ }
4012
+ if (opts.weights !== void 0 && !opts.weights.includes(face.weight)) continue;
4013
+ if (face.italic) {
4014
+ skip("italic \u2014 the cache cannot represent italic faces end to end yet; the upright face serves the family");
4015
+ continue;
4016
+ }
4017
+ if (face.variable) {
4018
+ skip("variable font \u2014 the mount injects STATIC faces (a variable file renders thinner stems than recorded rasters, measured); install or point at a static instance");
4019
+ continue;
4020
+ }
4021
+ if (/condensed|narrow|compressed|expanded|extended/i.test(face.subfamily)) {
4022
+ skip("width variant \u2014 a Condensed/Expanded face is a different design, not a weight of this family");
4023
+ continue;
4024
+ }
4025
+ if (taken.has(face.weight)) {
4026
+ skip(`another face already provided weight ${face.weight}`);
4027
+ continue;
4028
+ }
4029
+ taken.add(face.weight);
4030
+ const existing = prior.find((p) => p.family.toLowerCase() === face.family.toLowerCase() && p.weight === face.weight);
4031
+ if (existing !== void 0 && !existing.source.startsWith("system:")) {
4032
+ overwrote.push({
4033
+ family: face.family,
4034
+ weight: face.weight,
4035
+ priorSource: existing.source,
4036
+ priorSha256: existing.sha256,
4037
+ priorLicense: normalizeFontLicense(existing.license)
4038
+ });
4039
+ }
4040
+ added.push({ ...addLocalFont(face.family, face.weight, face.file, cacheDir, face.faceIndex, "system"), subfamily: face.subfamily || "Regular" });
4041
+ }
4042
+ return { added, skipped, overwrote };
4043
+ }
3801
4044
  var DEFAULT_FONT_CACHE, UA, FONT_LICENSES, GOOGLE_LICENSE_IDS, GOOGLE_FONT_FILE_PREFIX;
3802
4045
  var init_font_resolve = __esm({
3803
4046
  "packages/verify/src/font-resolve.ts"() {
3804
4047
  "use strict";
3805
4048
  init_font_collection();
4049
+ init_font_discovery();
3806
4050
  DEFAULT_FONT_CACHE = fontCacheDir();
3807
4051
  UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36";
3808
4052
  FONT_LICENSES = ["OFL-1.1", "Apache-2.0", "UFL-1.0", "proprietary", "unknown"];
@@ -3818,17 +4062,17 @@ var init_font_resolve = __esm({
3818
4062
 
3819
4063
  // packages/verify/src/font-faces.ts
3820
4064
  import { createHash as createHash2 } from "node:crypto";
3821
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
3822
- import path11 from "node:path";
4065
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
4066
+ import path12 from "node:path";
3823
4067
  function injectedGroups(manifestPath2) {
3824
- if (!existsSync7(manifestPath2)) return { groups: [], shared: false };
3825
- const claimed = JSON.parse(readFileSync4(manifestPath2, "utf8"));
3826
- const resolveFile = (f) => path11.isAbsolute(f) && existsSync7(f) ? f : path11.resolve(path11.dirname(manifestPath2), path11.basename(f));
4068
+ if (!existsSync8(manifestPath2)) return { groups: [], shared: false };
4069
+ const claimed = JSON.parse(readFileSync5(manifestPath2, "utf8"));
4070
+ const resolveFile = (f) => path12.isAbsolute(f) && existsSync8(f) ? f : path12.resolve(path12.dirname(manifestPath2), path12.basename(f));
3827
4071
  const byFile = /* @__PURE__ */ new Map();
3828
4072
  for (const f of claimed) {
3829
4073
  const file = resolveFile(f.file);
3830
- if (!existsSync7(file)) continue;
3831
- if (createHash2("sha256").update(readFileSync4(file)).digest("hex") !== f.sha256) continue;
4074
+ if (!existsSync8(file)) continue;
4075
+ if (createHash2("sha256").update(readFileSync5(file)).digest("hex") !== f.sha256) continue;
3832
4076
  const k = `${f.family}:${f.file}`;
3833
4077
  const e = byFile.get(k) ?? { family: f.family, weights: [], file };
3834
4078
  e.weights.push(f.weight);
@@ -3837,14 +4081,14 @@ function injectedGroups(manifestPath2) {
3837
4081
  const groups = [...byFile.values()];
3838
4082
  return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
3839
4083
  }
3840
- function fontFaceCss(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
4084
+ function fontFaceCss(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
3841
4085
  const { groups, shared } = injectedGroups(manifestPath2);
3842
4086
  return groups.map((e) => {
3843
4087
  const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
3844
- return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync4(e.file).toString("base64")}) format('woff2'); }`;
4088
+ return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync5(e.file).toString("base64")}) format('woff2'); }`;
3845
4089
  }).join("\n");
3846
4090
  }
3847
- function injectedFamilyWeights(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
4091
+ function injectedFamilyWeights(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
3848
4092
  const { groups, shared } = injectedGroups(manifestPath2);
3849
4093
  const out = /* @__PURE__ */ new Map();
3850
4094
  for (const g of groups) {
@@ -3869,17 +4113,17 @@ var init_font_faces = __esm({
3869
4113
  });
3870
4114
 
3871
4115
  // packages/verify/src/admission.ts
3872
- import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync8, writeFileSync as writeFileSync4 } from "node:fs";
3873
- import path12 from "node:path";
4116
+ import { readFileSync as readFileSync6, readdirSync as readdirSync3, existsSync as existsSync9, writeFileSync as writeFileSync4 } from "node:fs";
4117
+ import path13 from "node:path";
3874
4118
  import { build as build2 } from "esbuild";
3875
4119
  import postcss from "postcss";
3876
4120
  import tailwindcss from "tailwindcss";
3877
4121
  import { chromium as chromium2 } from "playwright-core";
3878
4122
  function fontWeightsByFamily() {
3879
- const mPath = path12.join(fontCacheDir(), "manifest.json");
4123
+ const mPath = path13.join(fontCacheDir(), "manifest.json");
3880
4124
  const out = /* @__PURE__ */ new Map();
3881
- if (!existsSync8(mPath)) return out;
3882
- for (const f of JSON.parse(readFileSync5(mPath, "utf8")))
4125
+ if (!existsSync9(mPath)) return out;
4126
+ for (const f of JSON.parse(readFileSync6(mPath, "utf8")))
3883
4127
  out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
3884
4128
  return out;
3885
4129
  }
@@ -3899,7 +4143,7 @@ var init_admission = __esm({
3899
4143
  });
3900
4144
 
3901
4145
  // packages/verify/src/tasks.ts
3902
- import path13 from "node:path";
4146
+ import path14 from "node:path";
3903
4147
  var CALENDAR_CONFIGS, CALENDAR_API, CALENDAR_BEHAVIORS, BUTTON_CONFIGS, BUTTON_API, COMBO_FIX, COMBO_CONFIGS, COMBO_API, MODAL_CONFIGS, MODAL_API, BUTTON_BEHAVIORS, COMBO_BEHAVIORS, MODAL_BEHAVIORS, TASKS;
3904
4148
  var init_tasks = __esm({
3905
4149
  "packages/verify/src/tasks.ts"() {
@@ -4059,7 +4303,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4059
4303
  ];
4060
4304
  TASKS = {
4061
4305
  calendar: {
4062
- set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
4306
+ set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
4063
4307
  entry: "Calendar.tsx",
4064
4308
  configs: CALENDAR_CONFIGS,
4065
4309
  systemApi: CALENDAR_API,
@@ -4067,7 +4311,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4067
4311
  prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
4068
4312
  },
4069
4313
  "shadcn-button": {
4070
- set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
4314
+ set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
4071
4315
  entry: "Button.tsx",
4072
4316
  configs: BUTTON_CONFIGS,
4073
4317
  systemApi: BUTTON_API,
@@ -4075,7 +4319,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4075
4319
  prelude: { controls: ["> *"], textInputs: [] }
4076
4320
  },
4077
4321
  combobox: {
4078
- set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
4322
+ set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
4079
4323
  entry: "ComboBox.tsx",
4080
4324
  configs: COMBO_CONFIGS,
4081
4325
  systemApi: COMBO_API,
@@ -4083,7 +4327,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4083
4327
  prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
4084
4328
  },
4085
4329
  modal: {
4086
- set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
4330
+ set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
4087
4331
  entry: "Modal.tsx",
4088
4332
  configs: MODAL_CONFIGS,
4089
4333
  systemApi: MODAL_API,
@@ -4101,8 +4345,8 @@ __export(behavior_exports, {
4101
4345
  compileMount: () => compileMount,
4102
4346
  recordingIsDark: () => recordingIsDark
4103
4347
  });
4104
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
4105
- import path14 from "node:path";
4348
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
4349
+ import path15 from "node:path";
4106
4350
  import { build as build3 } from "esbuild";
4107
4351
  import { chromium as chromium3 } from "playwright-core";
4108
4352
  import { PNG as PNG2 } from "pngjs";
@@ -4111,12 +4355,12 @@ function getFontFaces() {
4111
4355
  return _fontFaces;
4112
4356
  }
4113
4357
  async function compileMount(task, bundleDir) {
4114
- const entryTsx = path14.join(bundleDir, task.entry);
4115
- if (!existsSync9(entryTsx)) return { error: `${task.entry} missing` };
4358
+ const entryTsx = path15.join(bundleDir, task.entry);
4359
+ if (!existsSync10(entryTsx)) return { error: `${task.entry} missing` };
4116
4360
  const mountSrc = `
4117
4361
  import { createElement } from "react";
4118
4362
  import { createRoot } from "react-dom/client";
4119
- import * as B from ${JSON.stringify(path14.resolve(entryTsx))};
4363
+ import * as B from ${JSON.stringify(path15.resolve(entryTsx))};
4120
4364
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
4121
4365
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
4122
4366
  // Callbacks cannot ride the JSON config: specs NAME spy props and the
@@ -4397,10 +4641,10 @@ async function runSteps(page, spec, renderPose) {
4397
4641
  function recordingIsDark(task) {
4398
4642
  const rep = task.configs[0]?.rep;
4399
4643
  if (rep === void 0) return false;
4400
- const f = path14.join(task.set, rep, "get_screenshot.json");
4401
- if (!existsSync9(f)) return false;
4644
+ const f = path15.join(task.set, rep, "get_screenshot.json");
4645
+ if (!existsSync10(f)) return false;
4402
4646
  try {
4403
- const env = JSON.parse(readFileSync6(f, "utf8")).content.find((c) => c.type === "image");
4647
+ const env = JSON.parse(readFileSync7(f, "utf8")).content.find((c) => c.type === "image");
4404
4648
  if (env?.data === void 0) return false;
4405
4649
  const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
4406
4650
  let sum = 0;
@@ -4472,7 +4716,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
4472
4716
  const deadlineMs = timeoutMs + 1e4;
4473
4717
  const js = await compileMount(task, bundleDir);
4474
4718
  if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
4475
- const css = ["tokens.css", "styles.css"].map((f) => path14.join(bundleDir, f)).filter((f) => existsSync9(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
4719
+ const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) => existsSync10(f)).map((f) => readFileSync7(f, "utf8")).join("\n");
4476
4720
  const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
4477
4721
  const browser = await chromium3.connect(server.wsEndpoint());
4478
4722
  const results = [];
@@ -4615,8 +4859,8 @@ var init_behavior = __esm({
4615
4859
  });
4616
4860
 
4617
4861
  // packages/verify/src/bundle-quality.ts
4618
- import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "node:fs";
4619
- import path15 from "node:path";
4862
+ import { readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "node:fs";
4863
+ import path16 from "node:path";
4620
4864
  function definedVars(tokensCss) {
4621
4865
  if (tokensCss === void 0) return void 0;
4622
4866
  const names = /* @__PURE__ */ new Set();
@@ -4625,19 +4869,19 @@ function definedVars(tokensCss) {
4625
4869
  }
4626
4870
  function recordedTokenMapState(setDir, reps) {
4627
4871
  const readMap = (file) => {
4628
- if (!existsSync10(file)) return void 0;
4872
+ if (!existsSync11(file)) return void 0;
4629
4873
  try {
4630
- const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync7(file, "utf8"))) || "{}");
4874
+ const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync8(file, "utf8"))) || "{}");
4631
4875
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4632
4876
  } catch {
4633
4877
  return {};
4634
4878
  }
4635
4879
  };
4636
- const setLevel = readMap(path15.join(setDir, "get_variable_defs.json"));
4880
+ const setLevel = readMap(path16.join(setDir, "get_variable_defs.json"));
4637
4881
  if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
4638
4882
  let recorded = false;
4639
4883
  for (const rep of reps) {
4640
- const m = readMap(path15.join(setDir, rep, "get_variable_defs.json"));
4884
+ const m = readMap(path16.join(setDir, rep, "get_variable_defs.json"));
4641
4885
  if (m === void 0) continue;
4642
4886
  recorded = true;
4643
4887
  if (Object.keys(m).length > 0) return "populated";
@@ -4702,11 +4946,11 @@ function fontStackFindings(sheets, coverage) {
4702
4946
  }
4703
4947
  async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
4704
4948
  const findings = [];
4705
- const entryPath = path15.join(bundleDir, entry);
4706
- const cssPath = path15.join(bundleDir, "styles.css");
4707
- const tokensPath = path15.join(bundleDir, "tokens.css");
4708
- const css = existsSync10(cssPath) ? readFileSync7(cssPath, "utf8") : "";
4709
- const tokensCss = existsSync10(tokensPath) ? readFileSync7(tokensPath, "utf8") : void 0;
4949
+ const entryPath = path16.join(bundleDir, entry);
4950
+ const cssPath = path16.join(bundleDir, "styles.css");
4951
+ const tokensPath = path16.join(bundleDir, "tokens.css");
4952
+ const css = existsSync11(cssPath) ? readFileSync8(cssPath, "utf8") : "";
4953
+ const tokensCss = existsSync11(tokensPath) ? readFileSync8(tokensPath, "utf8") : void 0;
4710
4954
  findings.push(
4711
4955
  ...fontStackFindings(
4712
4956
  [
@@ -4716,11 +4960,11 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
4716
4960
  injectedFamilyWeights(fontManifest)
4717
4961
  )
4718
4962
  );
4719
- if (existsSync10(entryPath)) {
4963
+ if (existsSync11(entryPath)) {
4720
4964
  const workDir = newScratchDir("quality");
4721
4965
  try {
4722
- const tsxPath = path15.join(workDir, entry);
4723
- writeFileSync5(tsxPath, readFileSync7(entryPath, "utf8"));
4966
+ const tsxPath = path16.join(workDir, entry);
4967
+ writeFileSync5(tsxPath, readFileSync8(entryPath, "utf8"));
4724
4968
  for (const d of runTscStrict([tsxPath]).diagnostics) {
4725
4969
  findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
4726
4970
  }
@@ -4795,8 +5039,8 @@ var init_effect_geometry = __esm({
4795
5039
  });
4796
5040
 
4797
5041
  // packages/verify/src/bundle-score.ts
4798
- import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
4799
- import path16 from "node:path";
5042
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
5043
+ import path17 from "node:path";
4800
5044
  import { build as build4 } from "esbuild";
4801
5045
  import { chromium as chromium4 } from "playwright-core";
4802
5046
  function getFontFaces2() {
@@ -4850,7 +5094,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
4850
5094
  }
4851
5095
  function metadataRoot(set, rep) {
4852
5096
  try {
4853
- const text = JSON.parse(readFileSync8(path16.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
5097
+ const text = JSON.parse(readFileSync9(path17.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
4854
5098
  return parseMetadataStructure(text);
4855
5099
  } catch {
4856
5100
  return void 0;
@@ -4905,19 +5149,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
4905
5149
  });
4906
5150
  }
4907
5151
  function repMeta(set, rep) {
4908
- const text = JSON.parse(readFileSync8(path16.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
5152
+ const text = JSON.parse(readFileSync9(path17.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
4909
5153
  const root = parseMetadataStructure(text);
4910
5154
  return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
4911
5155
  }
4912
5156
  function repRef(set, rep) {
4913
- const env = JSON.parse(readFileSync8(path16.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
5157
+ const env = JSON.parse(readFileSync9(path17.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
4914
5158
  return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
4915
5159
  }
4916
5160
  function repEffectExtents(set, rep) {
4917
- const file = path16.join(set, rep, "get_design_context.json");
4918
- if (!existsSync11(file)) return void 0;
5161
+ const file = path17.join(set, rep, "get_design_context.json");
5162
+ if (!existsSync12(file)) return void 0;
4919
5163
  try {
4920
- const text = JSON.parse(readFileSync8(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
5164
+ const text = JSON.parse(readFileSync9(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
4921
5165
  const extents = shadowExtents(text);
4922
5166
  return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
4923
5167
  } catch {
@@ -4928,13 +5172,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
4928
5172
  if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
4929
5173
  const CONFIGS2 = task.configs;
4930
5174
  const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
4931
- const entryTsx = path16.join(bundleDir, task.entry);
4932
- if (!existsSync11(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
4933
- const css = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f)).filter((f) => existsSync11(f)).map((f) => readFileSync8(f, "utf8")).join("\n");
5175
+ const entryTsx = path17.join(bundleDir, task.entry);
5176
+ if (!existsSync12(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
5177
+ const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
4934
5178
  const mountSrc = `
4935
5179
  import { createElement } from "react";
4936
5180
  import { createRoot } from "react-dom/client";
4937
- import * as B from ${JSON.stringify(path16.resolve(entryTsx))};
5181
+ import * as B from ${JSON.stringify(path17.resolve(entryTsx))};
4938
5182
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
4939
5183
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
4940
5184
  const root = document.getElementById("root");
@@ -5029,12 +5273,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
5029
5273
  return name === void 0 ? c : { ...c, name };
5030
5274
  });
5031
5275
  if (opts.evidenceDir !== void 0) {
5032
- writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
5033
- writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
5034
- writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
5276
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
5277
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
5278
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
5035
5279
  for (const [i, c] of absent.slice(0, 3).entries()) {
5036
- writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
5037
- writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
5280
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
5281
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
5038
5282
  }
5039
5283
  }
5040
5284
  return {
@@ -5142,15 +5386,15 @@ var init_prelude = __esm({
5142
5386
  });
5143
5387
 
5144
5388
  // packages/verify/src/parity.ts
5145
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
5146
- import path17 from "node:path";
5389
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
5390
+ import path18 from "node:path";
5147
5391
  import { chromium as chromium6 } from "playwright-core";
5148
5392
  function getFontFaces3() {
5149
5393
  _fontFaces3 ??= fontFaceCss();
5150
5394
  return _fontFaces3;
5151
5395
  }
5152
- function hoverForcedConfigs(task) {
5153
- return task.configs.filter((c) => {
5396
+ function hoverForcedConfigs(authority) {
5397
+ return authority.filter((c) => {
5154
5398
  const forced = c.props["data-tendril-state"];
5155
5399
  return typeof forced === "string" && forced.split(/\s+/).includes("hover");
5156
5400
  });
@@ -5165,27 +5409,37 @@ function withoutHoverToken(props) {
5165
5409
  }
5166
5410
  return rest;
5167
5411
  }
5168
- async function checkHoverParity(task, bundleDir, opts = {}) {
5169
- const configs = hoverForcedConfigs(task);
5412
+ async function checkHoverParity(task, bundleDir, authority, opts = {}) {
5413
+ const configs = hoverForcedConfigs(authority);
5170
5414
  if (configs.length === 0) return [];
5171
5415
  const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
5172
5416
  const deadlineMs = timeoutMs + 1e4;
5173
5417
  const js = await compileMount(task, bundleDir);
5174
5418
  if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
5175
- const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
5419
+ const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
5176
5420
  const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
5177
5421
  const browser = await chromium6.connect(server.wsEndpoint());
5178
5422
  const results = [];
5179
5423
  try {
5180
5424
  for (const cfg of configs) {
5181
- const shoot = async (props, realHover) => {
5425
+ const box = (() => {
5426
+ try {
5427
+ return repMeta(task.set, cfg.rep);
5428
+ } catch {
5429
+ return void 0;
5430
+ }
5431
+ })();
5432
+ const rootCss = box === void 0 ? "#root{position:static;display:inline-block}" : `#root{width:${box.w}px;height:${box.h}px;position:absolute;left:20px;top:20px}
5433
+ #root > *{min-width:${box.w}px;min-height:${box.h}px}`;
5434
+ const viewport = box === void 0 ? { width: 900, height: 700 } : { width: box.w + 48, height: box.h + 48 };
5435
+ const shoot = async (component, props, realHover) => {
5182
5436
  const html = `<!doctype html><html><head><meta charset="utf-8"><style>
5183
5437
  ${getFontFaces3()}
5184
5438
  ${css}
5185
5439
  body{margin:0;padding:20px}
5186
- #root{position:static;display:inline-block}
5187
- </style></head><body><div id="root"></div><script>window.__cfg=${JSON.stringify({ component: cfg.component, props })}</script><script>${js}</script></body></html>`;
5188
- const page = await browser.newPage({ viewport: { width: 900, height: 700 } });
5440
+ ${rootCss}
5441
+ </style></head><body><div id="root"></div><script>window.__cfg=${JSON.stringify({ component, props })}</script><script>${js}</script></body></html>`;
5442
+ const page = await browser.newPage({ viewport });
5189
5443
  try {
5190
5444
  page.setDefaultTimeout(timeoutMs);
5191
5445
  await page.route("**/*", (route) => route.request().url().startsWith("data:") ? route.continue() : route.abort());
@@ -5199,15 +5453,30 @@ body{margin:0;padding:20px}
5199
5453
  } else {
5200
5454
  await page.waitForTimeout(400);
5201
5455
  }
5202
- return await page.screenshot({ clip: { x: 0, y: 0, width: 900, height: 700 } });
5456
+ return await page.screenshot({ clip: { x: 0, y: 0, width: viewport.width, height: viewport.height } });
5203
5457
  } finally {
5204
5458
  await page.close();
5205
5459
  }
5206
5460
  };
5207
5461
  const work = (async () => {
5208
- const forcedShot = await shoot(cfg.props, false);
5462
+ const adapter = task.configs.find((c) => c.rep === cfg.rep);
5463
+ if (adapter === void 0) {
5464
+ return { id: `parity:${cfg.rep}`, pass: false, detail: "the recording proves this hover pose and the bundle's adapter does not map it \u2014 an unmapped config is a FAIL, not an exemption from the check" };
5465
+ }
5466
+ const forcedShot = await shoot(adapter.component, cfg.props, false);
5209
5467
  if (!Buffer.isBuffer(forcedShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `forced mount: ${forcedShot.error}` };
5210
- const realShot = await shoot(withoutHoverToken(cfg.props), true);
5468
+ if (JSON.stringify(adapter.props) !== JSON.stringify(cfg.props)) {
5469
+ const adapterShot = await shoot(adapter.component, adapter.props, false);
5470
+ if (!Buffer.isBuffer(adapterShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `adapter mount: ${adapterShot.error}` };
5471
+ if (Buffer.compare(adapterShot, forcedShot) !== 0) {
5472
+ return {
5473
+ id: `parity:${cfg.rep}`,
5474
+ pass: false,
5475
+ detail: `the adapter drives this hover pose through props that do not render what the prescribed API renders (adapter ${JSON.stringify(adapter.props)} vs authored ${JSON.stringify(cfg.props)}) \u2014 the forcing contract is part of the certified surface; align the adapter with the authored API`
5476
+ };
5477
+ }
5478
+ }
5479
+ const realShot = await shoot(adapter.component, withoutHoverToken(cfg.props), true);
5211
5480
  if (!Buffer.isBuffer(realShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `real-hover mount: ${realShot.error}` };
5212
5481
  if (Buffer.compare(forcedShot, realShot) !== 0) {
5213
5482
  return {
@@ -5247,13 +5516,14 @@ var init_parity = __esm({
5247
5516
  init_behavior();
5248
5517
  init_font_faces();
5249
5518
  init_mount_limits();
5519
+ init_bundle_score();
5250
5520
  }
5251
5521
  });
5252
5522
 
5253
5523
  // packages/verify/src/composition.ts
5254
5524
  import { createRequire as createRequire2 } from "node:module";
5255
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
5256
- import path18 from "node:path";
5525
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
5526
+ import path19 from "node:path";
5257
5527
  import { build as build6 } from "esbuild";
5258
5528
  import { chromium as chromium7 } from "playwright-core";
5259
5529
  function getFontFaces4() {
@@ -5261,9 +5531,9 @@ function getFontFaces4() {
5261
5531
  return _fontFaces4;
5262
5532
  }
5263
5533
  async function compileInstrumentedMount(task, bundleDir) {
5264
- const entryTsx = path18.join(bundleDir, task.entry);
5265
- if (!existsSync13(entryTsx)) return { error: `${task.entry} missing` };
5266
- const requireFromVerify = createRequire2(path18.join(VERIFY_PKG_DIR, "package.json"));
5534
+ const entryTsx = path19.join(bundleDir, task.entry);
5535
+ if (!existsSync14(entryTsx)) return { error: `${task.entry} missing` };
5536
+ const requireFromVerify = createRequire2(path19.join(VERIFY_PKG_DIR, "package.json"));
5267
5537
  let realJsxPath;
5268
5538
  try {
5269
5539
  realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
@@ -5274,7 +5544,7 @@ async function compileInstrumentedMount(task, bundleDir) {
5274
5544
  import { createElement } from "react";
5275
5545
  import { createRoot } from "react-dom/client";
5276
5546
  import { __registerParts } from "react/jsx-runtime";
5277
- import * as B from ${JSON.stringify(path18.resolve(entryTsx))};
5547
+ import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
5278
5548
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
5279
5549
  const pairs: Array<[unknown, string]> = [];
5280
5550
  for (const name of cfg.partComponents) {
@@ -5329,7 +5599,7 @@ function expectedParts(task, roles, mainSlug) {
5329
5599
  }
5330
5600
  function interiorRegions(setDir, roles) {
5331
5601
  const mains = roles.main;
5332
- const withInterior = mains.filter((m) => existsSync13(path18.join(setDir, m, "get_metadata_interior.json")));
5602
+ const withInterior = mains.filter((m) => existsSync14(path19.join(setDir, m, "get_metadata_interior.json")));
5333
5603
  if (withInterior.length === 0) {
5334
5604
  return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
5335
5605
  }
@@ -5346,7 +5616,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
5346
5616
  if (typeof js !== "string") {
5347
5617
  return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
5348
5618
  }
5349
- const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
5619
+ const css = ["tokens.css", "styles.css"].map((f) => path19.join(bundleDir, f)).filter((f) => existsSync14(f)).map((f) => readFileSync11(f, "utf8")).join("\n");
5350
5620
  const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
5351
5621
  const browser = await chromium7.connect(server.wsEndpoint());
5352
5622
  const PAD = 4;
@@ -5451,7 +5721,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
5451
5721
  const deadlineMs = timeoutMs + 1e4;
5452
5722
  const js = await compileInstrumentedMount(task, bundleDir);
5453
5723
  if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
5454
- const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
5724
+ const css = ["tokens.css", "styles.css"].map((f) => path19.join(bundleDir, f)).filter((f) => existsSync14(f)).map((f) => readFileSync11(f, "utf8")).join("\n");
5455
5725
  const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
5456
5726
  const browser = await chromium7.connect(server.wsEndpoint());
5457
5727
  try {
@@ -5541,17 +5811,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
5541
5811
  });
5542
5812
 
5543
5813
  // packages/verify/src/occlusion.ts
5544
- import { existsSync as existsSync14 } from "node:fs";
5545
- import path19 from "node:path";
5814
+ import { existsSync as existsSync15 } from "node:fs";
5815
+ import path20 from "node:path";
5546
5816
  import { build as build7 } from "esbuild";
5547
5817
  import { chromium as chromium8 } from "playwright-core";
5548
5818
  async function compileTwoUp(task, bundleDir) {
5549
- const entryTsx = path19.join(bundleDir, task.entry);
5550
- if (!existsSync14(entryTsx)) return { error: `${task.entry} missing` };
5819
+ const entryTsx = path20.join(bundleDir, task.entry);
5820
+ if (!existsSync15(entryTsx)) return { error: `${task.entry} missing` };
5551
5821
  const src = `
5552
5822
  import { createElement } from "react";
5553
5823
  import { createRoot } from "react-dom/client";
5554
- import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
5824
+ import * as B from ${JSON.stringify(path20.resolve(entryTsx))};
5555
5825
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
5556
5826
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
5557
5827
  for (const id of ["first", "second"]) {
@@ -5720,6 +5990,7 @@ var init_src4 = __esm({
5720
5990
  init_prelude();
5721
5991
  init_mount_limits();
5722
5992
  init_font_collection();
5993
+ init_font_discovery();
5723
5994
  init_font_faces();
5724
5995
  init_font_resolve();
5725
5996
  init_paths();
@@ -5730,23 +6001,23 @@ var init_src4 = __esm({
5730
6001
  });
5731
6002
 
5732
6003
  // packages/cli/src/environment.ts
5733
- import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
5734
- import path20 from "node:path";
6004
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
6005
+ import path21 from "node:path";
5735
6006
  import { createHash as createHash3 } from "node:crypto";
5736
6007
  import { fileURLToPath as fileURLToPath5 } from "node:url";
5737
6008
  function cliVersion() {
5738
6009
  try {
5739
- return JSON.parse(readFileSync11(path20.join(path20.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
6010
+ return JSON.parse(readFileSync12(path21.join(path21.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
5740
6011
  } catch {
5741
6012
  return "dev";
5742
6013
  }
5743
6014
  }
5744
6015
  function environmentStamp(taskFamilies) {
5745
- const manifestPath2 = path20.join(fontCacheDir(), "manifest.json");
6016
+ const manifestPath2 = path21.join(fontCacheDir(), "manifest.json");
5746
6017
  let fontsHash = null;
5747
- if (existsSync15(manifestPath2)) {
6018
+ if (existsSync16(manifestPath2)) {
5748
6019
  try {
5749
- const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
6020
+ const entries = JSON.parse(readFileSync12(manifestPath2, "utf8"));
5750
6021
  const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
5751
6022
  const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
5752
6023
  fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
@@ -5790,8 +6061,8 @@ var init_describe = __esm({
5790
6061
  });
5791
6062
 
5792
6063
  // packages/cli/src/env.ts
5793
- import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
5794
- import path21 from "node:path";
6064
+ import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
6065
+ import path22 from "node:path";
5795
6066
  function parseEnv(content) {
5796
6067
  const entries = /* @__PURE__ */ new Map();
5797
6068
  for (const line of content.split("\n")) {
@@ -5803,9 +6074,9 @@ function parseEnv(content) {
5803
6074
  function resolveCredential(name) {
5804
6075
  const fromProcess = process.env[name];
5805
6076
  if (fromProcess) return fromProcess;
5806
- const envPath = path21.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
5807
- if (!existsSync16(envPath)) return void 0;
5808
- return parseEnv(readFileSync12(envPath, "utf8")).get(name);
6077
+ const envPath = path22.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
6078
+ if (!existsSync17(envPath)) return void 0;
6079
+ return parseEnv(readFileSync13(envPath, "utf8")).get(name);
5809
6080
  }
5810
6081
  var init_env = __esm({
5811
6082
  "packages/cli/src/env.ts"() {
@@ -5865,17 +6136,17 @@ var init_output = __esm({
5865
6136
  });
5866
6137
 
5867
6138
  // packages/cli/src/entitlement.ts
5868
- import { chmodSync, existsSync as existsSync17, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
6139
+ import { chmodSync, existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
5869
6140
  import crypto from "node:crypto";
5870
- import os3 from "node:os";
5871
- import path22 from "node:path";
6141
+ import os4 from "node:os";
6142
+ import path23 from "node:path";
5872
6143
  function entitlementPath() {
5873
- return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path22.join(os3.homedir(), ".tendril", "entitlement.json");
6144
+ return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path23.join(os4.homedir(), ".tendril", "entitlement.json");
5874
6145
  }
5875
6146
  function readStoredEntitlement(file = entitlementPath()) {
5876
- if (!existsSync17(file)) return void 0;
6147
+ if (!existsSync18(file)) return void 0;
5877
6148
  try {
5878
- const parsed = JSON.parse(readFileSync13(file, "utf8"));
6149
+ const parsed = JSON.parse(readFileSync14(file, "utf8"));
5879
6150
  if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
5880
6151
  return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
5881
6152
  } catch {
@@ -5883,7 +6154,7 @@ function readStoredEntitlement(file = entitlementPath()) {
5883
6154
  }
5884
6155
  }
5885
6156
  function writeStoredEntitlement(stored, file = entitlementPath()) {
5886
- mkdirSync4(path22.dirname(file), { recursive: true });
6157
+ mkdirSync4(path23.dirname(file), { recursive: true });
5887
6158
  writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
5888
6159
  `);
5889
6160
  chmodSync(file, 384);
@@ -5968,9 +6239,9 @@ var init_entitlement = __esm({
5968
6239
 
5969
6240
  // packages/cli/src/commands/doctor.ts
5970
6241
  import { spawnSync } from "node:child_process";
5971
- import { existsSync as existsSync18, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
5972
- import os4 from "node:os";
5973
- import path23 from "node:path";
6242
+ import { existsSync as existsSync19, readFileSync as readFileSync15, readdirSync as readdirSync4 } from "node:fs";
6243
+ import os5 from "node:os";
6244
+ import path24 from "node:path";
5974
6245
  function withDeadline(work, ms) {
5975
6246
  return Promise.race([
5976
6247
  work,
@@ -6030,19 +6301,19 @@ async function runDoctorChecks(options) {
6030
6301
  remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
6031
6302
  });
6032
6303
  }
6033
- const fontManifest = path23.join(fontCacheDir(), "manifest.json");
6304
+ const fontManifest = path24.join(fontCacheDir(), "manifest.json");
6034
6305
  checks.push(
6035
- existsSync18(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
6306
+ existsSync19(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync15(fontManifest, "utf8")).length} faces)` } : {
6036
6307
  name: "font-cache",
6037
6308
  ok: true,
6038
6309
  detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
6039
6310
  remediation: `Nothing to do now: \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` fetches exactly what a recording declares, and generate/verify name that command \u2014 with the set filled in \u2014 when they need it.`
6040
6311
  }
6041
6312
  );
6042
- const pluginRoot = path23.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
6043
- if (existsSync18(pluginRoot)) {
6313
+ const pluginRoot = path24.join(os5.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
6314
+ if (existsSync19(pluginRoot)) {
6044
6315
  try {
6045
- const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
6316
+ const versions = readdirSync4(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
6046
6317
  const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
6047
6318
  if (newest !== void 0) {
6048
6319
  const skewed = versionIsNewer(newest, cliVersion());
@@ -7566,9 +7837,9 @@ __export(record_exports, {
7566
7837
  runRecordPlan: () => runRecordPlan,
7567
7838
  runRecordStatus: () => runRecordStatus
7568
7839
  });
7569
- import { existsSync as existsSync20, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
7570
- import os5 from "node:os";
7571
- import path26 from "node:path";
7840
+ import { existsSync as existsSync21, mkdtempSync as mkdtempSync2, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "node:fs";
7841
+ import os6 from "node:os";
7842
+ import path27 from "node:path";
7572
7843
  import { writeFileSync as writeFileSync9 } from "node:fs";
7573
7844
  function recordsInteractionState(reports) {
7574
7845
  const evident = (s) => stateTokens(s).some((t) => INTERACTION_EVIDENCE_VALUES.has(t));
@@ -7591,7 +7862,7 @@ function interactionDisclosure(component, reports) {
7591
7862
  };
7592
7863
  }
7593
7864
  function symbolsFromMetadataEnvelope(file, sourceFrame) {
7594
- const env = JSON.parse(readFileSync16(file, "utf8"));
7865
+ const env = JSON.parse(readFileSync17(file, "utf8"));
7595
7866
  const text = env.content.map((c) => c.text ?? "").join("\n");
7596
7867
  const symbols = [];
7597
7868
  const walk2 = (node, ancestor) => {
@@ -7649,7 +7920,7 @@ function runRecordPlan(opts) {
7649
7920
  if (rawFile !== void 0) {
7650
7921
  try {
7651
7922
  const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
7652
- const tmp = path26.join(mkdtempSync2(path26.join(os5.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
7923
+ const tmp = path27.join(mkdtempSync2(path27.join(os6.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
7653
7924
  writeFileSync9(tmp, JSON.stringify(envelope));
7654
7925
  metadataEntries.push({ file: tmp });
7655
7926
  } catch (err) {
@@ -7671,7 +7942,7 @@ function runRecordPlan(opts) {
7671
7942
  let metadataTruncated = false;
7672
7943
  for (const { file, frame } of metadataEntries) {
7673
7944
  try {
7674
- const parsed = symbolsFromMetadataEnvelope(path26.resolve(file), frame);
7945
+ const parsed = symbolsFromMetadataEnvelope(path27.resolve(file), frame);
7675
7946
  symbols.push(...parsed.symbols);
7676
7947
  if (parsed.truncated) metadataTruncated = true;
7677
7948
  } catch (err) {
@@ -7705,7 +7976,7 @@ function runRecordPlan(opts) {
7705
7976
  if (symbols.length === 0) {
7706
7977
  const leads = metadataEntries.flatMap(({ file }) => {
7707
7978
  try {
7708
- const env = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
7979
+ const env = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
7709
7980
  return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
7710
7981
  } catch {
7711
7982
  return [];
@@ -7779,7 +8050,7 @@ function runRecordPlan(opts) {
7779
8050
  // a CLI flag and the MCP surface has no parameter for it.
7780
8051
  decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
7781
8052
  text: "Record a reduced pose set (anchor + one-factor sweeps + conflict crosses) instead of the full variant matrix. This buys calls with coverage \u2014 sampling is blind to multi-axis interactions and the report discloses the poses it skipped \u2014 so only the user may accept that trade. There is no tool parameter for it; the user runs it themselves in their own terminal, before any pose is recorded.",
7782
- userRuns: [`rm ${quoteArg(path26.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
8053
+ userRuns: [`rm ${quoteArg(path27.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
7783
8054
  },
7784
8055
  {
7785
8056
  id: "larger-allowance",
@@ -7906,10 +8177,11 @@ function nextPayload(setDir) {
7906
8177
  const instruction = nextInstruction(setDir);
7907
8178
  const status = sessionStatus(setDir);
7908
8179
  const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
7909
- if (instruction === null && !existsSync20(path26.join(setDir, "get_variable_defs.json"))) {
8180
+ if (instruction === null && !status.setTokenMapRecorded) {
7910
8181
  const manifest = loadManifest(setDir);
7911
8182
  const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
7912
- return { slug: "__set__", nodeId: frameNode, tool: "get_variable_defs", note: `SET-LEVEL: call get_variable_defs on the component frame and ingest with --rep __set__. ${ENVELOPE_HELP}`, progress };
8183
+ const invalidNote = status.setTokenMapInvalid !== void 0 ? ` (the existing set-level file is UNUSABLE \u2014 ${status.setTokenMapInvalid} \u2014 re-record it)` : "";
8184
+ return { slug: "__set__", nodeId: frameNode, tool: "get_variable_defs", note: `SET-LEVEL: call get_variable_defs on the component frame and ingest with --rep __set__.${invalidNote} ${ENVELOPE_HELP}`, progress };
7913
8185
  }
7914
8186
  return instruction === null ? { complete: true, progress } : {
7915
8187
  ...instruction,
@@ -7929,13 +8201,13 @@ function runRecordNext(opts) {
7929
8201
  const payload = nextPayload(opts.setDir);
7930
8202
  emitData(opts, payload, () => {
7931
8203
  if (payload["complete"] === true) {
7932
- process.stdout.write("set complete \u2014 every planned rep has its required envelopes\n");
8204
+ process.stdout.write("set complete \u2014 every planned rep has its required envelopes, all usable, and the set-level token map is recorded\n");
7933
8205
  return;
7934
8206
  }
7935
8207
  const progress = payload["progress"];
7936
8208
  process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
7937
8209
  \u2192 ${payload["note"]}
7938
- \u2192 then: ${tendrilCommand(`record ingest --set ${path26.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
8210
+ \u2192 then: ${tendrilCommand(`record ingest --set ${path27.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
7939
8211
  `);
7940
8212
  });
7941
8213
  }
@@ -8009,7 +8281,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
8009
8281
  const skipped = [];
8010
8282
  const failed = [];
8011
8283
  for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
8012
- if (existsSync20(path26.join(setDir, rep, name))) {
8284
+ if (existsSync21(path27.join(setDir, rep, name))) {
8013
8285
  skipped.push(name);
8014
8286
  continue;
8015
8287
  }
@@ -8031,16 +8303,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
8031
8303
  }
8032
8304
  function rawEnvelopeFromFile(file, parts) {
8033
8305
  if (parts) {
8034
- const blocks = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
8306
+ const blocks = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
8035
8307
  if (!Array.isArray(blocks) || blocks.length === 0 || blocks.some((p) => typeof p !== "string")) throw new Error("parts file must be a non-empty JSON array of strings");
8036
8308
  return { content: blocks.map((text) => ({ type: "text", text })) };
8037
8309
  }
8038
- return { content: [{ type: "text", text: readFileSync16(path26.resolve(file), "utf8") }] };
8310
+ return { content: [{ type: "text", text: readFileSync17(path27.resolve(file), "utf8") }] };
8039
8311
  }
8040
8312
  async function runRecordIngest(opts) {
8041
8313
  let payload;
8042
8314
  try {
8043
- payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(path26.resolve(opts.file), "utf8"));
8315
+ payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync17(path27.resolve(opts.file), "utf8"));
8044
8316
  } catch (err) {
8045
8317
  fail(opts, ExitCode.InputValidation, {
8046
8318
  error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
@@ -8052,7 +8324,7 @@ async function runRecordIngest(opts) {
8052
8324
  fail(opts, ExitCode.InputValidation, {
8053
8325
  error: "--raw is for text tool responses; screenshots are binary",
8054
8326
  code: "envelope-invalid",
8055
- remediation: `Use \`${tendrilCommand(`record fetch --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
8327
+ remediation: `Use \`${tendrilCommand(`record fetch --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
8056
8328
  });
8057
8329
  }
8058
8330
  if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
@@ -8072,7 +8344,7 @@ async function runRecordIngest(opts) {
8072
8344
  remediation: REINGEST_GUIDANCE
8073
8345
  });
8074
8346
  }
8075
- writeFileSync9(path26.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
8347
+ writeFileSync9(path27.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
8076
8348
  `);
8077
8349
  emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
8078
8350
  process.stdout.write("set-level get_variable_defs ingested\n");
@@ -8088,7 +8360,7 @@ async function runRecordIngest(opts) {
8088
8360
  if (assets !== void 0) {
8089
8361
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
8090
8362
  `);
8091
- for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
8363
+ for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
8092
8364
  `);
8093
8365
  }
8094
8366
  });
@@ -8161,15 +8433,15 @@ async function runRecordIngestRep(opts) {
8161
8433
  if (assets !== void 0) {
8162
8434
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
8163
8435
  `);
8164
- for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
8436
+ for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
8165
8437
  `);
8166
8438
  }
8167
8439
  });
8168
8440
  }
8169
8441
  function runRecordAsset(opts) {
8170
8442
  if (opts.dir !== void 0) {
8171
- const dir = path26.resolve(opts.dir);
8172
- const names = readdirSync5(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
8443
+ const dir = path27.resolve(opts.dir);
8444
+ const names = readdirSync6(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
8173
8445
  if (names.length === 0) {
8174
8446
  fail(opts, ExitCode.InputValidation, {
8175
8447
  error: `no asset-*.<ext> files found in ${dir}`,
@@ -8180,7 +8452,7 @@ function runRecordAsset(opts) {
8180
8452
  const ingested = [];
8181
8453
  try {
8182
8454
  for (const name of names) {
8183
- ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path26.join(dir, name)));
8455
+ ingestAsset(opts.setDir, opts.rep, name, readFileSync17(path27.join(dir, name)));
8184
8456
  ingested.push(name);
8185
8457
  }
8186
8458
  } catch (err) {
@@ -8200,11 +8472,11 @@ function runRecordAsset(opts) {
8200
8472
  fail(opts, ExitCode.InputValidation, {
8201
8473
  error: "pass --name and --file for a single asset, or --dir for a batch",
8202
8474
  code: "asset-rejected",
8203
- remediation: tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
8475
+ remediation: tendrilCommand(`record asset --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
8204
8476
  });
8205
8477
  }
8206
8478
  try {
8207
- ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(path26.resolve(opts.file)));
8479
+ ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync17(path27.resolve(opts.file)));
8208
8480
  emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
8209
8481
  process.stdout.write(`ingested ${opts.rep}/${opts.name}
8210
8482
  `);
@@ -8223,10 +8495,21 @@ function runRecordStatus(opts) {
8223
8495
  for (const r of status.reps) {
8224
8496
  process.stdout.write(`${r.missing.length === 0 ? "DONE " : "TODO "} ${r.slug.padEnd(24)} recorded=[${r.recorded.join(",")}]${r.missing.length > 0 ? ` missing=[${r.missing.join(",")}]` : ""}
8225
8497
  `);
8226
- }
8227
- process.stdout.write(status.complete ? "\nset complete\n" : `
8228
- ${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
8498
+ for (const inv of r.invalid ?? []) {
8499
+ process.stdout.write(` ${r.slug}/${inv.tool}: recorded file is UNUSABLE \u2014 ${inv.reason} (re-record this call)
8229
8500
  `);
8501
+ }
8502
+ }
8503
+ const pending = status.reps.filter((r) => r.missing.length > 0).length;
8504
+ process.stdout.write(
8505
+ status.complete ? "\nset complete\n" : pending > 0 ? `
8506
+ ${pending} rep(s) pending
8507
+ ` : status.setTokenMapInvalid !== void 0 ? `
8508
+ all reps recorded, but the SET-LEVEL token map file is UNUSABLE (${status.setTokenMapInvalid}) \u2014 re-record it via \`record next\`
8509
+ ` : `
8510
+ all reps recorded, but the SET-LEVEL token map is missing \u2014 run \`record next\` for the get_variable_defs step (without it briefs ship an empty token table)
8511
+ `
8512
+ );
8230
8513
  });
8231
8514
  }
8232
8515
  function narrowedRoles(derived, override) {
@@ -8240,7 +8523,7 @@ function narrowedRoles(derived, override) {
8240
8523
  function rolesFromFile(opts, file, derived) {
8241
8524
  let json;
8242
8525
  try {
8243
- json = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
8526
+ json = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
8244
8527
  } catch (err) {
8245
8528
  fail(opts, ExitCode.InputValidation, {
8246
8529
  error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -8278,11 +8561,11 @@ function rolesFromFile(opts, file, derived) {
8278
8561
  };
8279
8562
  }
8280
8563
  function runRecordFinish(opts) {
8281
- if (!existsSync20(path26.join(opts.setDir, "recording-set.json"))) {
8564
+ if (!existsSync21(path27.join(opts.setDir, "recording-set.json"))) {
8282
8565
  fail(opts, ExitCode.InputValidation, {
8283
8566
  error: `no recording-set.json in ${opts.setDir}`,
8284
8567
  code: "no-recording-set",
8285
- remediation: `Run \`${tendrilCommand(`record plan --set ${path26.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
8568
+ remediation: `Run \`${tendrilCommand(`record plan --set ${path27.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
8286
8569
  });
8287
8570
  }
8288
8571
  const { manifest, raw } = readManifestFile(opts.setDir);
@@ -8310,17 +8593,17 @@ function runRecordFinish(opts) {
8310
8593
  fail(opts, ExitCode.ConfirmationRequired, {
8311
8594
  error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
8312
8595
  code: "roles-confirmation-not-interactive",
8313
- remediation: `A human runs \`${tendrilCommand(`record finish --set ${path26.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
8596
+ remediation: `A human runs \`${tendrilCommand(`record finish --set ${path27.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
8314
8597
  });
8315
8598
  }
8316
8599
  const merged = { ...raw, roles };
8317
- const { issues } = validateRecordingSet(merged, (rel) => existsSync20(path26.join(opts.setDir, rel)));
8600
+ const { issues } = validateRecordingSet(merged, (rel) => existsSync21(path27.join(opts.setDir, rel)));
8318
8601
  const errors = issues.filter((i) => i.severity === "error");
8319
8602
  if (errors.length > 0) {
8320
8603
  fail(opts, ExitCode.InputValidation, {
8321
8604
  error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
8322
8605
  code: "recording-set-invalid",
8323
- remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path26.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
8606
+ remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path27.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
8324
8607
  });
8325
8608
  }
8326
8609
  for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
@@ -8554,8 +8837,8 @@ var init_engine_curated = __esm({
8554
8837
  });
8555
8838
 
8556
8839
  // packages/generate/src/loop.ts
8557
- import { existsSync as existsSync21, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
8558
- import path27 from "node:path";
8840
+ import { existsSync as existsSync22, mkdirSync as mkdirSync6, readFileSync as readFileSync18, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
8841
+ import path28 from "node:path";
8559
8842
  import { z as z11 } from "zod";
8560
8843
  function objective(scores, behaviors) {
8561
8844
  const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
@@ -8592,9 +8875,9 @@ ${absentLines.join("\n")}` : ""}
8592
8875
  Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Reading the pair: ink credits a recorded pixel when ANY render ink lands within 1px of it, so ink=1 proves coverage \u2014 not presence, shape, or colour. A wrong-shaped mark over the right region, or a wrong-coloured one that is still ink, scores 1.000; and a recorded mark within ~30 channel-sum of the backdrop (#f6f6f6 on white is 27) is not ink at all, so it can be absent at ink 1.000 with no MISSING line. With ink=1 AND low sim, coverage held while pixels differ, so start with the TWO causes below \u2014 but never read ink=1 as "nothing is missing": confirm every faint or small recorded mark (hairlines, dividers, low-contrast controls) exists in your render. GEOMETRY: wrong position/size/radius; the diff shows shifted edges and bands. WRONG TEXT WEIGHT OR FACE: the diff shows a uniform haze over glyph runs; CSS binds the font FAMILY before the weight, a later family in the stack is never consulted for a weight the first one lacks, and font-synthesis: none rules out faux-bold \u2014 so a stack led by a family the kit holds at one weight renders every other weight in that face, silently. Check the font stack against the kit's cached faces before concluding geometry is at fault. LOW ink with high sim means recorded marks are absent or painted invisibly. Do not regress PASS configs. ${mode === "files" ? "Update the files in your candidate directory and re-run the score." : "Reply with the complete corrected files in the same FILE format."}`;
8593
8876
  }
8594
8877
  function archivePriorRun(outDir) {
8595
- if (!existsSync21(path27.join(outDir, "run-log.json")) && !existsSync21(path27.join(outDir, "loop-state.json"))) return void 0;
8878
+ if (!existsSync22(path28.join(outDir, "run-log.json")) && !existsSync22(path28.join(outDir, "loop-state.json"))) return void 0;
8596
8879
  let n = 1;
8597
- while (existsSync21(`${outDir}-prev-${n}`)) n += 1;
8880
+ while (existsSync22(`${outDir}-prev-${n}`)) n += 1;
8598
8881
  renameSync(outDir, `${outDir}-prev-${n}`);
8599
8882
  return `${outDir}-prev-${n}`;
8600
8883
  }
@@ -8603,14 +8886,14 @@ async function runEngineLoop(opts) {
8603
8886
  const plateau = opts.plateau ?? 2;
8604
8887
  const progress = opts.onProgress ?? (() => {
8605
8888
  });
8606
- const statePath = path27.join(opts.outDir, "loop-state.json");
8607
- const resuming = opts.resume === true && existsSync21(statePath);
8889
+ const statePath = path28.join(opts.outDir, "loop-state.json");
8890
+ const resuming = opts.resume === true && existsSync22(statePath);
8608
8891
  if (!resuming) {
8609
8892
  const archived = archivePriorRun(opts.outDir);
8610
8893
  if (archived !== void 0) progress(`previous run archived to ${archived}`);
8611
8894
  }
8612
8895
  mkdirSync6(opts.outDir, { recursive: true });
8613
- const scratch = path27.join(opts.outDir, ".candidate");
8896
+ const scratch = path28.join(opts.outDir, ".candidate");
8614
8897
  let attempts = [];
8615
8898
  let log = [];
8616
8899
  let best;
@@ -8618,7 +8901,7 @@ async function runEngineLoop(opts) {
8618
8901
  let nonAccepted = 0;
8619
8902
  let stopReason = "max-iterations";
8620
8903
  if (resuming) {
8621
- const restored = LoopStateSchema.parse(JSON.parse(readFileSync17(statePath, "utf8")));
8904
+ const restored = LoopStateSchema.parse(JSON.parse(readFileSync18(statePath, "utf8")));
8622
8905
  attempts = restored.attempts;
8623
8906
  log = restored.iterations;
8624
8907
  spentUsd = restored.spentUsd;
@@ -8638,7 +8921,7 @@ async function runEngineLoop(opts) {
8638
8921
  };
8639
8922
  const writeCandidate = (files) => {
8640
8923
  mkdirSync6(scratch, { recursive: true });
8641
- for (const [name, content] of Object.entries(files)) writeFileSync10(path27.join(scratch, name), content);
8924
+ for (const [name, content] of Object.entries(files)) writeFileSync10(path28.join(scratch, name), content);
8642
8925
  };
8643
8926
  const scoreCandidate = async (candidate, iter, usd, modelMs) => {
8644
8927
  writeCandidate(candidate.files);
@@ -8696,8 +8979,8 @@ async function runEngineLoop(opts) {
8696
8979
  const usd = candidate.usage?.usd ?? 0;
8697
8980
  spentUsd += usd;
8698
8981
  if (candidate.raw !== void 0) {
8699
- mkdirSync6(path27.join(opts.outDir, "responses"), { recursive: true });
8700
- writeFileSync10(path27.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
8982
+ mkdirSync6(path28.join(opts.outDir, "responses"), { recursive: true });
8983
+ writeFileSync10(path28.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
8701
8984
  }
8702
8985
  if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
8703
8986
  const finish = candidate.usage?.finishReason ?? "?";
@@ -8723,10 +9006,10 @@ async function runEngineLoop(opts) {
8723
9006
  }
8724
9007
  }
8725
9008
  }
8726
- if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path27.join(opts.outDir, name), content);
9009
+ if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path28.join(opts.outDir, name), content);
8727
9010
  const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
8728
9011
  writeFileSync10(
8729
- path27.join(opts.outDir, "run-log.json"),
9012
+ path28.join(opts.outDir, "run-log.json"),
8730
9013
  `${JSON.stringify(
8731
9014
  {
8732
9015
  ...opts.meta,
@@ -8793,8 +9076,8 @@ var init_loop2 = __esm({
8793
9076
  });
8794
9077
 
8795
9078
  // packages/generate/src/brief.ts
8796
- import { existsSync as existsSync22, readFileSync as readFileSync18 } from "node:fs";
8797
- import path28 from "node:path";
9079
+ import { existsSync as existsSync23, readFileSync as readFileSync19 } from "node:fs";
9080
+ import path29 from "node:path";
8798
9081
  function singleAxes2(name) {
8799
9082
  const parsed = parseVariantAxes(name);
8800
9083
  if (parsed === void 0) return void 0;
@@ -9035,12 +9318,15 @@ function authorBehaviors(api, extras = {}) {
9035
9318
  return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
9036
9319
  }
9037
9320
  function envelopeText(file) {
9038
- return envelopeFirstTextPart(JSON.parse(readFileSync18(file, "utf8")));
9321
+ return envelopeFirstTextPart(JSON.parse(readFileSync19(file, "utf8")));
9322
+ }
9323
+ function metadataText(file) {
9324
+ return envelopeTextContent(JSON.parse(readFileSync19(file, "utf8")));
9039
9325
  }
9040
9326
  function dismissEvidence(setDir, repSlugs) {
9041
9327
  for (const slug of repSlugs) {
9042
- const f = path28.join(setDir, slug, "get_design_context.json");
9043
- if (!existsSync22(f)) continue;
9328
+ const f = path29.join(setDir, slug, "get_design_context.json");
9329
+ if (!existsSync23(f)) continue;
9044
9330
  const text = envelopeText(f);
9045
9331
  const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*(?:true|false)\b/i.exec(text) ?? /\b(\w*dismiss\w*)\??\s*:\s*boolean\b/i.exec(text);
9046
9332
  if (propHit !== null) return `emission prop "${propHit[1]}"`;
@@ -9084,13 +9370,13 @@ function recordedFontNeeds(setDir, opts = {}) {
9084
9370
  }
9085
9371
  };
9086
9372
  const manifest = loadManifest(setDir);
9087
- const setDefs = path28.join(setDir, "get_variable_defs.json");
9088
- if (existsSync22(setDefs)) fromDefs(envelopeText(setDefs));
9373
+ const setDefs = path29.join(setDir, "get_variable_defs.json");
9374
+ if (existsSync23(setDefs)) fromDefs(envelopeText(setDefs));
9089
9375
  for (const rep of manifest.reps) {
9090
- const ctx = path28.join(setDir, rep.slug, "get_design_context.json");
9091
- if (existsSync22(ctx)) fromEmission(envelopeText(ctx));
9092
- const defs = path28.join(setDir, rep.slug, "get_variable_defs.json");
9093
- if (existsSync22(defs)) fromDefs(envelopeText(defs));
9376
+ const ctx = path29.join(setDir, rep.slug, "get_design_context.json");
9377
+ if (existsSync23(ctx)) fromEmission(envelopeText(ctx));
9378
+ const defs = path29.join(setDir, rep.slug, "get_variable_defs.json");
9379
+ if (existsSync23(defs)) fromDefs(envelopeText(defs));
9094
9380
  }
9095
9381
  return [...byFamily.entries()].map(([family, paired]) => {
9096
9382
  const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
@@ -9101,10 +9387,10 @@ function symbolFontGlyphCount(setDir, reps) {
9101
9387
  const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
9102
9388
  const glyphs = /* @__PURE__ */ new Set();
9103
9389
  for (const rep of reps) {
9104
- const file = path28.join(setDir, rep, "get_metadata.json");
9105
- if (!existsSync22(file)) continue;
9390
+ const file = path29.join(setDir, rep, "get_metadata.json");
9391
+ if (!existsSync23(file)) continue;
9106
9392
  try {
9107
- const text = JSON.parse(readFileSync18(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
9393
+ const text = JSON.parse(readFileSync19(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
9108
9394
  for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
9109
9395
  const name = decodeXmlEntities(m[1]).replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
9110
9396
  if (PUA.test(name)) glyphs.add(name);
@@ -9130,8 +9416,8 @@ function recordedTextSlots(setDir, repSlugs) {
9130
9416
  const propRep = [];
9131
9417
  const perRep = [];
9132
9418
  for (const slug of repSlugs) {
9133
- const f = path28.join(setDir, slug, "get_design_context.json");
9134
- if (!existsSync22(f)) continue;
9419
+ const f = path29.join(setDir, slug, "get_design_context.json");
9420
+ if (!existsSync23(f)) continue;
9135
9421
  const code = envelopeText(f);
9136
9422
  const props = /* @__PURE__ */ new Map();
9137
9423
  for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
@@ -9156,9 +9442,9 @@ function recordedTextSlots(setDir, repSlugs) {
9156
9442
  }
9157
9443
  const axisValuesBySlug = /* @__PURE__ */ new Map();
9158
9444
  for (const slug of repSlugs) {
9159
- const metaFile = path28.join(setDir, slug, "get_metadata.json");
9160
- if (!existsSync22(metaFile)) continue;
9161
- const name = symbolName(envelopeText(metaFile));
9445
+ const metaFile = path29.join(setDir, slug, "get_metadata.json");
9446
+ if (!existsSync23(metaFile)) continue;
9447
+ const name = symbolName(metadataText(metaFile));
9162
9448
  if (name === void 0) continue;
9163
9449
  const values = /* @__PURE__ */ new Set();
9164
9450
  for (const part of name.split(",")) {
@@ -9256,12 +9542,12 @@ function authorTaskFromSet(setDir, opts = {}) {
9256
9542
  const poses = [];
9257
9543
  const missing = [];
9258
9544
  for (const rep of manifest.reps) {
9259
- const metaFile = path28.join(setDir, rep.slug, "get_metadata.json");
9260
- if (!existsSync22(metaFile)) {
9545
+ const metaFile = path29.join(setDir, rep.slug, "get_metadata.json");
9546
+ if (!existsSync23(metaFile)) {
9261
9547
  missing.push(rep.slug);
9262
9548
  continue;
9263
9549
  }
9264
- const name = symbolName(envelopeText(metaFile));
9550
+ const name = symbolName(metadataText(metaFile));
9265
9551
  if (name === void 0) {
9266
9552
  missing.push(rep.slug);
9267
9553
  continue;
@@ -9271,8 +9557,8 @@ function authorTaskFromSet(setDir, opts = {}) {
9271
9557
  if (missing.length > 0) {
9272
9558
  throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
9273
9559
  }
9274
- const setMeta = path28.join(setDir, "get_metadata.json");
9275
- const latticeNames = manifest.latticeNames ?? (existsSync22(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
9560
+ const setMeta = path29.join(setDir, "get_metadata.json");
9561
+ const latticeNames = manifest.latticeNames ?? (existsSync23(setMeta) ? [...metadataText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
9276
9562
  const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
9277
9563
  const recordedFonts = recordedFontFamilies(setDir);
9278
9564
  const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
@@ -9455,10 +9741,10 @@ var init_brief = __esm({
9455
9741
  });
9456
9742
 
9457
9743
  // packages/generate/src/segments.ts
9458
- import { existsSync as existsSync23, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
9459
- import path29 from "node:path";
9744
+ import { existsSync as existsSync24, readFileSync as readFileSync20, readdirSync as readdirSync7 } from "node:fs";
9745
+ import path30 from "node:path";
9460
9746
  function repText(set, rep, tool) {
9461
- const env = JSON.parse(readFileSync19(path29.join(set, rep, `${tool}.json`), "utf8"));
9747
+ const env = JSON.parse(readFileSync20(path30.join(set, rep, `${tool}.json`), "utf8"));
9462
9748
  return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
9463
9749
  }
9464
9750
  function stripFigmaInstructions(emission) {
@@ -9518,18 +9804,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
9518
9804
  }
9519
9805
  function buildSegments(task, mode = "fenced") {
9520
9806
  const SET = task.set;
9807
+ let defsRecorded = existsSync24(path30.join(SET, "get_variable_defs.json"));
9521
9808
  let rawDefs = {};
9522
- if (existsSync23(path29.join(SET, "get_variable_defs.json"))) {
9523
- const text = envelopeFirstTextPart(JSON.parse(readFileSync19(path29.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
9809
+ if (existsSync24(path30.join(SET, "get_variable_defs.json"))) {
9810
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync20(path30.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
9524
9811
  try {
9525
9812
  rawDefs = JSON.parse(text);
9526
9813
  } catch {
9527
9814
  }
9528
9815
  } else {
9529
9816
  for (const cfg of task.configs) {
9530
- const f = path29.join(SET, cfg.rep, "get_variable_defs.json");
9531
- if (!existsSync23(f)) continue;
9532
- const text = envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) || "{}";
9817
+ const f = path30.join(SET, cfg.rep, "get_variable_defs.json");
9818
+ if (!existsSync24(f)) continue;
9819
+ defsRecorded = true;
9820
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync20(f, "utf8"))) || "{}";
9533
9821
  try {
9534
9822
  for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
9535
9823
  } catch {
@@ -9537,25 +9825,27 @@ function buildSegments(task, mode = "fenced") {
9537
9825
  }
9538
9826
  }
9539
9827
  const emissionTexts = task.configs.map((cfg) => {
9540
- const f = path29.join(SET, cfg.rep, "get_design_context.json");
9541
- return existsSync23(f) ? envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) : "";
9828
+ const f = path30.join(SET, cfg.rep, "get_design_context.json");
9829
+ return existsSync24(f) ? envelopeFirstTextPart(JSON.parse(readFileSync20(f, "utf8"))) : "";
9542
9830
  });
9543
9831
  const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
9544
9832
  const defs = JSON.stringify(map, null, 1);
9545
9833
  const parts = [
9546
9834
  "Implement the component family below. For each recorded config: the symbol box, Figma's emission (intent \u2014 write your own clean implementation; emissions are VERBATIM Figma output and may contain non-compiling artifacts like duplicate consts \u2014 never copy them as code), and its SVG assets (inline these; network is denied at render).",
9547
- `
9835
+ defsRecorded ? `
9548
9836
  ## Design tokens (use these CSS custom property names)
9549
9837
  \`\`\`json
9550
9838
  ${defs}
9551
- \`\`\`${note}`
9839
+ \`\`\`${note}` : `
9840
+ ## Design tokens
9841
+ TOKEN MAP NEVER RECORDED: this set holds no get_variable_defs envelope, so whether the kit exposes tokens is UNKNOWN \u2014 not answered. Literal values are your only option here and are correct to use; do NOT invent token names. (The set can record the map later: \`record next\` asks for get_variable_defs once, at set level.)${note}`
9552
9842
  ];
9553
9843
  for (const cfg of task.configs) {
9554
9844
  const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
9555
9845
  const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
9556
- const assets = readdirSync6(path29.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
9846
+ const assets = readdirSync7(path30.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
9557
9847
  \`\`\`svg
9558
- ${readFileSync19(path29.join(SET, cfg.rep, f), "utf8")}
9848
+ ${readFileSync20(path30.join(SET, cfg.rep, f), "utf8")}
9559
9849
  \`\`\``).join("\n");
9560
9850
  parts.push(`
9561
9851
  ## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
@@ -9580,7 +9870,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
9580
9870
  } else {
9581
9871
  parts.push(`
9582
9872
  ## Output format
9583
- Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path29.basename(task.set)}-candidate/\` unless you were handed another path. Pass that same directory to \`tendril engine score\` every round and keep writing into it \u2014 the scorer stamps the bundle there, writes its evidence beside your files, and appends its score-history.jsonl lines there (one when a round starts, one when it scores); a fresh directory each round throws all of that away. Do not paste file contents into chat \u2014 the scorer reads the directory.`);
9873
+ Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path30.basename(task.set)}-candidate/\` unless you were handed another path. Pass that same directory to \`tendril engine score\` every round and keep writing into it \u2014 the scorer stamps the bundle there, writes its evidence beside your files, and appends its score-history.jsonl lines there (one when a round starts, one when it scores); a fresh directory each round throws all of that away. Do not paste file contents into chat \u2014 the scorer reads the directory.`);
9584
9874
  }
9585
9875
  return parts.join("\n");
9586
9876
  }
@@ -9648,8 +9938,8 @@ var init_adapter = __esm({
9648
9938
 
9649
9939
  // packages/generate/src/bundle-emit.ts
9650
9940
  import { createHash as createHash4 } from "node:crypto";
9651
- import { copyFileSync, existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync20, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
9652
- import path30 from "node:path";
9941
+ import { copyFileSync, existsSync as existsSync25, mkdirSync as mkdirSync7, readFileSync as readFileSync21, readdirSync as readdirSync8, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
9942
+ import path31 from "node:path";
9653
9943
  function pinFromConfigs(configs) {
9654
9944
  const domains = /* @__PURE__ */ new Map();
9655
9945
  const kinds = /* @__PURE__ */ new Map();
@@ -9718,15 +10008,23 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9718
10008
  const notices = [];
9719
10009
  const licenseTexts = /* @__PURE__ */ new Map();
9720
10010
  for (const face of faces) {
9721
- const src = path30.join(cacheDir, path30.basename(face.file));
9722
- const target = `./fonts/${path30.basename(face.file)}`;
9723
- const format = FONT_FORMATS[path30.extname(face.file).toLowerCase()] ?? "truetype";
10011
+ const src = path31.join(cacheDir, path31.basename(face.file));
10012
+ const target = `./fonts/${path31.basename(face.file)}`;
10013
+ const format = FONT_FORMATS[path31.extname(face.file).toLowerCase()] ?? "truetype";
9724
10014
  const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
9725
10015
  const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
9726
10016
  const license = normalizeFontLicense(face.license);
9727
- const terms = REDISTRIBUTABLE.get(license);
10017
+ const terms = face.source.startsWith("system:") || face.source.startsWith("local:") ? void 0 : REDISTRIBUTABLE.get(license);
9728
10018
  if (terms === void 0) {
9729
- if (face.source.startsWith("local:")) {
10019
+ if (face.source.startsWith("system:")) {
10020
+ lines.push(
10021
+ `/* '${family}' ${face.weight} came from this machine's installed system fonts (tendril fonts`,
10022
+ ` add-system; sha256 ${face.sha256.slice(0, 16)}\u2026). OS-bundled faces are never copied into`,
10023
+ " bundles regardless of any recorded licence \u2014 the consuming machine provides its own copy,",
10024
+ ` or you place one you licence at ${target} and uncomment: */`,
10025
+ `/* ${decl} */`
10026
+ );
10027
+ } else if (face.source.startsWith("local:")) {
9730
10028
  lines.push(
9731
10029
  `/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
9732
10030
  ` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
@@ -9750,14 +10048,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9750
10048
  `/* ${decl} */`
9751
10049
  );
9752
10050
  }
9753
- } else if (existsSync24(src) && createHash4("sha256").update(readFileSync20(src)).digest("hex") === face.sha256) {
9754
- mkdirSync7(path30.join(bundleDir, "fonts"), { recursive: true });
9755
- copyFileSync(src, path30.join(bundleDir, "fonts", path30.basename(face.file)));
10051
+ } else if (existsSync25(src) && createHash4("sha256").update(readFileSync21(src)).digest("hex") === face.sha256) {
10052
+ mkdirSync7(path31.join(bundleDir, "fonts"), { recursive: true });
10053
+ copyFileSync(src, path31.join(bundleDir, "fonts", path31.basename(face.file)));
9756
10054
  licenseTexts.set(terms.file, terms.text);
9757
10055
  const upstream = upstreamAttribution(face);
9758
10056
  notices.push(
9759
10057
  "",
9760
- `${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path30.basename(face.file)}`,
10058
+ `${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path31.basename(face.file)}`,
9761
10059
  ` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
9762
10060
  ` source: ${face.source}`,
9763
10061
  ` sha256: ${face.sha256}`,
@@ -9771,9 +10069,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9771
10069
  }
9772
10070
  if (lines.length === 0) return null;
9773
10071
  if (notices.length > 0) {
9774
- const fontsDir = path30.join(bundleDir, "fonts");
9775
- for (const [file, text] of licenseTexts) writeFileSync11(path30.join(fontsDir, file), text);
9776
- writeFileSync11(path30.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
10072
+ const fontsDir = path31.join(bundleDir, "fonts");
10073
+ for (const [file, text] of licenseTexts) writeFileSync11(path31.join(fontsDir, file), text);
10074
+ writeFileSync11(path31.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
9777
10075
  `);
9778
10076
  header.push(
9779
10077
  "/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
@@ -9785,10 +10083,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9785
10083
  `;
9786
10084
  }
9787
10085
  function countLatticeSymbols(setDir) {
9788
- const manifestFile = path30.join(setDir, "recording-set.json");
9789
- if (existsSync24(manifestFile)) {
10086
+ const manifestFile = path31.join(setDir, "recording-set.json");
10087
+ if (existsSync25(manifestFile)) {
9790
10088
  try {
9791
- const stored = JSON.parse(readFileSync20(manifestFile, "utf8"));
10089
+ const stored = JSON.parse(readFileSync21(manifestFile, "utf8"));
9792
10090
  if (stored.variantScope !== "component-set") return null;
9793
10091
  const lattice = stored.latticeNames;
9794
10092
  if (lattice !== void 0 && lattice.length > 0) return lattice.length;
@@ -9796,13 +10094,13 @@ function countLatticeSymbols(setDir) {
9796
10094
  }
9797
10095
  }
9798
10096
  const files = [
9799
- path30.join(setDir, "get_metadata.json"),
9800
- ...existsSync24(setDir) ? readdirSync7(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path30.join(setDir, f)) : []
9801
- ].filter((f) => existsSync24(f));
10097
+ path31.join(setDir, "get_metadata.json"),
10098
+ ...existsSync25(setDir) ? readdirSync8(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path31.join(setDir, f)) : []
10099
+ ].filter((f) => existsSync25(f));
9802
10100
  if (files.length === 0) return null;
9803
10101
  let count = 0;
9804
10102
  for (const f of files) {
9805
- const text = envelopeTextContent(JSON.parse(readFileSync20(f, "utf8")));
10103
+ const text = envelopeTextContent(JSON.parse(readFileSync21(f, "utf8")));
9806
10104
  count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
9807
10105
  }
9808
10106
  return count > 0 ? count : null;
@@ -9810,21 +10108,21 @@ function countLatticeSymbols(setDir) {
9810
10108
  function recordingSetHash(setDir, configs) {
9811
10109
  const relPaths = [];
9812
10110
  for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
9813
- if (existsSync24(path30.join(setDir, name))) relPaths.push(name);
10111
+ if (existsSync25(path31.join(setDir, name))) relPaths.push(name);
9814
10112
  }
9815
10113
  for (const cfg of configs) {
9816
10114
  for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
9817
- if (existsSync24(path30.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
10115
+ if (existsSync25(path31.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
9818
10116
  }
9819
- if (existsSync24(path30.join(setDir, cfg.rep))) {
9820
- for (const asset of readdirSync7(path30.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
10117
+ if (existsSync25(path31.join(setDir, cfg.rep))) {
10118
+ for (const asset of readdirSync8(path31.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
9821
10119
  relPaths.push(`${cfg.rep}/${asset}`);
9822
10120
  }
9823
10121
  }
9824
10122
  }
9825
10123
  return hashRecordingSet(
9826
10124
  relPaths,
9827
- (p) => new Uint8Array(readFileSync20(path30.join(setDir, p))),
10125
+ (p) => new Uint8Array(readFileSync21(path31.join(setDir, p))),
9828
10126
  (chunks) => {
9829
10127
  const h = createHash4("sha256");
9830
10128
  for (const c of chunks) h.update(c);
@@ -9845,10 +10143,11 @@ function emitBundleV1(opts) {
9845
10143
  const certified = statuses.filter((s) => s.status === "certified").length;
9846
10144
  const pass = statuses.filter((s) => s.status !== "fail").length;
9847
10145
  const lattice = countLatticeSymbols(opts.task.set);
9848
- const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
10146
+ const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
9849
10147
  const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
9850
- const cssFiles = ["styles.css", "tokens.css"].map((f) => path30.join(opts.bundleDir, f)).filter((f) => existsSync24(f));
9851
- const families = cssFontFamilies(cssFiles.map((f) => readFileSync20(f, "utf8")).join("\n"));
10148
+ const parity = opts.behaviors.filter((b) => b.id.startsWith("parity:"));
10149
+ const cssFiles = ["styles.css", "tokens.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f));
10150
+ const families = cssFontFamilies(cssFiles.map((f) => readFileSync21(f, "utf8")).join("\n"));
9852
10151
  const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
9853
10152
  family: f.family,
9854
10153
  weight: f.weight,
@@ -9877,7 +10176,7 @@ function emitBundleV1(opts) {
9877
10176
  // resolvable via verify's --set override).
9878
10177
  path: (() => {
9879
10178
  const base = process.env["INIT_CWD"] ?? process.cwd();
9880
- const rel = path30.relative(base, opts.task.set);
10179
+ const rel = path31.relative(base, opts.task.set);
9881
10180
  return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
9882
10181
  })(),
9883
10182
  component: opts.componentName,
@@ -9901,21 +10200,21 @@ function emitBundleV1(opts) {
9901
10200
  })
9902
10201
  };
9903
10202
  const written = [];
9904
- const manifestPath2 = path30.join(opts.bundleDir, "component.json");
10203
+ const manifestPath2 = path31.join(opts.bundleDir, "component.json");
9905
10204
  writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
9906
10205
  `);
9907
10206
  written.push(manifestPath2);
9908
- const stylesPath = path30.join(opts.bundleDir, "styles.css");
9909
- if (existsSync24(stylesPath)) {
10207
+ const stylesPath = path31.join(opts.bundleDir, "styles.css");
10208
+ if (existsSync25(stylesPath)) {
9910
10209
  const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
9911
- const current = readFileSync20(stylesPath, "utf8");
10210
+ const current = readFileSync21(stylesPath, "utf8");
9912
10211
  const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
9913
10212
  writeFileSync11(stylesPath, `${comment}
9914
10213
  ${stripped}`);
9915
10214
  written.push(stylesPath);
9916
10215
  }
9917
- const fontsCssPath = path30.join(opts.bundleDir, "fonts.css");
9918
- rmSync3(path30.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
10216
+ const fontsCssPath = path31.join(opts.bundleDir, "fonts.css");
10217
+ rmSync3(path31.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
9919
10218
  rmSync3(fontsCssPath, { force: true });
9920
10219
  const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
9921
10220
  if (fontsCss !== null) {
@@ -9923,7 +10222,7 @@ ${stripped}`);
9923
10222
  written.push(fontsCssPath);
9924
10223
  }
9925
10224
  const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
9926
- const statusLine = `bundle: ${pass}/${statuses.length} recorded configs \u2265 pass bar (${certified} certified), ` + (interaction.length === 0 ? "interaction behaviors NONE VERIFIED" : `interaction behaviors ${interaction.filter((b) => b.pass).length}/${interaction.length}`) + (prelude.length === 0 ? "" : `, page hygiene ${prelude.filter((b) => b.pass).length}/${prelude.length}`) + (unrecorded === null ? "" : `; ${unrecorded} lattice configs UNVERIFIED`) + ` \u2014 claims in component.json, recompute with \`tendril verify\``;
10225
+ const statusLine = `bundle: ${pass}/${statuses.length} recorded configs \u2265 pass bar (${certified} certified), ` + (interaction.length === 0 ? "interaction behaviors NONE VERIFIED" : `interaction behaviors ${interaction.filter((b) => b.pass).length}/${interaction.length}`) + (parity.length === 0 ? "" : `, state parity ${parity.filter((b) => b.pass).length}/${parity.length}`) + (prelude.length === 0 ? "" : `, page hygiene ${prelude.filter((b) => b.pass).length}/${prelude.length}`) + (unrecorded === null ? "" : `; ${unrecorded} lattice configs UNVERIFIED`) + ` \u2014 claims in component.json, recompute with \`tendril verify\``;
9927
10226
  return { manifest, statusLine, written };
9928
10227
  }
9929
10228
  var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
@@ -10360,9 +10659,9 @@ var init_src7 = __esm({
10360
10659
  });
10361
10660
 
10362
10661
  // packages/cli/src/font-guidance.ts
10363
- import path31 from "node:path";
10662
+ import path32 from "node:path";
10364
10663
  function fontsUnprovenRemediation(setDir) {
10365
- const set = setDir === void 0 ? void 0 : path31.resolve(setDir);
10664
+ const set = setDir === void 0 ? void 0 : path32.resolve(setDir);
10366
10665
  if (set !== void 0) {
10367
10666
  try {
10368
10667
  const needs = recordedFontNeeds(set);
@@ -10430,20 +10729,28 @@ __export(fonts_exports, {
10430
10729
  DEFAULT_FONT_CACHE: () => DEFAULT_FONT_CACHE,
10431
10730
  familyMismatch: () => familyMismatch,
10432
10731
  runFontsAdd: () => runFontsAdd,
10732
+ runFontsAddSystem: () => runFontsAddSystem,
10733
+ runFontsDiscover: () => runFontsDiscover,
10433
10734
  runFontsRequired: () => runFontsRequired,
10434
10735
  runFontsResolve: () => runFontsResolve,
10435
10736
  runFontsResolveSet: () => runFontsResolveSet,
10436
10737
  runFontsStatus: () => runFontsStatus
10437
10738
  });
10438
- import { existsSync as existsSync25, readFileSync as readFileSync21 } from "node:fs";
10439
- import path32 from "node:path";
10739
+ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
10740
+ import path33 from "node:path";
10440
10741
  async function runFontsResolve(opts) {
10441
10742
  const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
10442
- emitData(opts, result, () => {
10743
+ const queryRefusal = systemFaceRefusal(opts.family.trim());
10744
+ emitData(opts, { ...result, ...queryRefusal !== void 0 ? { queryRefusal } : {} }, () => {
10443
10745
  for (const f of result.resolved) process.stdout.write(`resolved ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
10444
10746
  `);
10445
10747
  for (const f of result.failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
10446
10748
  `);
10749
+ if (queryRefusal !== void 0 && result.failures.length > 0) {
10750
+ process.stdout.write(` "${opts.family}" is a refusal-class family: ${queryRefusal}
10751
+ Options: proceed under a disclosed substitute (rules out certification), or register a file you license yourself: ${tendrilCommand(`fonts add ${quoteArg(opts.family)} <weight> <file>`)} \u2014 bundles declare the face and never carry its bytes.
10752
+ `);
10753
+ }
10447
10754
  });
10448
10755
  if (result.failures.length > 0) {
10449
10756
  warn(opts, `${result.failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
@@ -10451,7 +10758,7 @@ async function runFontsResolve(opts) {
10451
10758
  }
10452
10759
  }
10453
10760
  async function runFontsResolveSet(opts) {
10454
- const setDir = path32.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
10761
+ const setDir = path33.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
10455
10762
  let needs = [];
10456
10763
  try {
10457
10764
  needs = recordedFontNeeds(setDir);
@@ -10501,7 +10808,8 @@ async function runFontsResolveSet(opts) {
10501
10808
  unlicensed.push(...result.failures);
10502
10809
  }
10503
10810
  }
10504
- emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures }, () => {
10811
+ const refusals = [...new Set(failures.map((f) => f.family))].map((family) => ({ family, reason: systemFaceRefusal(family.trim()) })).filter((r) => r.reason !== void 0);
10812
+ emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures, ...refusals.length > 0 ? { refusals } : {} }, () => {
10505
10813
  process.stdout.write(`set declares: ${needs.map((n) => `${n.family} (${n.weights.join(", ")})`).join(" \xB7 ")}
10506
10814
  `);
10507
10815
  for (const f of cached2) process.stdout.write(`cached ${f.family} ${f.weight}
@@ -10514,6 +10822,15 @@ async function runFontsResolveSet(opts) {
10514
10822
  `);
10515
10823
  for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
10516
10824
  `);
10825
+ for (const fam of [...new Set(failures.map((f) => f.family))]) {
10826
+ const refusal = systemFaceRefusal(fam);
10827
+ process.stdout.write(
10828
+ refusal !== void 0 ? ` "${fam}" is a refusal-class family: ${refusal}
10829
+ Options: proceed and let the mount substitute a provided face (disclosed; rules out certification), or register a file you license yourself: ${tendrilCommand(`fonts add ${quoteArg(fam)} <weight> <file>`)} \u2014 licensing is yours to judge; bundles declare the face and never carry its bytes.
10830
+ ` : ` if "${fam}" is installed on this machine: ${tendrilCommand(`fonts discover ${quoteArg(fam)}`)} then ${tendrilCommand(`fonts add-system ${quoteArg(fam)} --set ${quoteArg(setDir)}`)} (licence stays unknown; bundles declare, never copy)
10831
+ `
10832
+ );
10833
+ }
10517
10834
  });
10518
10835
  if (byteDrift.length > 0) {
10519
10836
  warn(opts, `${byteDrift.map((f) => `${f.family} ${f.weight}`).join(", ")}: the foundry now serves different bytes than the cache held \u2014 the faces were re-fetched to record their licence, so re-pin any font lock deliberately`);
@@ -10524,16 +10841,16 @@ async function runFontsResolveSet(opts) {
10524
10841
  }
10525
10842
  }
10526
10843
  function runFontsStatus(opts) {
10527
- const manifestPath2 = path32.join(opts.cacheDir, "manifest.json");
10528
- if (!existsSync25(manifestPath2)) {
10844
+ const manifestPath2 = path33.join(opts.cacheDir, "manifest.json");
10845
+ if (!existsSync26(manifestPath2)) {
10529
10846
  fail(opts, ExitCode.FontsUnproven, {
10530
10847
  error: `no font cache at ${opts.cacheDir}`,
10531
10848
  code: "fonts-unresolved",
10532
10849
  remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
10533
10850
  });
10534
10851
  }
10535
- const faces = JSON.parse(readFileSync21(manifestPath2, "utf8"));
10536
- const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path32.resolve(opts.lock), opts.cacheDir) : null;
10852
+ const faces = JSON.parse(readFileSync22(manifestPath2, "utf8"));
10853
+ const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path33.resolve(opts.lock), opts.cacheDir) : null;
10537
10854
  emitData(opts, { faces, lockVerdicts }, () => {
10538
10855
  for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
10539
10856
  `);
@@ -10577,13 +10894,13 @@ function familyMismatch(family, declared) {
10577
10894
  }
10578
10895
  function runFontsAdd(opts) {
10579
10896
  if (opts.set !== void 0) {
10580
- const declared = taskFontFamilies(path32.resolve(opts.set)) ?? [];
10897
+ const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
10581
10898
  const mismatch = familyMismatch(opts.family, declared);
10582
10899
  if (mismatch !== void 0) {
10583
10900
  fail(opts, ExitCode.InputValidation, {
10584
10901
  error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. Adding it under this name would cache a face the mount never matches, and scoring would keep refusing for the family that is still missing.`,
10585
10902
  code: "font-family-not-declared",
10586
- remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${path32.resolve(opts.set)}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set to add a family this set does not declare.`
10903
+ remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${path33.resolve(opts.set)}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set to add a family this set does not declare.`
10587
10904
  });
10588
10905
  }
10589
10906
  } else {
@@ -10610,6 +10927,81 @@ function runFontsAdd(opts) {
10610
10927
  `);
10611
10928
  });
10612
10929
  }
10930
+ function runFontsDiscover(opts) {
10931
+ const faces = opts.family !== void 0 ? systemFacesForFamily(opts.family, opts.dirs) : discoverSystemFaces(opts.dirs);
10932
+ const rows = faces.map((f) => ({ ...f, ...systemFaceRefusal(f.family) !== void 0 ? { refused: systemFaceRefusal(f.family) } : {} }));
10933
+ const queryRefusal = opts.family !== void 0 ? systemFaceRefusal(opts.family) : void 0;
10934
+ emitData(opts, { faces: rows, dirs: opts.dirs ?? systemFontDirs(), ...queryRefusal !== void 0 ? { queryRefusal } : {} }, () => {
10935
+ if (rows.length === 0) {
10936
+ process.stdout.write(`no matching system faces found (scanned: ${(opts.dirs ?? systemFontDirs()).join(", ")})
10937
+ `);
10938
+ if (queryRefusal !== void 0) {
10939
+ process.stdout.write(
10940
+ `note: "${opts.family}" is a refusal-class family regardless \u2014 ${queryRefusal}
10941
+ A file you license yourself can still be registered for local scoring: ${tendrilCommand(`fonts add ${quoteArg(opts.family ?? "")} <weight> <file>`)} \u2014 licensing is yours to judge, and bundles declare the face but never carry its bytes.
10942
+ `
10943
+ );
10944
+ }
10945
+ return;
10946
+ }
10947
+ for (const f of rows) {
10948
+ const flags = [f.refused !== void 0 ? "REFUSED" : "", f.italic ? "italic" : "", f.variable ? "variable" : ""].filter((x) => x !== "").join(" ");
10949
+ process.stdout.write(`${f.family} \u2014 ${f.subfamily || "Regular"} (${f.weight})${flags === "" ? "" : ` [${flags}]`} ${f.file}${f.faceIndex !== void 0 ? ` #${f.faceIndex}` : ""}
10950
+ `);
10951
+ }
10952
+ const eligible = rows.filter((f) => f.refused === void 0 && !f.italic && !f.variable);
10953
+ if (eligible.length > 0 && opts.family !== void 0) {
10954
+ process.stdout.write(`
10955
+ cache them: ${tendrilCommand(`fonts add-system ${quoteArg(opts.family)}`)}
10956
+ `);
10957
+ }
10958
+ });
10959
+ }
10960
+ function runFontsAddSystem(opts) {
10961
+ if (opts.set !== void 0) {
10962
+ const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
10963
+ const mismatch = familyMismatch(opts.family, declared);
10964
+ if (mismatch !== void 0) {
10965
+ fail(opts, ExitCode.InputValidation, {
10966
+ error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. A face cached under a name the mount never matches leaves scoring refusing for the family that is still missing.`,
10967
+ code: "font-family-not-declared",
10968
+ remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(path33.resolve(opts.set))}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set.`
10969
+ });
10970
+ }
10971
+ } else {
10972
+ warn(
10973
+ opts,
10974
+ `family "${opts.family}" was NOT checked against a recording \u2014 pass --set <recording-dir> to have the spelling verified, since a face cached under a name the mount does not match leaves scoring refusing for a family that still looks provided`
10975
+ );
10976
+ }
10977
+ const result = addSystemFamily(opts.family, { cacheDir: opts.cacheDir, ...opts.weights !== void 0 ? { weights: opts.weights } : {} });
10978
+ if (result.refusal !== void 0) {
10979
+ fail(opts, ExitCode.InputValidation, {
10980
+ error: `"${opts.family}" is a refusal-class family: ${result.refusal}`,
10981
+ code: "font-family-refused",
10982
+ remediation: `Pick a face the design system can actually license for rendering, or resolve a served family: ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}.`
10983
+ });
10984
+ }
10985
+ if (result.added.length === 0) {
10986
+ fail(opts, ExitCode.InputValidation, {
10987
+ error: result.skipped.length > 0 ? `no cacheable face for "${opts.family}" \u2014 every match was skipped: ${result.skipped.map((s) => `${s.subfamily || "Regular"} (${s.reason})`).join("; ")}` : `no installed face matches "${opts.family}" \u2014 nothing in ${systemFontDirs().join(", ")} declares that family`,
10988
+ code: "font-family-not-installed",
10989
+ remediation: `See what IS installed: ${tendrilCommand(`fonts discover ${quoteArg(opts.family)}`)} (or all faces with ${tendrilCommand("fonts discover")}).`
10990
+ });
10991
+ }
10992
+ for (const o of result.overwrote) {
10993
+ warn(
10994
+ opts,
10995
+ `OVERWROTE ${o.family} ${o.weight}: the cache held a ${o.priorLicense} face from ${o.priorSource} (sha ${o.priorSha256.slice(0, 12)}\u2026) and it has been REPLACED by this machine's system bytes \u2014 the environment stamp changes, and any bundle emitted under the old face now fails checkFontLock against this cache. Restore the foundry face with ${tendrilCommand(`fonts resolve "${o.family}" --weights ${o.weight}`)} if that was not intended`
10996
+ );
10997
+ }
10998
+ emitData(opts, result, () => {
10999
+ for (const f of result.added) process.stdout.write(`added ${f.family} ${f.weight} \u2014 ${f.subfamily} \u2014 from ${f.source} (${f.sha256.slice(0, 12)}\u2026) \u2014 licence unknown: bundles DECLARE this face, its bytes never ship
11000
+ `);
11001
+ for (const s of result.skipped) process.stdout.write(`skipped ${s.subfamily || "Regular"} ${s.weight}: ${s.reason}
11002
+ `);
11003
+ });
11004
+ }
10613
11005
  var init_fonts = __esm({
10614
11006
  "packages/cli/src/commands/fonts.ts"() {
10615
11007
  "use strict";
@@ -10640,15 +11032,24 @@ __export(verify_exports, {
10640
11032
  resolveComposition: () => resolveComposition,
10641
11033
  runVerify: () => runVerify
10642
11034
  });
10643
- import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
10644
- import path33 from "node:path";
11035
+ import { existsSync as existsSync27, readFileSync as readFileSync23 } from "node:fs";
11036
+ import path34 from "node:path";
10645
11037
  function interactionCoverage(behaviors) {
10646
- const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
11038
+ const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
11039
+ const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
10647
11040
  return {
10648
11041
  interactionChecks: interaction.length,
10649
11042
  interactionPassed: interaction.filter((b) => b.pass).length,
10650
- preludeChecks: behaviors.length - interaction.length,
10651
- operability: interaction.length > 0 && interaction.every((b) => b.pass) ? "verified" : "unverified"
11043
+ preludeChecks: behaviors.length - interaction.length - parity.length,
11044
+ parityChecks: parity.length,
11045
+ parityPassed: parity.filter((b) => b.pass).length,
11046
+ // Asymmetric on purpose (review finding F3): a parity PASS never
11047
+ // makes operability "verified" (parity is pixel-parity, not
11048
+ // someone typing or clicking — the §0j miscount), but a parity
11049
+ // FAIL still forbids it — "verified" printed beside a broken hover
11050
+ // state is the one word doing too much lifting again, from the
11051
+ // other direction.
11052
+ operability: interaction.length > 0 && interaction.every((b) => b.pass) && parity.every((b) => b.pass) ? "verified" : "unverified"
10652
11053
  };
10653
11054
  }
10654
11055
  function operabilityReport(input) {
@@ -10681,6 +11082,16 @@ function operabilityReport(input) {
10681
11082
  unverified: `the interaction checks RAN and ${failed} of ${interactionChecks} failed (each named in the FAIL behavior rows) \u2014 this component WAS exercised and did not behave as the recording requires; unverified here means measured and wrong, not unmeasured.`
10682
11083
  };
10683
11084
  }
11085
+ const { parityChecks, parityPassed } = interactionCoverage(input.behaviors);
11086
+ if (parityPassed < parityChecks) {
11087
+ const failed = parityChecks - parityPassed;
11088
+ return {
11089
+ checks: interactionChecks,
11090
+ passed: interactionPassed,
11091
+ short: `${failed} of ${parityChecks} state-parity check(s) failed`,
11092
+ unverified: `the interaction checks passed but ${failed} of ${parityChecks} state-parity check(s) FAILED (the parity: rows) \u2014 a certified forced state is masking a broken real one, so operability cannot read as verified beside it.`
11093
+ };
11094
+ }
10684
11095
  return { checks: interactionChecks, passed: interactionPassed };
10685
11096
  }
10686
11097
  function operabilityLine(state) {
@@ -10773,7 +11184,7 @@ function compositionReport(input) {
10773
11184
  function eyeCheck(bundleDir) {
10774
11185
  return {
10775
11186
  command: tendrilCommand(`inspect "${bundleDir}"`),
10776
- sheetPath: path33.join(bundleDir, "verify-evidence", "inspect.html"),
11187
+ sheetPath: path34.join(bundleDir, "verify-evidence", "inspect.html"),
10777
11188
  note: "magnified recorded-vs-rendered crops of every small node; scores cannot see shape. The command WRITES/refreshes the sheet at sheetPath \u2014 re-run it after every verify; an existing sheet may show stale crops."
10778
11189
  };
10779
11190
  }
@@ -10785,7 +11196,7 @@ function taskFromManifest(opts, manifest, setDir) {
10785
11196
  const adapterSlugs = Object.keys(manifest.propAdapter);
10786
11197
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
10787
11198
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
10788
- const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
11199
+ const registry = Object.values(TASKS).find((t) => path34.resolve(t.set) === path34.resolve(setDir));
10789
11200
  const authored = (() => {
10790
11201
  if (registry !== void 0) return void 0;
10791
11202
  try {
@@ -10811,6 +11222,10 @@ function taskFromManifest(opts, manifest, setDir) {
10811
11222
  task,
10812
11223
  unmapped,
10813
11224
  adapterOnly,
11225
+ // The parity AUTHORITY: CLI-owned configs for the same poses —
11226
+ // recording-derived (authored) or registry-declared — never the
11227
+ // adapter, which is the graded artifact framing itself (§0j).
11228
+ authorityConfigs: behaviorSource.configs,
10814
11229
  // Registry sets carry hand-declared behaviors and no derivation
10815
11230
  // runs, so their evidence is UNKNOWN, not empty: an empty list is
10816
11231
  // spent downstream as "the recording holds no interactive pose".
@@ -10820,19 +11235,19 @@ function taskFromManifest(opts, manifest, setDir) {
10820
11235
  }
10821
11236
  async function runVerify(opts) {
10822
11237
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
10823
- const setOverride = opts.set !== void 0 ? path33.resolve(callerCwd, opts.set) : void 0;
10824
- opts = { ...opts, bundleDir: path33.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
10825
- if (!existsSync26(opts.bundleDir)) {
11238
+ const setOverride = opts.set !== void 0 ? path34.resolve(callerCwd, opts.set) : void 0;
11239
+ opts = { ...opts, bundleDir: path34.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
11240
+ if (!existsSync27(opts.bundleDir)) {
10826
11241
  fail(opts, ExitCode.InputValidation, {
10827
11242
  error: `bundle directory not found: ${opts.bundleDir}`,
10828
11243
  code: "bundle-missing",
10829
11244
  remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
10830
11245
  });
10831
11246
  }
10832
- const manifestPath2 = path33.join(opts.bundleDir, "component.json");
11247
+ const manifestPath2 = path34.join(opts.bundleDir, "component.json");
10833
11248
  let manifest;
10834
- if (existsSync26(manifestPath2)) {
10835
- const { manifest: parsed, issues } = readBundleManifest(readFileSync22(manifestPath2, "utf8"));
11249
+ if (existsSync27(manifestPath2)) {
11250
+ const { manifest: parsed, issues } = readBundleManifest(readFileSync23(manifestPath2, "utf8"));
10836
11251
  if (issues.length > 0) {
10837
11252
  fail(opts, ExitCode.InputValidation, {
10838
11253
  error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -10846,6 +11261,7 @@ async function runVerify(opts) {
10846
11261
  let unmapped = [];
10847
11262
  let interactionEvidence;
10848
11263
  let unmappedInteractionEvidence = [];
11264
+ let authorityConfigs;
10849
11265
  let availability = ROLES_NOT_RESOLVED;
10850
11266
  if (opts.task !== void 0) {
10851
11267
  const registry = TASKS[opts.task];
@@ -10859,21 +11275,21 @@ async function runVerify(opts) {
10859
11275
  task = registry;
10860
11276
  } else if (manifest !== void 0) {
10861
11277
  const resolveSetDir = (p) => {
10862
- if (path33.isAbsolute(p)) return p;
10863
- const fromRepo = path33.resolve(REPO_ROOT, p);
10864
- if (existsSync26(fromRepo)) return fromRepo;
10865
- return path33.resolve(callerCwd, p);
11278
+ if (path34.isAbsolute(p)) return p;
11279
+ const fromRepo = path34.resolve(REPO_ROOT, p);
11280
+ if (existsSync27(fromRepo)) return fromRepo;
11281
+ return path34.resolve(callerCwd, p);
10866
11282
  };
10867
11283
  const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
10868
- if (!existsSync26(path33.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path33.resolve(t.set) === path33.resolve(setDir))) {
11284
+ if (!existsSync27(path34.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path34.resolve(t.set) === path34.resolve(setDir))) {
10869
11285
  fail(opts, ExitCode.RecordingIncomplete, {
10870
11286
  error: `recording set not found or unmanifested: ${setDir}`,
10871
11287
  code: "recording-set-missing",
10872
11288
  remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
10873
11289
  });
10874
11290
  }
10875
- const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
10876
- if (registry !== void 0 && !existsSync26(path33.join(setDir, "recording-set.json"))) {
11291
+ const registry = Object.values(TASKS).find((t) => path34.resolve(t.set) === path34.resolve(setDir));
11292
+ if (registry !== void 0 && !existsSync27(path34.join(setDir, "recording-set.json"))) {
10877
11293
  const recordedSlugs = registry.configs.map((c) => c.rep);
10878
11294
  const adapterSlugs = Object.keys(manifest.propAdapter);
10879
11295
  unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
@@ -10882,10 +11298,12 @@ async function runVerify(opts) {
10882
11298
  entry: manifest.entry,
10883
11299
  configs: adapterSlugs.filter((s) => recordedSlugs.includes(s)).map((rep) => ({ rep, component: manifest.propAdapter[rep].component, props: manifest.propAdapter[rep].props }))
10884
11300
  };
11301
+ authorityConfigs = registry.configs;
10885
11302
  } else {
10886
11303
  const built = taskFromManifest(opts, manifest, setDir);
10887
11304
  task = built.task;
10888
11305
  unmapped = built.unmapped;
11306
+ authorityConfigs = built.authorityConfigs;
10889
11307
  interactionEvidence = built.interactionEvidence;
10890
11308
  unmappedInteractionEvidence = built.unmappedInteractionEvidence;
10891
11309
  for (const s of built.adapterOnly) warn(opts, `prop adapter maps "${s}" which is not in the recording set \u2014 ignored`);
@@ -10897,9 +11315,9 @@ async function runVerify(opts) {
10897
11315
  warn(opts, `recording set content differs from the bundle's provenance stamp (${hash.slice(0, 12)}\u2026 vs ${manifest.provenance.recordingSet.hash.slice(0, 12)}\u2026) \u2014 scores apply to the CURRENT set`);
10898
11316
  }
10899
11317
  for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
10900
- const p = path33.join(opts.bundleDir, name);
10901
- if (!existsSync26(p)) continue;
10902
- const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync22(p)));
11318
+ const p = path34.join(opts.bundleDir, name);
11319
+ if (!existsSync27(p)) continue;
11320
+ const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync23(p)));
10903
11321
  if (issues.length > 0) {
10904
11322
  fail(opts, ExitCode.InputValidation, {
10905
11323
  error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -10937,7 +11355,7 @@ async function runVerify(opts) {
10937
11355
  warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
10938
11356
  }
10939
11357
  const missing = task.configs.filter(
10940
- (c) => !existsSync26(path33.join(task.set, c.rep, "get_screenshot.json")) || !existsSync26(path33.join(task.set, c.rep, "get_metadata.json"))
11358
+ (c) => !existsSync27(path34.join(task.set, c.rep, "get_screenshot.json")) || !existsSync27(path34.join(task.set, c.rep, "get_metadata.json"))
10941
11359
  );
10942
11360
  if (missing.length > 0) {
10943
11361
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -10947,12 +11365,12 @@ async function runVerify(opts) {
10947
11365
  });
10948
11366
  }
10949
11367
  const bar = BARS2[opts.bar];
10950
- const evidenceDir = path33.join(opts.bundleDir, "verify-evidence");
11368
+ const evidenceDir = path34.join(opts.bundleDir, "verify-evidence");
10951
11369
  const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
10952
11370
  const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
10953
- const bundleCss = ["tokens.css", "styles.css"].map((f) => path33.join(opts.bundleDir, f)).filter((f) => existsSync26(f)).map((f) => readFileSync22(f, "utf8")).join("\n");
11371
+ const bundleCss = ["tokens.css", "styles.css"].map((f) => path34.join(opts.bundleDir, f)).filter((f) => existsSync27(f)).map((f) => readFileSync23(f, "utf8")).join("\n");
10954
11372
  const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
10955
- const parity = await checkHoverParity(task, opts.bundleDir);
11373
+ const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
10956
11374
  const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
10957
11375
  const structural = roles !== void 0 ? await checkStructuralComposition(task, opts.bundleDir, roles) : [];
10958
11376
  const regionsOut = roles !== void 0 ? interiorRegions(task.set, roles) : void 0;
@@ -11135,6 +11553,9 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
11135
11553
  `INCOMPLETE ${cov.unrecordedConfigs} of ${cov.latticeConfigs} lattice poses were NEVER RECORDED \u2014 nothing verifies them; any implementation of those poses is inference, not verified truth. Re-plan the set (full matrix is the default) and record the missing poses.
11136
11554
  `
11137
11555
  );
11556
+ } else if (typeof cov.latticeConfigs === "number") {
11557
+ process.stdout.write(`LATTICE all ${cov.latticeConfigs} poses of the component set's own lattice are recorded and scored \u2014 the denominator is the set's, not the recorded subset's
11558
+ `);
11138
11559
  } else if (cov.latticeConfigs === null) {
11139
11560
  process.stdout.write(`COVERAGE denominator unknown (set predates lattice tracking) \u2014 scored configs are verified; completeness is not established
11140
11561
  `);
@@ -11142,7 +11563,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
11142
11563
  }
11143
11564
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
11144
11565
  `);
11145
- const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path33.join(opts.bundleDir, f)).filter((f) => existsSync26(f)).map((f) => readFileSync22(f, "utf8")).join("\n")));
11566
+ const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path34.join(opts.bundleDir, f)).filter((f) => existsSync27(f)).map((f) => readFileSync23(f, "utf8")).join("\n")));
11146
11567
  if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
11147
11568
  process.stdout.write(`fonts: scored with Tendril-cache faces \u2014 a consuming app must provision the same families (the bundle ships fonts.css when faces are shippable; sha-pinned list in component.json requiredFonts)
11148
11569
  `);
@@ -11232,18 +11653,18 @@ __export(engine_exports, {
11232
11653
  runEngineBrief: () => runEngineBrief,
11233
11654
  runEngineScore: () => runEngineScore
11234
11655
  });
11235
- import { appendFileSync, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
11236
- import path34 from "node:path";
11656
+ import { appendFileSync, existsSync as existsSync28, mkdirSync as mkdirSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync12 } from "node:fs";
11657
+ import path35 from "node:path";
11237
11658
  function resolveEngineTask(opts, callerCwd) {
11238
- const asPath = path34.resolve(callerCwd, opts.taskOrSet);
11239
- const isSet = existsSync27(path34.join(asPath, "recording-set.json"));
11659
+ const asPath = path35.resolve(callerCwd, opts.taskOrSet);
11660
+ const isSet = existsSync28(path35.join(asPath, "recording-set.json"));
11240
11661
  const registry = TASKS[opts.taskOrSet];
11241
11662
  if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
11242
11663
  if (isSet) {
11243
11664
  try {
11244
11665
  const authored = authorTaskFromSet(asPath);
11245
11666
  for (const d of authored.disclosures) warn(opts, d);
11246
- return { task: authored.task, name: path34.basename(asPath), ref: asPath, disclosures: authored.disclosures, interactionEvidence: authored.api.interactionEvidence, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
11667
+ return { task: authored.task, name: path35.basename(asPath), ref: asPath, disclosures: authored.disclosures, interactionEvidence: authored.api.interactionEvidence, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
11247
11668
  } catch (err) {
11248
11669
  fail(opts, ExitCode.InputValidation, {
11249
11670
  error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -11270,9 +11691,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
11270
11691
  const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
11271
11692
  const segments = buildSegments(task, "files");
11272
11693
  let notRecorded;
11273
- const manifestPath2 = path34.join(task.set, "recording-set.json");
11274
- if (existsSync27(manifestPath2)) {
11275
- notRecorded = JSON.parse(readFileSync23(manifestPath2, "utf8")).notRecorded;
11694
+ const manifestPath2 = path35.join(task.set, "recording-set.json");
11695
+ if (existsSync28(manifestPath2)) {
11696
+ notRecorded = JSON.parse(readFileSync24(manifestPath2, "utf8")).notRecorded;
11276
11697
  }
11277
11698
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
11278
11699
 
@@ -11280,7 +11701,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
11280
11701
  DO NOT INVENT PIXELS FOR THESE POSES. Implement them ONLY as the composition of recorded per-axis truth (the custom-property composition rule: one paint axis sets variables, the other consumes) and list every such pose in your report as UNRECORDED-COMPOSED. Recording them is the real fix: re-plan the set (full matrix is the default) and record the missing poses.
11281
11702
  ${notRecorded}` : "";
11282
11703
  let fontProvisioning;
11283
- if (existsSync27(manifestPath2)) {
11704
+ if (existsSync28(manifestPath2)) {
11284
11705
  const missingFams = unprovisionedFamilies(task.set);
11285
11706
  const unprovided = unprovisionedFaces(task.set);
11286
11707
  const weightOnly = missingFams.length === 0;
@@ -11306,9 +11727,9 @@ ${notRecorded}` : "";
11306
11727
 
11307
11728
  === TASK PAYLOAD (recorded truth, verbatim) ===
11308
11729
  ${segments}`;
11309
- const payloadFile = path34.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
11310
- const candidateDirSuggestion = path34.resolve(callerCwd, `tendril-out/${name}-candidate`);
11311
- mkdirSync8(path34.dirname(payloadFile), { recursive: true });
11730
+ const payloadFile = path35.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
11731
+ const candidateDirSuggestion = path35.resolve(callerCwd, `tendril-out/${name}-candidate`);
11732
+ mkdirSync8(path35.dirname(payloadFile), { recursive: true });
11312
11733
  writeFileSync12(payloadFile, payload);
11313
11734
  emitData(
11314
11735
  opts,
@@ -11359,15 +11780,18 @@ ${segments}`;
11359
11780
  );
11360
11781
  }
11361
11782
  function appendScoreHistory(candidateDir, entry) {
11362
- appendFileSync(path34.join(candidateDir, "score-history.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
11783
+ const file = path35.join(candidateDir, "score-history.jsonl");
11784
+ const starts = existsSync28(file) ? readFileSync24(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
11785
+ const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
11786
+ appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
11363
11787
  `);
11364
11788
  }
11365
11789
  async function runEngineScore(opts) {
11366
11790
  requireEntitlement(opts);
11367
11791
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
11368
- const candidateDir = path34.resolve(callerCwd, opts.candidateDir);
11792
+ const candidateDir = path35.resolve(callerCwd, opts.candidateDir);
11369
11793
  const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
11370
- if (!existsSync27(candidateDir)) {
11794
+ if (!existsSync28(candidateDir)) {
11371
11795
  fail(opts, ExitCode.InputValidation, {
11372
11796
  error: `candidate directory not found: ${candidateDir}`,
11373
11797
  code: "candidate-missing",
@@ -11392,10 +11816,10 @@ async function runEngineScore(opts) {
11392
11816
  for (const g of missingWeights(task.set)) {
11393
11817
  warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
11394
11818
  }
11395
- if (opts.rebind !== true && existsSync27(path34.join(candidateDir, "component.json"))) {
11819
+ if (opts.rebind !== true && existsSync28(path35.join(candidateDir, "component.json"))) {
11396
11820
  const prior = (() => {
11397
11821
  try {
11398
- const read = readBundleManifest(readFileSync23(path34.join(candidateDir, "component.json"), "utf8"));
11822
+ const read = readBundleManifest(readFileSync24(path35.join(candidateDir, "component.json"), "utf8"));
11399
11823
  return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
11400
11824
  } catch {
11401
11825
  return { unreadable: true };
@@ -11417,9 +11841,9 @@ async function runEngineScore(opts) {
11417
11841
  }
11418
11842
  }
11419
11843
  const bar = BARS3[opts.bar];
11420
- const evidenceDir = path34.join(candidateDir, "verify-evidence");
11844
+ const evidenceDir = path35.join(candidateDir, "verify-evidence");
11421
11845
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
11422
- const parity = await checkHoverParity(task, candidateDir);
11846
+ const parity = await checkHoverParity(task, candidateDir, task.configs);
11423
11847
  const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
11424
11848
  const parityCoverage = parity.length > 0 ? `${parity.filter((p) => p.pass).length}/${parity.length} hover-forced configs` : "not applicable (no hover-forced configs in this set)";
11425
11849
  const obj = objective(scores, behaviors);
@@ -11447,7 +11871,10 @@ ${absentFindings.join("\n")}` : "";
11447
11871
 
11448
11872
  CERTIFICATION: ${certifiedReps.length}/${scores.length} configs at the certification bar (sim \u2265${certBar.sim} AND ink \u2265${certBar.ink}, exact values, after parity demotion \u2014 composition checks at verify can demote further).${certifiedReps.length < scores.length ? ` Below cert: ${scores.filter((sc) => !certifiedSet.has(sc.rep)).map((sc) => sc.rep).join(", ")}.` : ""}
11449
11873
  METRIC DEADBAND (read before iterating on near-misses): the scored similarity/ink deliberately tolerate \xB11px edge shift and antialiased-edge differences \u2014 cross-rasterizer noise absorption. A change entirely inside that band moves these numbers by EXACTLY ZERO (working as designed, not a stuck scorer). The per-config \`exact\` fields in the JSON are tolerance-free and move first: compare exact across rounds to confirm a small fix landed, and stop iterating when only exact moves \u2014 the bar reads the tolerant numbers.`;
11450
- const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + qualityFeedback;
11874
+ const stampNotice = `
11875
+
11876
+ PROVENANCE STAMP: this scoring call itself (the Tendril CLI) just wrote/refreshed a comment on line 1 of styles.css carrying these scores, marked non-authoritative. If your editor or host reports styles.css was modified externally, that modification is this scorer \u2014 expected, not tampering. Keep the comment; it self-invalidates on any edit and \`tendril verify\` recomputes it.`;
11877
+ const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + stampNotice + qualityFeedback;
11451
11878
  const emitted = emitBundleV1({
11452
11879
  bundleDir: candidateDir,
11453
11880
  task,
@@ -11460,7 +11887,17 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
11460
11887
  environment: environmentStamp(taskFontFamilies(task.set)),
11461
11888
  substitutedFamilies
11462
11889
  });
11463
- appendScoreHistory(candidateDir, { event: "round-scored", bar: opts.bar, pass: obj[0], total, certified: certifiedReps.length, floor: obj[1], mean: obj[2] });
11890
+ appendScoreHistory(candidateDir, {
11891
+ event: "round-scored",
11892
+ bar: opts.bar,
11893
+ pass: obj[0],
11894
+ total,
11895
+ pixelConfigsPassing: scores.filter((s) => s.pass).length,
11896
+ pixelConfigs: scores.length,
11897
+ certified: certifiedReps.length,
11898
+ floor: obj[1],
11899
+ mean: obj[2]
11900
+ });
11464
11901
  const coverage = interactionCoverage(behaviors);
11465
11902
  const operability = operabilityReport({ behaviors, interactionEvidence });
11466
11903
  const evidenceUnverified = (interactionEvidence?.length ?? 0) > 0 && coverage.interactionChecks === 0;
@@ -11502,7 +11939,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
11502
11939
  parityCoverage,
11503
11940
  evidenceDir,
11504
11941
  bundleManifest: emitted.written[0],
11505
- note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
11942
+ note: "the Tendril CLI (this scoring call) wrote the bundle provenance comment onto styles.css line 1 \u2014 an EXPECTED external modification, not tampering; preserve it in any post-score edit (it self-invalidates on edit and verify recomputes it)",
11506
11943
  allPass,
11507
11944
  environment: { ...environmentStamp(taskFontFamilies(task.set)), ruler: cliVersion() },
11508
11945
  ...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
@@ -11575,11 +12012,11 @@ var codeconnect_exports = {};
11575
12012
  __export(codeconnect_exports, {
11576
12013
  runCodeConnect: () => runCodeConnect
11577
12014
  });
11578
- import { existsSync as existsSync28, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
11579
- import path35 from "node:path";
12015
+ import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
12016
+ import path36 from "node:path";
11580
12017
  function runCodeConnect(opts) {
11581
12018
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
11582
- const bundleDir = path35.resolve(callerCwd, opts.bundleDir);
12019
+ const bundleDir = path36.resolve(callerCwd, opts.bundleDir);
11583
12020
  let url;
11584
12021
  try {
11585
12022
  url = new URL(opts.figmaUrl);
@@ -11595,7 +12032,7 @@ function runCodeConnect(opts) {
11595
12032
  }
11596
12033
  let manifest;
11597
12034
  try {
11598
- const read = readBundleManifest(readFileSync24(path35.join(bundleDir, "component.json"), "utf8"));
12035
+ const read = readBundleManifest(readFileSync25(path36.join(bundleDir, "component.json"), "utf8"));
11599
12036
  if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
11600
12037
  manifest = read.manifest;
11601
12038
  } catch (err) {
@@ -11605,8 +12042,8 @@ function runCodeConnect(opts) {
11605
12042
  remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
11606
12043
  });
11607
12044
  }
11608
- const setDir = path35.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
11609
- if (!existsSync28(path35.join(setDir, "recording-set.json"))) {
12045
+ const setDir = path36.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
12046
+ if (!existsSync29(path36.join(setDir, "recording-set.json"))) {
11610
12047
  fail(opts, ExitCode.InputValidation, {
11611
12048
  error: `recording set not found at ${setDir}`,
11612
12049
  code: "codeconnect-no-set",
@@ -11627,10 +12064,10 @@ function runCodeConnect(opts) {
11627
12064
  const component = api.component;
11628
12065
  const recManifest = loadManifest(setDir);
11629
12066
  const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
11630
- const meta = path35.join(setDir, r.slug, "get_metadata.json");
11631
- if (!existsSync28(meta)) return void 0;
12067
+ const meta = path36.join(setDir, r.slug, "get_metadata.json");
12068
+ if (!existsSync29(meta)) return void 0;
11632
12069
  try {
11633
- return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync24(meta, "utf8"))))?.[1];
12070
+ return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync25(meta, "utf8"))))?.[1];
11634
12071
  } catch {
11635
12072
  return void 0;
11636
12073
  }
@@ -11688,7 +12125,7 @@ function runCodeConnect(opts) {
11688
12125
  axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
11689
12126
  fragmentVars.push(varName);
11690
12127
  }
11691
- const entryRel = path35.relative(callerCwd, path35.join(bundleDir, manifest.entry));
12128
+ const entryRel = path36.relative(callerCwd, path36.join(bundleDir, manifest.entry));
11692
12129
  const trust = manifest.trustStatement.split("\n")[0] ?? "";
11693
12130
  const lines = [
11694
12131
  `// url=${opts.figmaUrl}`,
@@ -11709,7 +12146,7 @@ function runCodeConnect(opts) {
11709
12146
  `}`,
11710
12147
  ``
11711
12148
  ].join("\n");
11712
- const outFile = path35.resolve(callerCwd, opts.out ?? path35.join(bundleDir, `${component}.figma.ts`));
12149
+ const outFile = path36.resolve(callerCwd, opts.out ?? path36.join(bundleDir, `${component}.figma.ts`));
11713
12150
  writeFileSync13(outFile, lines);
11714
12151
  emitData(
11715
12152
  opts,
@@ -11749,17 +12186,17 @@ var init_codeconnect = __esm({
11749
12186
 
11750
12187
  // packages/mcp/src/server.ts
11751
12188
  import { createHash as createHash5 } from "node:crypto";
11752
- import { existsSync as existsSync29, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "node:fs";
11753
- import os6 from "node:os";
11754
- import path36 from "node:path";
12189
+ import { existsSync as existsSync30, mkdtempSync as mkdtempSync3, readFileSync as readFileSync26, readdirSync as readdirSync9, writeFileSync as writeFileSync14 } from "node:fs";
12190
+ import os7 from "node:os";
12191
+ import path37 from "node:path";
11755
12192
  import { fileURLToPath as fileURLToPath6 } from "node:url";
11756
12193
  import { z as z12 } from "zod";
11757
12194
  function sourceHash() {
11758
- const dir = path36.dirname(fileURLToPath6(import.meta.url));
12195
+ const dir = path37.dirname(fileURLToPath6(import.meta.url));
11759
12196
  const h = createHash5("sha256");
11760
- for (const f of readdirSync8(dir).filter((n) => n.endsWith(".ts")).sort()) {
12197
+ for (const f of readdirSync9(dir).filter((n) => n.endsWith(".ts")).sort()) {
11761
12198
  h.update(f);
11762
- h.update(readFileSync25(path36.join(dir, f)));
12199
+ h.update(readFileSync26(path37.join(dir, f)));
11763
12200
  }
11764
12201
  return h.digest("hex").slice(0, 16);
11765
12202
  }
@@ -11767,10 +12204,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
11767
12204
  var init_server = __esm({
11768
12205
  "packages/mcp/src/server.ts"() {
11769
12206
  "use strict";
11770
- REPO_ROOT3 = path36.resolve(path36.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
11771
- CLI_BIN = path36.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
11772
- BUNDLED_CLI = path36.join(path36.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
11773
- CLI_SPAWN = existsSync29(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
12207
+ REPO_ROOT3 = path37.resolve(path37.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
12208
+ CLI_BIN = path37.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
12209
+ BUNDLED_CLI = path37.join(path37.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
12210
+ CLI_SPAWN = existsSync30(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
11774
12211
  str = (d) => z12.string().describe(d);
11775
12212
  optStr = (d) => z12.string().optional().describe(d);
11776
12213
  TOOLS = [
@@ -11800,7 +12237,7 @@ var init_server = __esm({
11800
12237
  const single = i["metadata"];
11801
12238
  const parts = i["metadataParts"];
11802
12239
  if (single !== void 0 || parts !== void 0) {
11803
- const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
12240
+ const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
11804
12241
  if (single !== void 0) {
11805
12242
  writeFileSync14(tmp, single);
11806
12243
  argvOut.push("--metadata-raw-file", tmp);
@@ -11817,11 +12254,19 @@ var init_server = __esm({
11817
12254
  },
11818
12255
  {
11819
12256
  name: "tendril_permissions",
11820
- description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges per-tool entries into the project's .claude/settings.local.json, idempotent, never touches other settings), or list the entries without writing. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* (for those, write:true installs PLUGIN-prefixed names that will not match: list without writing and adapt the prefix instead). A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
12257
+ description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges the MCP per-tool entries, the run's shell surface, and project-scoped Write/Edit with deny guards for ./.claude and ./.git into the project's .claude/settings.local.json, idempotent, never touches other keys), or list the entries without writing. The shell/Write grants are CONVENIENCE, not a security boundary \u2014 the output's note names exactly what they trade; RELAY it with the offer. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* \u2014 when this session's tool names differ from the plugin defaults, pass figmaPrefix/tendrilPrefix with the prefixes you actually see, or the written entries never match. A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
11821
12258
  schema: z12.object({
11822
- write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
12259
+ write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries"),
12260
+ figmaPrefix: z12.string().optional().describe("The Figma server's entry prefix AS THIS SESSION NAMES ITS TOOLS \u2014 the part before __get_metadata (e.g. mcp__figma-remote, mcp__plugin_figma_figma). Pass it whenever the session's Figma tools are not mcp__plugin_figma_figma__* \u2014 entries written under the wrong prefix never match anything (measured: six dead entries in a figma-remote session)."),
12261
+ tendrilPrefix: z12.string().optional().describe("Same for the tendril server when this session's tendril tools are not mcp__plugin_tendril_tendril__* (e.g. mcp__tendril for claude mcp add installs).")
11823
12262
  }),
11824
- argv: (i) => ["permissions", "--claude", ...i["write"] === true ? ["--write"] : []]
12263
+ argv: (i) => [
12264
+ "permissions",
12265
+ "--claude",
12266
+ ...i["write"] === true ? ["--write"] : [],
12267
+ ...typeof i["figmaPrefix"] === "string" ? ["--figma-prefix", i["figmaPrefix"]] : [],
12268
+ ...typeof i["tendrilPrefix"] === "string" ? ["--tendril-prefix", i["tendrilPrefix"]] : []
12269
+ ]
11825
12270
  },
11826
12271
  {
11827
12272
  name: "tendril_doctor",
@@ -11871,7 +12316,7 @@ var init_server = __esm({
11871
12316
  const bridge = (label, single, parts) => {
11872
12317
  if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
11873
12318
  if (single === void 0 && parts === void 0) return;
11874
- const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
12319
+ const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
11875
12320
  if (single !== void 0) {
11876
12321
  writeFileSync14(tmp, single);
11877
12322
  argvOut.push(`--${label}-file`, tmp);
@@ -11914,7 +12359,7 @@ var init_server = __esm({
11914
12359
  const file = i["file"];
11915
12360
  if ([text, texts, file].filter((x) => x !== void 0).length !== 1) throw new Error("pass exactly one of `text` (single-block response), `texts` (multi-block response), or `file` (a saved envelope)");
11916
12361
  if (file !== void 0) return [...base, "--file", file];
11917
- const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
12362
+ const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
11918
12363
  if (text !== void 0) {
11919
12364
  writeFileSync14(tmp, text);
11920
12365
  return [...base, "--file", tmp, "--raw"];
@@ -11946,7 +12391,7 @@ var init_server = __esm({
11946
12391
  {
11947
12392
  name: "tendril_record_status",
11948
12393
  annotations: { readOnlyHint: true },
11949
- description: "Recording-set completeness: per-rep recorded/missing tools.",
12394
+ description: "Recording-set completeness: per-rep recorded/missing tools, files that exist but are unusable (invalid \u2014 re-record those), and whether the set-level token map is recorded. `complete` means USABLE by the next pipeline step, including the set-level get_variable_defs.",
11950
12395
  schema: z12.object({ setDir: str("recording set directory") }),
11951
12396
  argv: (i) => ["record", "status", "--set", i["setDir"]]
11952
12397
  },
@@ -11977,7 +12422,7 @@ var init_server = __esm({
11977
12422
  },
11978
12423
  {
11979
12424
  name: "tendril_engine_score",
11980
- description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, hover parity \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself.",
12425
+ description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, state parity (recording-selected \u2014 a bundle cannot unschedule it) \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself.",
11981
12426
  schema: z12.object({
11982
12427
  taskOrSet: str("reference task name or recording-set directory"),
11983
12428
  candidateDir: str("directory containing the proposed bundle files"),
@@ -12067,15 +12512,16 @@ __export(permissions_exports, {
12067
12512
  PERMISSIONS_DESCRIPTION: () => PERMISSIONS_DESCRIPTION,
12068
12513
  buildPermissions: () => buildPermissions,
12069
12514
  mergeAllowlist: () => mergeAllowlist,
12070
- runPermissions: () => runPermissions
12515
+ runPermissions: () => runPermissions,
12516
+ writeSelection: () => writeSelection
12071
12517
  });
12072
- import { existsSync as existsSync30, mkdirSync as mkdirSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
12073
- import os7 from "node:os";
12074
- import path37 from "node:path";
12075
- function mergeAllowlist(file, entries) {
12518
+ import { existsSync as existsSync31, mkdirSync as mkdirSync9, readFileSync as readFileSync27, writeFileSync as writeFileSync15 } from "node:fs";
12519
+ import os8 from "node:os";
12520
+ import path38 from "node:path";
12521
+ function mergeAllowlist(file, entries, denyEntries = []) {
12076
12522
  let settings = {};
12077
- if (existsSync30(file) && readFileSync26(file, "utf8").trim() !== "") {
12078
- settings = JSON.parse(readFileSync26(file, "utf8"));
12523
+ if (existsSync31(file) && readFileSync27(file, "utf8").trim() !== "") {
12524
+ settings = JSON.parse(readFileSync27(file, "utf8"));
12079
12525
  if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
12080
12526
  }
12081
12527
  const permissions = settings["permissions"] ??= {};
@@ -12085,16 +12531,31 @@ function mergeAllowlist(file, entries) {
12085
12531
  const present = new Set(allow.filter((x) => typeof x === "string"));
12086
12532
  const added = entries.filter((e) => !present.has(e));
12087
12533
  const alreadyPresent = entries.filter((e) => present.has(e));
12088
- if (added.length > 0) {
12534
+ let denyAdded = [];
12535
+ if (denyEntries.length > 0) {
12536
+ const deny = permissions["deny"] ??= [];
12537
+ if (!Array.isArray(deny)) throw new Error("permissions.deny is not an array");
12538
+ const denyPresent = new Set(deny.filter((x) => typeof x === "string"));
12539
+ denyAdded = denyEntries.filter((e) => !denyPresent.has(e));
12540
+ deny.push(...denyAdded);
12541
+ }
12542
+ if (added.length > 0 || denyAdded.length > 0) {
12089
12543
  allow.push(...added);
12090
- mkdirSync9(path37.dirname(file), { recursive: true });
12544
+ mkdirSync9(path38.dirname(file), { recursive: true });
12091
12545
  writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
12092
12546
  `);
12093
12547
  }
12094
- return { added, alreadyPresent };
12548
+ return { added, alreadyPresent, denyAdded };
12095
12549
  }
12096
12550
  async function buildPermissions(options) {
12097
12551
  const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
12552
+ for (const [flag, value] of [["--figma-prefix", options.figmaPrefix], ["--tendril-prefix", options.tendrilPrefix]]) {
12553
+ if (value !== void 0 && !LEGAL_MCP_PREFIX.test(value)) {
12554
+ throw new Error(`${flag} must look like mcp__<server-name> (letters, digits, _ and - only) \u2014 got ${JSON.stringify(value)}`);
12555
+ }
12556
+ }
12557
+ const figmaPrefix = options.figmaPrefix ?? FIGMA_PLUGIN_PREFIX;
12558
+ const tendrilPrefix = options.tendrilPrefix ?? TENDRIL_PLUGIN_PREFIX;
12098
12559
  const pipeline = new Set(FIGMA_TOOL_FALLBACK);
12099
12560
  let figmaTools = FIGMA_TOOL_FALLBACK;
12100
12561
  try {
@@ -12106,38 +12567,64 @@ async function buildPermissions(options) {
12106
12567
  }
12107
12568
  return {
12108
12569
  host: "claude",
12109
- serverEntries: [TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX],
12570
+ serverEntries: [tendrilPrefix, figmaPrefix],
12110
12571
  toolEntries: [
12111
- ...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${TENDRIL_PLUGIN_PREFIX}__${n}`),
12112
- ...figmaTools.map((name) => `${FIGMA_PLUGIN_PREFIX}__${name}`)
12572
+ ...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${tendrilPrefix}__${n}`),
12573
+ ...figmaTools.map((name) => `${figmaPrefix}__${name}`)
12113
12574
  ],
12575
+ shellEntries: [...SHELL_READONLY_ENTRIES, ...SHELL_PIPELINE_ENTRIES],
12576
+ projectFileEntries: PROJECT_FILE_ENTRIES,
12577
+ projectDenyEntries: PROJECT_DENY_ENTRIES,
12578
+ manualCautionEntries: MANUAL_CAUTION_ENTRIES,
12579
+ shellEntriesNote: "These are CONVENIENCE grants replacing the ~300 prompts a measured run costs \u2014 not a security boundary. What they trade, named: (1) the read verbs (cat/grep/head/\u2026) let the agent read ANY file without a prompt, secrets included \u2014 remove those entries if that is unacceptable; (2) Claude Code Bash patterns are prefix matches and redirection is not a command separator (`cat x > file` rides `Bash(cat:*)`) \u2014 the host's docs call argument patterns best-effort, not an enforcement boundary (pipes and `&&`/`;` chains do decompose, each piece prompting on its own); (3) the per-subcommand CLI grants carry those subcommands' own write surface (recordings, bundles, score history, --out paths) and the npx form trusts future @latest publishes. Never auto-written: a blanket `tendril` grant (an agent could silently widen this very allowlist via `tendril permissions --write`), `find` (arbitrary delete/exec via -delete/-exec), clobber verbs \u2014 those stay a manual, deliberate addition. Write/Edit are project-scoped, written only to the project-local file, with deny entries for ./.claude/** and ./.git/** so the grant can never rewrite this settings file or plant git hooks. Every entry is removable any time.",
12114
12580
  serverEntriesCaution: "The server-wide entries allow EVERY tool those servers serve \u2014 for tendril that includes tendril_generate_curated (spends your OpenRouter budget when configured), and for Figma every tool the desktop server exposes, beyond the read tools this pipeline uses. The per-tool list is the recommended default.",
12115
- directConfigNote: `Installed via claude mcp add instead of the plugin? Replace the prefixes: ${TENDRIL_PLUGIN_PREFIX}__<tool> becomes mcp__<your-server-name>__<tool> (same for Figma).`
12581
+ directConfigNote: `Prefixes default to the plugin install shape. A different install (claude mcp add, the remote Figma server, \u2026) names its tools mcp__<server-name>__<tool> \u2014 pass the real prefixes with --figma-prefix / --tendril-prefix (MCP: figmaPrefix / tendrilPrefix) so the written entries actually match; run 20 measured six Figma entries that could never fire because the session's server was figma-remote.`
12116
12582
  };
12117
12583
  }
12584
+ function writeSelection(result, user) {
12585
+ return user ? { entries: [...result.toolEntries], denyEntries: [] } : { entries: [...result.toolEntries, ...result.shellEntries, ...result.projectFileEntries], denyEntries: [...result.projectDenyEntries] };
12586
+ }
12118
12587
  async function runPermissions(flags) {
12119
12588
  if (flags.describe) {
12120
12589
  printDescription(PERMISSIONS_DESCRIPTION);
12121
12590
  return;
12122
12591
  }
12123
- const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
12592
+ let result;
12593
+ try {
12594
+ result = await buildPermissions({
12595
+ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {},
12596
+ ...flags.figmaPrefix !== void 0 ? { figmaPrefix: flags.figmaPrefix } : {},
12597
+ ...flags.tendrilPrefix !== void 0 ? { tendrilPrefix: flags.tendrilPrefix } : {}
12598
+ });
12599
+ } catch (err) {
12600
+ fail(flags, ExitCode.InputValidation, {
12601
+ error: err instanceof Error ? err.message : String(err),
12602
+ code: "bad-prefix",
12603
+ remediation: "Pass the prefix exactly as the session names its MCP tools: the part before the final __<tool> (e.g. mcp__figma-remote, mcp__plugin_figma_figma)."
12604
+ });
12605
+ return;
12606
+ }
12124
12607
  if (flags.write) {
12125
12608
  const base = process.env["INIT_CWD"] ?? process.cwd();
12126
- const file = flags.user ? path37.join(os7.homedir(), ".claude", "settings.json") : path37.join(base, ".claude", "settings.local.json");
12609
+ const file = flags.user ? path38.join(os8.homedir(), ".claude", "settings.json") : path38.join(base, ".claude", "settings.local.json");
12610
+ const { entries, denyEntries } = writeSelection(result, flags.user === true);
12127
12611
  if (flags.dryRun) {
12128
- emitData(flags, { file, wouldAdd: result.toolEntries }, () => {
12129
- process.stdout.write(`dry-run: would merge ${result.toolEntries.length} per-tool entries into ${file}
12130
- `);
12612
+ emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
12613
+ process.stdout.write(
12614
+ `dry-run: would merge ${entries.length} allow entries (${flags.user ? "MCP tools only \u2014 shell and Write/Edit grants stay project-local" : "MCP tools + shell surface + project Write/Edit"})${denyEntries.length > 0 ? ` and ${denyEntries.length} deny guards (./.claude/**, ./.git/**)` : ""} into ${file}
12615
+ `
12616
+ );
12131
12617
  });
12132
12618
  return;
12133
12619
  }
12134
12620
  try {
12135
- const { added, alreadyPresent } = mergeAllowlist(file, result.toolEntries);
12136
- emitData(flags, { ...result, written: { file, added, alreadyPresent } }, () => {
12621
+ const { added, alreadyPresent, denyAdded } = mergeAllowlist(file, entries, denyEntries);
12622
+ emitData(flags, { ...result, written: { file, added, alreadyPresent, denyAdded } }, () => {
12137
12623
  process.stdout.write(
12138
- added.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
12139
- ` : `installed: ${added.length} allowlist entr${added.length === 1 ? "y" : "ies"} added to ${file}${alreadyPresent.length > 0 ? ` (${alreadyPresent.length} already present)` : ""}
12140
- restart or reopen the Claude Code session to pick up settings changes
12624
+ added.length === 0 && denyAdded.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
12625
+ ` : `installed: ${added.length} allowlist entr${added.length === 1 ? "y" : "ies"}${denyAdded.length > 0 ? ` and ${denyAdded.length} deny guard${denyAdded.length === 1 ? "" : "s"} (Write/Edit cannot touch ./.claude/** or ./.git/**)` : ""} added to ${file}${alreadyPresent.length > 0 ? ` (${alreadyPresent.length} already present)` : ""}
12626
+ ${flags.user ? "" : ` \u26A0 ${result.shellEntriesNote}
12627
+ `} restart or reopen the Claude Code session to pick up settings changes
12141
12628
  `
12142
12629
  );
12143
12630
  });
@@ -12162,6 +12649,24 @@ Or paste into .claude/settings.json under permissions.allow:
12162
12649
 
12163
12650
  ${quoted(result.toolEntries)}
12164
12651
 
12652
+ The SHELL surface a run actually uses:
12653
+
12654
+ ${quoted(result.shellEntries)}
12655
+
12656
+ \u26A0 ${result.shellEntriesNote}
12657
+
12658
+ Project-scoped file access (written only to the project-local file):
12659
+
12660
+ ${quoted(result.projectFileEntries)}
12661
+
12662
+ \u2026with these under permissions.deny (so the grant above can never rewrite the settings file or .git):
12663
+
12664
+ ${quoted(result.projectDenyEntries)}
12665
+
12666
+ Deliberate manual additions \u2014 clobber- or exec-capable, never auto-written:
12667
+
12668
+ ${quoted(result.manualCautionEntries)}
12669
+
12165
12670
  Shorter but broader \u2014 one entry per server:
12166
12671
 
12167
12672
  ${quoted(result.serverEntries)}
@@ -12173,7 +12678,7 @@ ${result.directConfigNote}
12173
12678
  );
12174
12679
  });
12175
12680
  }
12176
- var FIGMA_TOOL_FALLBACK, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX;
12681
+ var FIGMA_TOOL_FALLBACK, SHELL_READONLY_ENTRIES, SHELL_PIPELINE_ENTRIES, PROJECT_FILE_ENTRIES, PROJECT_DENY_ENTRIES, MANUAL_CAUTION_ENTRIES, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX, LEGAL_MCP_PREFIX;
12177
12682
  var init_permissions = __esm({
12178
12683
  "packages/cli/src/commands/permissions.ts"() {
12179
12684
  "use strict";
@@ -12185,15 +12690,47 @@ var init_permissions = __esm({
12185
12690
  init_output();
12186
12691
  init_doctor();
12187
12692
  FIGMA_TOOL_FALLBACK = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_motion_context", "get_figjam"];
12693
+ SHELL_READONLY_ENTRIES = [
12694
+ "Bash(date:*)",
12695
+ "Bash(ls:*)",
12696
+ "Bash(stat:*)",
12697
+ "Bash(wc:*)",
12698
+ "Bash(head:*)",
12699
+ "Bash(tail:*)",
12700
+ "Bash(cat:*)",
12701
+ "Bash(shasum:*)",
12702
+ "Bash(grep:*)"
12703
+ ];
12704
+ SHELL_PIPELINE_ENTRIES = [
12705
+ // The skill's own rule: candidate dirs are created with a bare
12706
+ // `mkdir -p`, no compounds — one grant covers the whole batch.
12707
+ "Bash(mkdir -p:*)",
12708
+ // The published CLI, PER SUBCOMMAND — never `tendril *`. The CLI
12709
+ // writes the settings file through Node fs, not the host's Write
12710
+ // tool, so the protected-path guard that stops `Write(./**)` from
12711
+ // touching .claude/ does NOT stop `tendril permissions --write`: a
12712
+ // blanket grant would let an allowlisted agent silently widen its own
12713
+ // allowlist (review finding, confirmed from the host's docs).
12714
+ // `permissions`, `activate` and `init` are deliberately absent.
12715
+ ...["verify", "inspect", "doctor", "record", "engine", "fonts", "generate"].flatMap((sub) => [
12716
+ `Bash(tendril ${sub}:*)`,
12717
+ `Bash(npx -y -p @tendrilapp/cli@latest tendril ${sub}:*)`
12718
+ ])
12719
+ ];
12720
+ PROJECT_FILE_ENTRIES = ["Write(./**)", "Edit(./**)"];
12721
+ PROJECT_DENY_ENTRIES = ["Write(./.claude/**)", "Edit(./.claude/**)", "Write(./.git/**)", "Edit(./.git/**)"];
12722
+ MANUAL_CAUTION_ENTRIES = ["Bash(cp -R:*)", "Bash(find:*)"];
12188
12723
  PERMISSIONS_DESCRIPTION = {
12189
12724
  name: "permissions",
12190
12725
  summary: "Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools).",
12191
12726
  args: [],
12192
12727
  flags: [
12193
12728
  { flag: "--claude", description: "Claude Code settings format (the default and currently only format)" },
12194
- { flag: "--write", description: "Merge the per-tool entries into .claude/settings.local.json in the current project (idempotent; creates the file; never touches other keys)" },
12195
- { flag: "--user", description: "With --write: target ~/.claude/settings.json (every project) instead of the project-local file" },
12729
+ { flag: "--write", description: "Merge the pipeline allowlist (MCP tools + shell surface + project Write/Edit, with deny guards for ./.claude and ./.git) into .claude/settings.local.json in the current project (idempotent; creates the file; never touches other keys)" },
12730
+ { flag: "--user", description: "With --write: target ~/.claude/settings.json instead \u2014 MCP tool entries ONLY; shell and Write/Edit grants stay project-local, never every-project" },
12196
12731
  { flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
12732
+ { flag: "--figma-prefix <prefix>", description: "The session's real Figma MCP entry prefix (e.g. mcp__figma-remote) \u2014 the plugin-install default cannot match other install shapes" },
12733
+ { flag: "--tendril-prefix <prefix>", description: "The session's real tendril MCP entry prefix (e.g. mcp__tendril for claude mcp add installs)" },
12197
12734
  { flag: "--json", description: "Machine-readable output" }
12198
12735
  ],
12199
12736
  output: {
@@ -12201,13 +12738,14 @@ var init_permissions = __esm({
12201
12738
  serverEntries: "string[] \u2014 one entry per MCP server (allows every tool it serves)",
12202
12739
  toolEntries: "string[] \u2014 per-tool entries for selective allowlists",
12203
12740
  directConfigNote: "string \u2014 prefix rewrite for non-plugin (claude mcp add) installs",
12204
- written: "with --write: { file, added, alreadyPresent }"
12741
+ written: "with --write: { file, added, alreadyPresent, denyAdded }"
12205
12742
  },
12206
12743
  exitCodes: { 0: "printed or written", 3: "with --write: the target file exists but is not JSON this command can safely rewrite" },
12207
12744
  examples: ["tendril permissions --claude --write", "tendril permissions --claude", "tendril permissions --claude --json"]
12208
12745
  };
12209
12746
  TENDRIL_PLUGIN_PREFIX = "mcp__plugin_tendril_tendril";
12210
12747
  FIGMA_PLUGIN_PREFIX = "mcp__plugin_figma_figma";
12748
+ LEGAL_MCP_PREFIX = /^mcp__[A-Za-z0-9_-]+$/;
12211
12749
  }
12212
12750
  });
12213
12751
 
@@ -12217,24 +12755,24 @@ __export(inspect_exports, {
12217
12755
  INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
12218
12756
  runInspect: () => runInspect
12219
12757
  });
12220
- import { existsSync as existsSync31, readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
12221
- import path38 from "node:path";
12758
+ import { existsSync as existsSync32, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
12759
+ import path39 from "node:path";
12222
12760
  async function runInspect(opts) {
12223
12761
  if (opts.describe) {
12224
12762
  printDescription(INSPECT_DESCRIPTION);
12225
12763
  return;
12226
12764
  }
12227
- const bundleDir = path38.resolve(opts.bundleDir);
12228
- const evidenceDir = path38.join(bundleDir, "verify-evidence");
12229
- const manifestPath2 = path38.join(bundleDir, "component.json");
12230
- if (!existsSync31(evidenceDir) || !existsSync31(manifestPath2)) {
12765
+ const bundleDir = path39.resolve(opts.bundleDir);
12766
+ const evidenceDir = path39.join(bundleDir, "verify-evidence");
12767
+ const manifestPath2 = path39.join(bundleDir, "component.json");
12768
+ if (!existsSync32(evidenceDir) || !existsSync32(manifestPath2)) {
12231
12769
  fail(opts, ExitCode.InputValidation, {
12232
- error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync31(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
12770
+ error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync32(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
12233
12771
  code: "no-evidence",
12234
12772
  remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
12235
12773
  });
12236
12774
  }
12237
- const { manifest } = readBundleManifest(readFileSync27(manifestPath2, "utf8"));
12775
+ const { manifest } = readBundleManifest(readFileSync28(manifestPath2, "utf8"));
12238
12776
  if (manifest === void 0) {
12239
12777
  fail(opts, ExitCode.InputValidation, {
12240
12778
  error: "component.json did not parse as a bundle manifest",
@@ -12242,8 +12780,8 @@ async function runInspect(opts) {
12242
12780
  remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
12243
12781
  });
12244
12782
  }
12245
- const setDir = path38.resolve(opts.set ?? manifest.provenance.recordingSet.path);
12246
- const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync31(path38.join(evidenceDir, `${rep}-ref.png`)) && existsSync31(path38.join(evidenceDir, `${rep}-render.png`)));
12783
+ const setDir = path39.resolve(opts.set ?? manifest.provenance.recordingSet.path);
12784
+ const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync32(path39.join(evidenceDir, `${rep}-ref.png`)) && existsSync32(path39.join(evidenceDir, `${rep}-render.png`)));
12247
12785
  if (reps.length === 0) {
12248
12786
  fail(opts, ExitCode.InputValidation, {
12249
12787
  error: "verify-evidence holds no ref/render pairs for this bundle's configs",
@@ -12254,15 +12792,15 @@ async function runInspect(opts) {
12254
12792
  let crops = 0;
12255
12793
  const sections = [];
12256
12794
  for (const rep of reps) {
12257
- const ref = new Uint8Array(readFileSync27(path38.join(evidenceDir, `${rep}-ref.png`)));
12258
- const render = new Uint8Array(readFileSync27(path38.join(evidenceDir, `${rep}-render.png`)));
12795
+ const ref = new Uint8Array(readFileSync28(path39.join(evidenceDir, `${rep}-ref.png`)));
12796
+ const render = new Uint8Array(readFileSync28(path39.join(evidenceDir, `${rep}-render.png`)));
12259
12797
  const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
12260
12798
  const cells = [];
12261
12799
  for (const [i, n] of nodes.entries()) {
12262
12800
  const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
12263
12801
  try {
12264
- writeFileSync16(path38.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
12265
- writeFileSync16(path38.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
12802
+ writeFileSync16(path39.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
12803
+ writeFileSync16(path39.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
12266
12804
  } catch {
12267
12805
  continue;
12268
12806
  }
@@ -12275,7 +12813,7 @@ async function runInspect(opts) {
12275
12813
  `<section><h2>${esc(rep)}</h2><div class="full"><span><em>recorded</em><img src="./${rep}-ref.png"></span><span><em>rendered</em><img src="./${rep}-render.png"></span><span><em>diff</em><img src="./${rep}-diff.png"></span></div>` + (cells.length > 0 ? `<div class="grid">${cells.join("")}</div>` : `<p class="none">no small recorded nodes in this config's sweep</p>`) + `</section>`
12276
12814
  );
12277
12815
  }
12278
- const sheet = path38.join(evidenceDir, "inspect.html");
12816
+ const sheet = path39.join(evidenceDir, "inspect.html");
12279
12817
  writeFileSync16(
12280
12818
  sheet,
12281
12819
  `<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
@@ -12353,17 +12891,17 @@ __export(generate_recorded_exports, {
12353
12891
  runGenerateRecorded: () => runGenerateRecorded
12354
12892
  });
12355
12893
  import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
12356
- import { existsSync as existsSync32, readFileSync as readFileSync28 } from "node:fs";
12357
- import path39 from "node:path";
12894
+ import { existsSync as existsSync33, readFileSync as readFileSync29 } from "node:fs";
12895
+ import path40 from "node:path";
12358
12896
  async function runGenerateRecorded(opts) {
12359
12897
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
12360
- const outDirAbs = path39.resolve(callerCwd, opts.out);
12361
- const recordedAsPath = path39.resolve(callerCwd, opts.recorded);
12898
+ const outDirAbs = path40.resolve(callerCwd, opts.out);
12899
+ const recordedAsPath = path40.resolve(callerCwd, opts.recorded);
12362
12900
  let task;
12363
12901
  let taskName;
12364
12902
  let authoredApi;
12365
12903
  let composition;
12366
- const isSet = existsSync32(path39.join(recordedAsPath, "recording-set.json"));
12904
+ const isSet = existsSync33(path40.join(recordedAsPath, "recording-set.json"));
12367
12905
  const registry = TASKS[opts.recorded];
12368
12906
  if (registry !== void 0 && !isSet) {
12369
12907
  task = registry;
@@ -12372,7 +12910,7 @@ async function runGenerateRecorded(opts) {
12372
12910
  try {
12373
12911
  const authored = authorTaskFromSet(recordedAsPath);
12374
12912
  task = authored.task;
12375
- taskName = path39.basename(recordedAsPath);
12913
+ taskName = path40.basename(recordedAsPath);
12376
12914
  authoredApi = authored.api;
12377
12915
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
12378
12916
  if (roles.success) composition = roles.data;
@@ -12406,7 +12944,7 @@ async function runGenerateRecorded(opts) {
12406
12944
  warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
12407
12945
  }
12408
12946
  const missing = task.configs.filter(
12409
- (c) => !existsSync32(path39.join(task.set, c.rep, "get_screenshot.json")) || !existsSync32(path39.join(task.set, c.rep, "get_metadata.json")) || !existsSync32(path39.join(task.set, c.rep, "get_design_context.json"))
12947
+ (c) => !existsSync33(path40.join(task.set, c.rep, "get_screenshot.json")) || !existsSync33(path40.join(task.set, c.rep, "get_metadata.json")) || !existsSync33(path40.join(task.set, c.rep, "get_design_context.json"))
12410
12948
  );
12411
12949
  if (missing.length > 0) {
12412
12950
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -12476,8 +13014,8 @@ async function runGenerateRecorded(opts) {
12476
13014
  ` : `${line}
12477
13015
  `);
12478
13016
  if (opts.dryRun) {
12479
- emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path39.join(outDirAbs, taskName) }, () => {
12480
- process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path39.join(outDirAbs, taskName)})
13017
+ emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path40.join(outDirAbs, taskName) }, () => {
13018
+ process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path40.join(outDirAbs, taskName)})
12481
13019
  `);
12482
13020
  });
12483
13021
  return;
@@ -12500,10 +13038,10 @@ async function runGenerateRecorded(opts) {
12500
13038
  });
12501
13039
  }
12502
13040
  }
12503
- const bundleDir = path39.join(outDirAbs, taskName);
12504
- if (existsSync32(path39.join(bundleDir, "component.json"))) {
13041
+ const bundleDir = path40.join(outDirAbs, taskName);
13042
+ if (existsSync33(path40.join(bundleDir, "component.json"))) {
12505
13043
  try {
12506
- const prior = readBundleManifest(readFileSync28(path39.join(bundleDir, "component.json"), "utf8")).manifest;
13044
+ const prior = readBundleManifest(readFileSync29(path40.join(bundleDir, "component.json"), "utf8")).manifest;
12507
13045
  if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
12508
13046
  fail(opts, ExitCode.InputValidation, {
12509
13047
  error: `${bundleDir} already holds a bundle bound to recording set "${prior.provenance.recordingSet.path}" \u2014 generating here against a different set would silently rewrite its verification identity`,
@@ -12669,7 +13207,7 @@ init_invocation();
12669
13207
  init_output();
12670
13208
  import { intro, isCancel, outro, password } from "@clack/prompts";
12671
13209
  import fs from "node:fs";
12672
- import path24 from "node:path";
13210
+ import path25 from "node:path";
12673
13211
  var INIT_DESCRIPTION = {
12674
13212
  name: "init",
12675
13213
  summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
@@ -12706,7 +13244,7 @@ async function runInit(flags) {
12706
13244
  printDescription(INIT_DESCRIPTION);
12707
13245
  return;
12708
13246
  }
12709
- const envPath = path24.resolve(process.cwd(), ".env");
13247
+ const envPath = path25.resolve(process.cwd(), ".env");
12710
13248
  const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
12711
13249
  let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
12712
13250
  let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
@@ -12727,7 +13265,7 @@ async function runInit(flags) {
12727
13265
  next.set(ENV_KEYS.figma, figmaToken);
12728
13266
  next.set(ENV_KEYS.openrouter, openrouterKey);
12729
13267
  const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
12730
- const gitignorePath = path24.resolve(process.cwd(), ".gitignore");
13268
+ const gitignorePath = path25.resolve(process.cwd(), ".gitignore");
12731
13269
  const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
12732
13270
  const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
12733
13271
  if (flags.dryRun) {
@@ -12783,14 +13321,14 @@ init_invocation();
12783
13321
  init_output();
12784
13322
  init_entitlement();
12785
13323
  import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
12786
- import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync19 } from "node:fs";
13324
+ import { readFileSync as readFileSync16, readdirSync as readdirSync5, existsSync as existsSync20 } from "node:fs";
12787
13325
 
12788
13326
  // packages/cli/src/pipeline.ts
12789
13327
  init_src2();
12790
13328
  init_src4();
12791
13329
  init_src6();
12792
13330
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
12793
- import path25 from "node:path";
13331
+ import path26 from "node:path";
12794
13332
 
12795
13333
  // packages/cli/src/assets-module.ts
12796
13334
  init_src();
@@ -13126,7 +13664,7 @@ async function runGenerationPipeline(input) {
13126
13664
  });
13127
13665
  const written = [];
13128
13666
  if (!input.dryRun) {
13129
- const dir = path25.resolve(input.outDir, semantics.componentName);
13667
+ const dir = path26.resolve(input.outDir, semantics.componentName);
13130
13668
  mkdirSync5(dir, { recursive: true });
13131
13669
  const files = {
13132
13670
  // Bundle-local tokens: THE emission the component's CSS resolves
@@ -13150,13 +13688,13 @@ async function runGenerationPipeline(input) {
13150
13688
  `
13151
13689
  };
13152
13690
  for (const [name, content] of Object.entries(files)) {
13153
- const filePath = path25.join(dir, name);
13691
+ const filePath = path26.join(dir, name);
13154
13692
  writeFileSync8(filePath, content);
13155
13693
  written.push(filePath);
13156
13694
  }
13157
13695
  for (const artifact of emitTokenArtifacts(input.mapping)) {
13158
- const filePath = path25.resolve(input.outDir, artifact.path);
13159
- mkdirSync5(path25.dirname(filePath), { recursive: true });
13696
+ const filePath = path26.resolve(input.outDir, artifact.path);
13697
+ mkdirSync5(path26.dirname(filePath), { recursive: true });
13160
13698
  writeFileSync8(filePath, artifact.content);
13161
13699
  written.push(filePath);
13162
13700
  }
@@ -13215,7 +13753,7 @@ var GENERATE_DESCRIPTION = {
13215
13753
  function resolveProvidedSource(flags, contextFile) {
13216
13754
  let raw;
13217
13755
  try {
13218
- raw = readFileSync15(contextFile, "utf8");
13756
+ raw = readFileSync16(contextFile, "utf8");
13219
13757
  } catch {
13220
13758
  fail(flags, ExitCode.InputValidation, {
13221
13759
  error: `Cannot read context file "${contextFile}".`,
@@ -13335,11 +13873,11 @@ token mapping (${mapping.flat.length} variables):
13335
13873
  let initialCode;
13336
13874
  let initialSemantics;
13337
13875
  try {
13338
- if (existsSync19(flags.out)) {
13339
- for (const entry of readdirSync4(flags.out)) {
13876
+ if (existsSync20(flags.out)) {
13877
+ for (const entry of readdirSync5(flags.out)) {
13340
13878
  const cjPath = `${flags.out}/${entry}/component.json`;
13341
- if (!existsSync19(cjPath)) continue;
13342
- const cj = JSON.parse(readFileSync15(cjPath, "utf8"));
13879
+ if (!existsSync20(cjPath)) continue;
13880
+ const cj = JSON.parse(readFileSync16(cjPath, "utf8"));
13343
13881
  if (cj.name !== void 0 && Array.isArray(cj.props)) {
13344
13882
  previousApi = JSON.stringify({
13345
13883
  componentName: cj.name,
@@ -13347,14 +13885,14 @@ token mapping (${mapping.flat.length} variables):
13347
13885
  });
13348
13886
  const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
13349
13887
  const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
13350
- if (flags.refine && existsSync19(tsxPath) && existsSync19(cssPath)) {
13888
+ if (flags.refine && existsSync20(tsxPath) && existsSync20(cssPath)) {
13351
13889
  initialCode = {
13352
- tsx: readFileSync15(tsxPath, "utf8"),
13353
- css: readFileSync15(cssPath, "utf8")
13890
+ tsx: readFileSync16(tsxPath, "utf8"),
13891
+ css: readFileSync16(cssPath, "utf8")
13354
13892
  };
13355
13893
  const semPath = `${flags.out}/${entry}/semantics.json`;
13356
- if (existsSync19(semPath)) {
13357
- initialSemantics = JSON.parse(readFileSync15(semPath, "utf8"));
13894
+ if (existsSync20(semPath)) {
13895
+ initialSemantics = JSON.parse(readFileSync16(semPath, "utf8"));
13358
13896
  }
13359
13897
  }
13360
13898
  break;
@@ -13631,6 +14169,23 @@ function buildProgram() {
13631
14169
  ...local["face"] !== void 0 ? { face: Number(local["face"]) } : {}
13632
14170
  });
13633
14171
  });
14172
+ fonts.command("discover").description("List faces installed on THIS machine (name-table parse; no OS tools, no network). Refusal-class, italic and variable faces are listed with why they cannot be cached.").argument("[family]", "family to match exactly (case/space-insensitive); omit to list everything").option("--cache <dir>", "cache directory (default: the per-user font cache)").action(async (family, _o, cmd) => {
14173
+ const flags = globalFlags(cmd.parent.parent);
14174
+ const { runFontsDiscover: runFontsDiscover2 } = await Promise.resolve().then(() => (init_fonts(), fonts_exports));
14175
+ runFontsDiscover2({ ...flags, cacheDir: cmd.opts()["cache"] ?? DEFAULT_FONT_CACHE, ...family !== void 0 ? { family } : {} });
14176
+ });
14177
+ fonts.command("add-system").description("Cache a family from the installed fonts so a licensed kit can be SCORED locally. Licence lands as unknown: bundles declare the face; its bytes never ship.").argument("<family>", "family exactly as fonts discover lists it").option("--weights <w...>", "only these numeric weights (default: every eligible static upright weight)").option("--set <dir>", "recording set to verify the family spelling against").option("--cache <dir>", "cache directory (default: the per-user font cache)").action(async (family, _o, cmd) => {
14178
+ const flags = globalFlags(cmd.parent.parent);
14179
+ const local = cmd.opts();
14180
+ const { runFontsAddSystem: runFontsAddSystem2 } = await Promise.resolve().then(() => (init_fonts(), fonts_exports));
14181
+ runFontsAddSystem2({
14182
+ ...flags,
14183
+ family,
14184
+ cacheDir: local["cache"] ?? DEFAULT_FONT_CACHE,
14185
+ ...local["weights"] !== void 0 ? { weights: local["weights"].map(Number) } : {},
14186
+ ...local["set"] !== void 0 ? { set: local["set"] } : {}
14187
+ });
14188
+ });
13634
14189
  fonts.command("status").option("--lock <file>", "hash lock to verify against (e.g. packages/verify/fixtures/fonts.lock.json)").option("--cache <dir>", "cache directory (default: the per-user font cache)").action(async (_o, cmd) => {
13635
14190
  const flags = globalFlags(cmd.parent.parent);
13636
14191
  const local = cmd.opts();
@@ -13681,7 +14236,7 @@ function buildProgram() {
13681
14236
  ...local["out"] !== void 0 ? { out: local["out"] } : {}
13682
14237
  });
13683
14238
  });
13684
- program.command("permissions").description("Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools) \u2014 no more transcribing tool names from prompts.").option("--claude", "Claude Code settings format (the default and currently only format)").option("--write", "merge the per-tool entries into .claude/settings.local.json in the current project (idempotent; never touches other keys)").option("--user", "with --write: target ~/.claude/settings.json (every project) instead").option("--mcp-url <url>", "Figma MCP endpoint to list live tool names from").action(async (_o, cmd) => {
14239
+ program.command("permissions").description("Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools) \u2014 no more transcribing tool names from prompts.").option("--claude", "Claude Code settings format (the default and currently only format)").option("--write", "merge the per-tool entries into .claude/settings.local.json in the current project (idempotent; never touches other keys)").option("--user", "with --write: target ~/.claude/settings.json \u2014 MCP tool entries only; shell and Write/Edit grants stay project-local").option("--mcp-url <url>", "Figma MCP endpoint to list live tool names from").option("--figma-prefix <prefix>", "the session's real Figma MCP entry prefix (e.g. mcp__figma-remote) when it is not the plugin default").option("--tendril-prefix <prefix>", "the session's real tendril MCP entry prefix (e.g. mcp__tendril for claude mcp add installs)").action(async (_o, cmd) => {
13685
14240
  const flags = globalFlags(cmd.parent);
13686
14241
  const local = cmd.opts();
13687
14242
  const { runPermissions: runPermissions2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
@@ -13689,7 +14244,9 @@ function buildProgram() {
13689
14244
  ...flags,
13690
14245
  ...local["write"] !== void 0 ? { write: local["write"] } : {},
13691
14246
  ...local["user"] !== void 0 ? { user: local["user"] } : {},
13692
- ...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {}
14247
+ ...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {},
14248
+ ...local["figmaPrefix"] !== void 0 ? { figmaPrefix: local["figmaPrefix"] } : {},
14249
+ ...local["tendrilPrefix"] !== void 0 ? { tendrilPrefix: local["tendrilPrefix"] } : {}
13693
14250
  });
13694
14251
  });
13695
14252
  program.command("inspect").description("Build an eye-verifiable detail sheet from verify evidence: magnified recorded-vs-rendered crops of every small recorded node (icons, controls, marks) plus full-frame triples, in one static HTML page.").argument("<bundleDir>", "bundle directory with component.json and verify-evidence").option("--set <dir>", "recording set override (default: the bundle's provenance path)").option("--max-area <px2>", "node area ceiling for the detail sweep", "1024").action(async (bundleDir, _o, cmd) => {