@tendrilapp/cli 0.1.27 → 0.1.28

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
@@ -1337,8 +1337,8 @@ var init_src = __esm({
1337
1337
  function variableNameToPath(name) {
1338
1338
  return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
1339
1339
  }
1340
- function tokenPathToCssVar(path40) {
1341
- return `--${path40.join("-")}`;
1340
+ function tokenPathToCssVar(path41) {
1341
+ return `--${path41.join("-")}`;
1342
1342
  }
1343
1343
  function toDtcgToken(variable, defaultMode) {
1344
1344
  const modes = Object.keys(variable.valuesByMode);
@@ -1382,11 +1382,11 @@ function toDtcgToken(variable, defaultMode) {
1382
1382
  }
1383
1383
  function mapVariablesToDtcg(variables, defaultMode = "light") {
1384
1384
  const entries = variables.map((variable) => {
1385
- const path40 = variableNameToPath(variable.name);
1386
- if (path40.length === 0) {
1385
+ const path41 = variableNameToPath(variable.name);
1386
+ if (path41.length === 0) {
1387
1387
  throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
1388
1388
  }
1389
- return { variable, path: path40 };
1389
+ return { variable, path: path41 };
1390
1390
  });
1391
1391
  const groupPrefixes = /* @__PURE__ */ new Set();
1392
1392
  for (const e of entries) {
@@ -1407,21 +1407,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
1407
1407
  }
1408
1408
  const tokens = {};
1409
1409
  const flat = [];
1410
- for (const { variable, path: path40 } of entries) {
1410
+ for (const { variable, path: path41 } of entries) {
1411
1411
  const token = toDtcgToken(variable, defaultMode);
1412
1412
  let group = tokens;
1413
- for (const segment of path40.slice(0, -1)) {
1413
+ for (const segment of path41.slice(0, -1)) {
1414
1414
  const existing = group[segment];
1415
1415
  group = existing ?? (group[segment] = {});
1416
1416
  }
1417
- const leaf = path40[path40.length - 1];
1417
+ const leaf = path41[path41.length - 1];
1418
1418
  if (group[leaf] !== void 0) {
1419
- throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path40.join(".")}" (variable ${variable.id})`);
1419
+ throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path41.join(".")}" (variable ${variable.id})`);
1420
1420
  }
1421
1421
  group[leaf] = token;
1422
1422
  flat.push({
1423
- path: path40.join("."),
1424
- cssVar: tokenPathToCssVar(path40),
1423
+ path: path41.join("."),
1424
+ cssVar: tokenPathToCssVar(path41),
1425
1425
  type: token.$type,
1426
1426
  value: token.$value
1427
1427
  });
@@ -1610,9 +1610,9 @@ function boundId(value) {
1610
1610
  return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
1611
1611
  }
1612
1612
  function resolveBinding(ctx, id) {
1613
- const path40 = ctx.pathById.get(id);
1614
- if (path40 === void 0) ctx.unresolved.add(id);
1615
- return path40;
1613
+ const path41 = ctx.pathById.get(id);
1614
+ if (path41 === void 0) ctx.unresolved.add(id);
1615
+ return path41;
1616
1616
  }
1617
1617
  function parseVariantProps(name) {
1618
1618
  if (!name.includes("=")) return void 0;
@@ -1647,8 +1647,8 @@ function walk(ctx, raw) {
1647
1647
  if (!isObject(paint) || paint["visible"] === false) continue;
1648
1648
  const id = boundId(paint);
1649
1649
  if (id !== void 0) {
1650
- const path40 = resolveBinding(ctx, id);
1651
- if (path40 !== void 0) tokens.add(path40);
1650
+ const path41 = resolveBinding(ctx, id);
1651
+ if (path41 !== void 0) tokens.add(path41);
1652
1652
  } else if (typeof paint["color"] === "string") {
1653
1653
  ctx.hardcoded.push({ node: name, property, value: paint["color"] });
1654
1654
  }
@@ -1656,8 +1656,8 @@ function walk(ctx, raw) {
1656
1656
  }
1657
1657
  const radiusId = boundId(raw["cornerRadius"]);
1658
1658
  if (radiusId !== void 0) {
1659
- const path40 = resolveBinding(ctx, radiusId);
1660
- if (path40 !== void 0) tokens.add(path40);
1659
+ const path41 = resolveBinding(ctx, radiusId);
1660
+ if (path41 !== void 0) tokens.add(path41);
1661
1661
  } else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
1662
1662
  ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
1663
1663
  }
@@ -1667,10 +1667,10 @@ function walk(ctx, raw) {
1667
1667
  layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
1668
1668
  const gapId = boundId(raw["itemSpacing"]);
1669
1669
  if (gapId !== void 0) {
1670
- const path40 = resolveBinding(ctx, gapId);
1671
- if (path40 !== void 0) {
1672
- layout.gap = path40;
1673
- tokens.add(path40);
1670
+ const path41 = resolveBinding(ctx, gapId);
1671
+ if (path41 !== void 0) {
1672
+ layout.gap = path41;
1673
+ tokens.add(path41);
1674
1674
  }
1675
1675
  } else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
1676
1676
  ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
@@ -1679,10 +1679,10 @@ function walk(ctx, raw) {
1679
1679
  for (const field of PADDING_FIELDS) {
1680
1680
  const id = boundId(raw[field]);
1681
1681
  if (id !== void 0) {
1682
- const path40 = resolveBinding(ctx, id);
1683
- if (path40 !== void 0) {
1684
- paddingPaths.push(path40);
1685
- tokens.add(path40);
1682
+ const path41 = resolveBinding(ctx, id);
1683
+ if (path41 !== void 0) {
1684
+ paddingPaths.push(path41);
1685
+ tokens.add(path41);
1686
1686
  }
1687
1687
  } else if (typeof raw[field] === "number" && raw[field] !== 0) {
1688
1688
  ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
@@ -3498,6 +3498,7 @@ function isCollection(bytes) {
3498
3498
  }
3499
3499
  function nameTableStrings(view, bytes, nameOffset, nameLength) {
3500
3500
  const out = /* @__PURE__ */ new Map();
3501
+ const rank = /* @__PURE__ */ new Map();
3501
3502
  if (nameOffset + 6 > bytes.length) return out;
3502
3503
  const count = view.getUint16(nameOffset + 2);
3503
3504
  const stringOffset = nameOffset + view.getUint16(nameOffset + 4);
@@ -3511,14 +3512,21 @@ function nameTableStrings(view, bytes, nameOffset, nameLength) {
3511
3512
  const offset = stringOffset + view.getUint16(rec + 10);
3512
3513
  if (offset + length > bytes.length || offset + length > nameOffset + nameLength) continue;
3513
3514
  const slice = bytes.subarray(offset, offset + length);
3514
- const isUtf16 = platformId === 3 || platformId === 0 || encodingId === 1;
3515
+ const languageId = view.getUint16(rec + 4);
3515
3516
  let value = "";
3516
- if (isUtf16) {
3517
+ if (platformId === 3 || platformId === 0) {
3517
3518
  for (let j = 0; j + 1 < slice.length; j += 2) value += String.fromCharCode(slice[j] << 8 | slice[j + 1]);
3518
- } else {
3519
+ } else if (platformId === 1 && encodingId === 0) {
3519
3520
  for (const b of slice) value += String.fromCharCode(b);
3521
+ } else {
3522
+ continue;
3523
+ }
3524
+ if (value === "") continue;
3525
+ const score = platformId === 3 && languageId === 1033 ? 5 : platformId === 0 ? 4 : platformId === 1 && languageId === 0 ? 3 : platformId === 3 ? 2 : 1;
3526
+ if (score > (rank.get(nameId) ?? 0)) {
3527
+ rank.set(nameId, score);
3528
+ out.set(nameId, value);
3520
3529
  }
3521
- if (value !== "" && !out.has(nameId)) out.set(nameId, value);
3522
3530
  }
3523
3531
  return out;
3524
3532
  }
@@ -3598,15 +3606,151 @@ var init_font_collection = __esm({
3598
3606
  }
3599
3607
  });
3600
3608
 
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";
3609
+ // packages/verify/src/font-discovery.ts
3610
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
3604
3611
  import os2 from "node:os";
3605
3612
  import path10 from "node:path";
3613
+ function weightFromSubfamily(subfamily) {
3614
+ for (const [re, w] of WEIGHT_TOKENS) if (re.test(subfamily)) return w;
3615
+ return void 0;
3616
+ }
3617
+ function systemFaceRefusal(family) {
3618
+ if (/^\.?SF([ -]|$)/i.test(family) || /^San Francisco/i.test(family) || /^\.?Apple/i.test(family) || /^\.?New York$/i.test(family)) {
3619
+ 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).";
3620
+ }
3621
+ if (/^Segoe UI Variable/i.test(family)) {
3622
+ 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).";
3623
+ }
3624
+ return void 0;
3625
+ }
3626
+ function faceAt(bytes, view, dirOffset, file, faceIndex) {
3627
+ if (dirOffset + 12 > bytes.length || !SFNT_VERSIONS.has(view.getUint32(dirOffset))) return null;
3628
+ const numTables = view.getUint16(dirOffset + 4);
3629
+ let names = /* @__PURE__ */ new Map();
3630
+ let os2Weight;
3631
+ let os2Italic = false;
3632
+ let variable = false;
3633
+ for (let t = 0; t < numTables; t++) {
3634
+ const rec = dirOffset + 12 + t * 16;
3635
+ if (rec + 16 > bytes.length) break;
3636
+ const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
3637
+ const offset = view.getUint32(rec + 8);
3638
+ const length = view.getUint32(rec + 12);
3639
+ if (tag === "name") names = nameTableStrings(view, bytes, offset, length);
3640
+ else if (tag === "OS/2" && offset + 64 <= bytes.length) {
3641
+ const w = view.getUint16(offset + 4);
3642
+ if (w >= 1 && w <= 1e3) os2Weight = w;
3643
+ os2Italic = (view.getUint16(offset + 62) & 1) !== 0;
3644
+ } else if (tag === "fvar") variable = true;
3645
+ }
3646
+ const family = names.get(16) ?? names.get(1);
3647
+ if (family === void 0 || family === "") return null;
3648
+ const subfamily = names.get(17) ?? names.get(2) ?? "";
3649
+ const italic = /italic|oblique|inclined|slanted/i.test(subfamily) || os2Italic;
3650
+ const weight = weightFromSubfamily(subfamily) ?? os2Weight ?? 400;
3651
+ return { family, subfamily, weight, italic, variable, file, ...faceIndex !== void 0 ? { faceIndex } : {} };
3652
+ }
3653
+ function facesInFile(file) {
3654
+ try {
3655
+ const bytes = new Uint8Array(readFileSync3(file));
3656
+ if (bytes.length < 12) return [];
3657
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
3658
+ if (isCollection(bytes)) {
3659
+ const numFonts = view.getUint32(8);
3660
+ const faces = [];
3661
+ for (let i = 0; i < numFonts; i++) {
3662
+ const f = faceAt(bytes, view, view.getUint32(12 + i * 4), file, i);
3663
+ if (f !== null) faces.push(f);
3664
+ }
3665
+ return faces;
3666
+ }
3667
+ const single = faceAt(bytes, view, 0, file);
3668
+ return single === null ? [] : [single];
3669
+ } catch {
3670
+ return [];
3671
+ }
3672
+ }
3673
+ function systemFontDirs() {
3674
+ const env = process.env["TENDRIL_SYSTEM_FONT_DIRS"];
3675
+ if (env !== void 0 && env !== "") return env.split(path10.delimiter).filter((d) => d !== "" && existsSync6(d));
3676
+ const home = os2.homedir();
3677
+ const dirs = process.platform === "darwin" ? ["/System/Library/Fonts", "/Library/Fonts", path10.join(home, "Library", "Fonts")] : process.platform === "win32" ? [
3678
+ path10.join(process.env["WINDIR"] ?? "C:\\Windows", "Fonts"),
3679
+ ...process.env["LOCALAPPDATA"] !== void 0 ? [path10.join(process.env["LOCALAPPDATA"], "Microsoft", "Windows", "Fonts")] : []
3680
+ ] : ["/usr/share/fonts", "/usr/local/share/fonts", path10.join(home, ".fonts"), path10.join(home, ".local", "share", "fonts")];
3681
+ return dirs.filter((d) => existsSync6(d));
3682
+ }
3683
+ function discoverSystemFaces(dirs = systemFontDirs(), depth = 3) {
3684
+ const seen = /* @__PURE__ */ new Set();
3685
+ const walk2 = (dirList, remaining) => {
3686
+ const faces = [];
3687
+ for (const dir of dirList) {
3688
+ let entries;
3689
+ try {
3690
+ entries = readdirSync2(dir, { withFileTypes: true });
3691
+ } catch {
3692
+ continue;
3693
+ }
3694
+ for (const e of entries) {
3695
+ const full = path10.join(dir, e.name);
3696
+ if (e.isDirectory()) {
3697
+ if (remaining > 1) faces.push(...walk2([full], remaining - 1));
3698
+ } else if (FONT_EXTENSIONS.has(path10.extname(e.name).toLowerCase())) {
3699
+ let key = full;
3700
+ try {
3701
+ key = realpathSync2(full);
3702
+ } catch {
3703
+ }
3704
+ if (seen.has(key)) continue;
3705
+ seen.add(key);
3706
+ faces.push(...facesInFile(full));
3707
+ }
3708
+ }
3709
+ }
3710
+ return faces;
3711
+ };
3712
+ return walk2(dirs, depth);
3713
+ }
3714
+ function systemFacesForFamily(family, dirs) {
3715
+ const want = normalize(family);
3716
+ return discoverSystemFaces(dirs).filter((f) => normalize(f.family) === want);
3717
+ }
3718
+ var WEIGHT_TOKENS, SFNT_VERSIONS, FONT_EXTENSIONS, normalize;
3719
+ var init_font_discovery = __esm({
3720
+ "packages/verify/src/font-discovery.ts"() {
3721
+ "use strict";
3722
+ init_font_collection();
3723
+ WEIGHT_TOKENS = [
3724
+ [/extra\s*light|ultra\s*light/i, 200],
3725
+ [/extra\s*bold|ultra\s*bold/i, 800],
3726
+ [/semi\s*bold|demi\s*bold|demi\b/i, 600],
3727
+ [/\bthin\b|\bhairline\b/i, 100],
3728
+ [/\blight\b/i, 300],
3729
+ [/\bmedium\b/i, 500],
3730
+ [/\bbold\b/i, 700],
3731
+ [/\bblack\b|\bheavy\b/i, 900],
3732
+ [/\bregular\b|\bnormal\b|\bbook\b|\broman\b/i, 400]
3733
+ ];
3734
+ SFNT_VERSIONS = /* @__PURE__ */ new Set([
3735
+ 65536,
3736
+ 1330926671,
3737
+ 1953658213
3738
+ /* true */
3739
+ ]);
3740
+ FONT_EXTENSIONS = /* @__PURE__ */ new Set([".ttf", ".otf", ".ttc"]);
3741
+ normalize = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
3742
+ }
3743
+ });
3744
+
3745
+ // packages/verify/src/font-resolve.ts
3746
+ import { createHash } from "node:crypto";
3747
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
3748
+ import os3 from "node:os";
3749
+ import path11 from "node:path";
3606
3750
  function fontCacheDir() {
3607
3751
  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");
3752
+ if (env !== void 0 && env !== "") return path11.resolve(env);
3753
+ return path11.join(os3.homedir(), ".tendril", "fonts");
3610
3754
  }
3611
3755
  function normalizeFontLicense(value) {
3612
3756
  return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
@@ -3671,29 +3815,29 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
3671
3815
  }
3672
3816
  const bytes = new Uint8Array(await fileRes.arrayBuffer());
3673
3817
  const sha256 = createHash("sha256").update(bytes).digest("hex");
3674
- const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
3818
+ const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
3675
3819
  writeFileSync3(file, bytes);
3676
3820
  resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
3677
3821
  } catch (err) {
3678
3822
  failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
3679
3823
  }
3680
3824
  }
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) }));
3825
+ const mPath = path11.join(cacheDir, "manifest.json");
3826
+ const prior = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
3827
+ const portable2 = resolved.map((m) => ({ ...m, file: path11.basename(m.file) }));
3684
3828
  const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
3685
3829
  if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
3686
3830
  `);
3687
3831
  return { resolved, failures };
3688
3832
  }
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();
3833
+ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex, provenance = "local") {
3834
+ const src = path11.resolve(filePath);
3835
+ if (!existsSync7(src)) throw new Error(`font file not found: ${src}`);
3836
+ const ext = path11.extname(src).toLowerCase();
3693
3837
  if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
3694
3838
  throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
3695
3839
  }
3696
- let bytes = new Uint8Array(readFileSync3(src));
3840
+ let bytes = new Uint8Array(readFileSync4(src));
3697
3841
  if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
3698
3842
  let storedExt = ext;
3699
3843
  if (isCollection(bytes)) {
@@ -3703,7 +3847,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
3703
3847
  const all = listCollectionFaces(bytes);
3704
3848
  const shown = (candidates.length > 0 ? candidates : all).map((f) => ` --face ${f.index} ${f.family ?? "(unnamed)"}${f.subfamily !== void 0 ? ` ${f.subfamily}` : ""}`).join("\n");
3705
3849
  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>:
3850
+ `${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
3851
  ${shown}`
3708
3852
  );
3709
3853
  }
@@ -3712,20 +3856,20 @@ ${shown}`
3712
3856
  }
3713
3857
  mkdirSync2(cacheDir, { recursive: true });
3714
3858
  const sha256 = createHash("sha256").update(bytes).digest("hex");
3715
- const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
3859
+ const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
3716
3860
  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) }];
3861
+ const face = { family, weight, source: `${provenance}:${path11.basename(src)}`, sha256, file, license: "unknown" };
3862
+ const mPath = path11.join(cacheDir, "manifest.json");
3863
+ const prior = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
3864
+ const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path11.basename(file) }];
3721
3865
  writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
3722
3866
  `);
3723
3867
  return face;
3724
3868
  }
3725
3869
  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")) : [];
3870
+ const lock = JSON.parse(readFileSync4(lockPath, "utf8"));
3871
+ const mPath = path11.join(cacheDir, "manifest.json");
3872
+ const manifest = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
3729
3873
  return lock.map((l) => {
3730
3874
  const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
3731
3875
  if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
@@ -3733,11 +3877,11 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
3733
3877
  });
3734
3878
  }
3735
3879
  function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3736
- const mPath = path10.join(cacheDir, "manifest.json");
3737
- if (!existsSync6(mPath)) return [];
3880
+ const mPath = path11.join(cacheDir, "manifest.json");
3881
+ if (!existsSync7(mPath)) return [];
3738
3882
  let entries;
3739
3883
  try {
3740
- entries = JSON.parse(readFileSync3(mPath, "utf8"));
3884
+ entries = JSON.parse(readFileSync4(mPath, "utf8"));
3741
3885
  } catch {
3742
3886
  return [];
3743
3887
  }
@@ -3751,8 +3895,8 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3751
3895
  return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
3752
3896
  }
3753
3897
  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")) : [];
3898
+ const mPath = path11.join(cacheDir, "manifest.json");
3899
+ const manifest = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
3756
3900
  const wanted = new Set(families.map((f) => f.toLowerCase()));
3757
3901
  return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
3758
3902
  family: f.family,
@@ -3765,11 +3909,11 @@ function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
3765
3909
  }));
3766
3910
  }
3767
3911
  function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
3768
- const mPath = path10.join(cacheDir, "manifest.json");
3769
- if (!existsSync6(mPath)) return [];
3912
+ const mPath = path11.join(cacheDir, "manifest.json");
3913
+ if (!existsSync7(mPath)) return [];
3770
3914
  let entries;
3771
3915
  try {
3772
- entries = JSON.parse(readFileSync3(mPath, "utf8"));
3916
+ entries = JSON.parse(readFileSync4(mPath, "utf8"));
3773
3917
  } catch {
3774
3918
  return [];
3775
3919
  }
@@ -3778,31 +3922,83 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
3778
3922
  ).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
3779
3923
  }
3780
3924
  function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3781
- const mPath = path10.join(cacheDir, "manifest.json");
3782
- if (!existsSync6(mPath)) return [];
3925
+ const mPath = path11.join(cacheDir, "manifest.json");
3926
+ if (!existsSync7(mPath)) return [];
3783
3927
  let entries;
3784
3928
  try {
3785
- entries = JSON.parse(readFileSync3(mPath, "utf8"));
3929
+ entries = JSON.parse(readFileSync4(mPath, "utf8"));
3786
3930
  } catch {
3787
3931
  return [];
3788
3932
  }
3789
3933
  const byFamily = /* @__PURE__ */ new Map();
3790
3934
  for (const e of entries) {
3791
3935
  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;
3936
+ const file = path11.isAbsolute(e.file) && existsSync7(e.file) ? e.file : path11.resolve(cacheDir, path11.basename(e.file));
3937
+ if (!existsSync7(file)) continue;
3938
+ if (createHash("sha256").update(readFileSync4(file)).digest("hex") !== e.sha256) continue;
3795
3939
  const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
3796
3940
  set.add(e.weight);
3797
3941
  byFamily.set(e.family, set);
3798
3942
  }
3799
3943
  return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
3800
3944
  }
3945
+ function addSystemFamily(family, opts = {}) {
3946
+ const refusal = systemFaceRefusal(family);
3947
+ if (refusal !== void 0) return { added: [], skipped: [], overwrote: [], refusal };
3948
+ const plainStyle = (sub) => weightFromSubfamily(sub) !== void 0 || /^(regular|plain|)$/i.test(sub.trim());
3949
+ const faces = [...systemFacesForFamily(family, opts.dirs)].sort((a, b) => {
3950
+ const pa = plainStyle(a.subfamily) ? 0 : 1;
3951
+ const pb = plainStyle(b.subfamily) ? 0 : 1;
3952
+ if (pa !== pb) return pa - pb;
3953
+ return a.subfamily.localeCompare(b.subfamily) || a.file.localeCompare(b.file) || (a.faceIndex ?? 0) - (b.faceIndex ?? 0);
3954
+ });
3955
+ const added = [];
3956
+ const skipped = [];
3957
+ const overwrote = [];
3958
+ const cacheDir = opts.cacheDir ?? DEFAULT_FONT_CACHE;
3959
+ const manifestFile = path11.join(cacheDir, "manifest.json");
3960
+ const prior = existsSync7(manifestFile) ? JSON.parse(readFileSync4(manifestFile, "utf8")) : [];
3961
+ const taken = /* @__PURE__ */ new Set();
3962
+ for (const face of faces) {
3963
+ const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
3964
+ if (opts.weights !== void 0 && !opts.weights.includes(face.weight)) continue;
3965
+ if (face.italic) {
3966
+ skip("italic \u2014 the cache cannot represent italic faces end to end yet; the upright face serves the family");
3967
+ continue;
3968
+ }
3969
+ if (face.variable) {
3970
+ 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");
3971
+ continue;
3972
+ }
3973
+ if (/condensed|narrow|compressed|expanded|extended/i.test(face.subfamily)) {
3974
+ skip("width variant \u2014 a Condensed/Expanded face is a different design, not a weight of this family");
3975
+ continue;
3976
+ }
3977
+ if (taken.has(face.weight)) {
3978
+ skip(`another face already provided weight ${face.weight}`);
3979
+ continue;
3980
+ }
3981
+ taken.add(face.weight);
3982
+ const existing = prior.find((p) => p.family.toLowerCase() === face.family.toLowerCase() && p.weight === face.weight);
3983
+ if (existing !== void 0 && !existing.source.startsWith("system:")) {
3984
+ overwrote.push({
3985
+ family: face.family,
3986
+ weight: face.weight,
3987
+ priorSource: existing.source,
3988
+ priorSha256: existing.sha256,
3989
+ priorLicense: normalizeFontLicense(existing.license)
3990
+ });
3991
+ }
3992
+ added.push({ ...addLocalFont(face.family, face.weight, face.file, cacheDir, face.faceIndex, "system"), subfamily: face.subfamily || "Regular" });
3993
+ }
3994
+ return { added, skipped, overwrote };
3995
+ }
3801
3996
  var DEFAULT_FONT_CACHE, UA, FONT_LICENSES, GOOGLE_LICENSE_IDS, GOOGLE_FONT_FILE_PREFIX;
3802
3997
  var init_font_resolve = __esm({
3803
3998
  "packages/verify/src/font-resolve.ts"() {
3804
3999
  "use strict";
3805
4000
  init_font_collection();
4001
+ init_font_discovery();
3806
4002
  DEFAULT_FONT_CACHE = fontCacheDir();
3807
4003
  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
4004
  FONT_LICENSES = ["OFL-1.1", "Apache-2.0", "UFL-1.0", "proprietary", "unknown"];
@@ -3818,17 +4014,17 @@ var init_font_resolve = __esm({
3818
4014
 
3819
4015
  // packages/verify/src/font-faces.ts
3820
4016
  import { createHash as createHash2 } from "node:crypto";
3821
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
3822
- import path11 from "node:path";
4017
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
4018
+ import path12 from "node:path";
3823
4019
  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));
4020
+ if (!existsSync8(manifestPath2)) return { groups: [], shared: false };
4021
+ const claimed = JSON.parse(readFileSync5(manifestPath2, "utf8"));
4022
+ const resolveFile = (f) => path12.isAbsolute(f) && existsSync8(f) ? f : path12.resolve(path12.dirname(manifestPath2), path12.basename(f));
3827
4023
  const byFile = /* @__PURE__ */ new Map();
3828
4024
  for (const f of claimed) {
3829
4025
  const file = resolveFile(f.file);
3830
- if (!existsSync7(file)) continue;
3831
- if (createHash2("sha256").update(readFileSync4(file)).digest("hex") !== f.sha256) continue;
4026
+ if (!existsSync8(file)) continue;
4027
+ if (createHash2("sha256").update(readFileSync5(file)).digest("hex") !== f.sha256) continue;
3832
4028
  const k = `${f.family}:${f.file}`;
3833
4029
  const e = byFile.get(k) ?? { family: f.family, weights: [], file };
3834
4030
  e.weights.push(f.weight);
@@ -3837,14 +4033,14 @@ function injectedGroups(manifestPath2) {
3837
4033
  const groups = [...byFile.values()];
3838
4034
  return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
3839
4035
  }
3840
- function fontFaceCss(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
4036
+ function fontFaceCss(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
3841
4037
  const { groups, shared } = injectedGroups(manifestPath2);
3842
4038
  return groups.map((e) => {
3843
4039
  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'); }`;
4040
+ 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
4041
  }).join("\n");
3846
4042
  }
3847
- function injectedFamilyWeights(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
4043
+ function injectedFamilyWeights(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
3848
4044
  const { groups, shared } = injectedGroups(manifestPath2);
3849
4045
  const out = /* @__PURE__ */ new Map();
3850
4046
  for (const g of groups) {
@@ -3869,17 +4065,17 @@ var init_font_faces = __esm({
3869
4065
  });
3870
4066
 
3871
4067
  // 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";
4068
+ import { readFileSync as readFileSync6, readdirSync as readdirSync3, existsSync as existsSync9, writeFileSync as writeFileSync4 } from "node:fs";
4069
+ import path13 from "node:path";
3874
4070
  import { build as build2 } from "esbuild";
3875
4071
  import postcss from "postcss";
3876
4072
  import tailwindcss from "tailwindcss";
3877
4073
  import { chromium as chromium2 } from "playwright-core";
3878
4074
  function fontWeightsByFamily() {
3879
- const mPath = path12.join(fontCacheDir(), "manifest.json");
4075
+ const mPath = path13.join(fontCacheDir(), "manifest.json");
3880
4076
  const out = /* @__PURE__ */ new Map();
3881
- if (!existsSync8(mPath)) return out;
3882
- for (const f of JSON.parse(readFileSync5(mPath, "utf8")))
4077
+ if (!existsSync9(mPath)) return out;
4078
+ for (const f of JSON.parse(readFileSync6(mPath, "utf8")))
3883
4079
  out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
3884
4080
  return out;
3885
4081
  }
@@ -3899,7 +4095,7 @@ var init_admission = __esm({
3899
4095
  });
3900
4096
 
3901
4097
  // packages/verify/src/tasks.ts
3902
- import path13 from "node:path";
4098
+ import path14 from "node:path";
3903
4099
  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
4100
  var init_tasks = __esm({
3905
4101
  "packages/verify/src/tasks.ts"() {
@@ -4059,7 +4255,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4059
4255
  ];
4060
4256
  TASKS = {
4061
4257
  calendar: {
4062
- set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
4258
+ set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
4063
4259
  entry: "Calendar.tsx",
4064
4260
  configs: CALENDAR_CONFIGS,
4065
4261
  systemApi: CALENDAR_API,
@@ -4067,7 +4263,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4067
4263
  prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
4068
4264
  },
4069
4265
  "shadcn-button": {
4070
- set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
4266
+ set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
4071
4267
  entry: "Button.tsx",
4072
4268
  configs: BUTTON_CONFIGS,
4073
4269
  systemApi: BUTTON_API,
@@ -4075,7 +4271,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4075
4271
  prelude: { controls: ["> *"], textInputs: [] }
4076
4272
  },
4077
4273
  combobox: {
4078
- set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
4274
+ set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
4079
4275
  entry: "ComboBox.tsx",
4080
4276
  configs: COMBO_CONFIGS,
4081
4277
  systemApi: COMBO_API,
@@ -4083,7 +4279,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4083
4279
  prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
4084
4280
  },
4085
4281
  modal: {
4086
- set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
4282
+ set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
4087
4283
  entry: "Modal.tsx",
4088
4284
  configs: MODAL_CONFIGS,
4089
4285
  systemApi: MODAL_API,
@@ -4101,8 +4297,8 @@ __export(behavior_exports, {
4101
4297
  compileMount: () => compileMount,
4102
4298
  recordingIsDark: () => recordingIsDark
4103
4299
  });
4104
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
4105
- import path14 from "node:path";
4300
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
4301
+ import path15 from "node:path";
4106
4302
  import { build as build3 } from "esbuild";
4107
4303
  import { chromium as chromium3 } from "playwright-core";
4108
4304
  import { PNG as PNG2 } from "pngjs";
@@ -4111,12 +4307,12 @@ function getFontFaces() {
4111
4307
  return _fontFaces;
4112
4308
  }
4113
4309
  async function compileMount(task, bundleDir) {
4114
- const entryTsx = path14.join(bundleDir, task.entry);
4115
- if (!existsSync9(entryTsx)) return { error: `${task.entry} missing` };
4310
+ const entryTsx = path15.join(bundleDir, task.entry);
4311
+ if (!existsSync10(entryTsx)) return { error: `${task.entry} missing` };
4116
4312
  const mountSrc = `
4117
4313
  import { createElement } from "react";
4118
4314
  import { createRoot } from "react-dom/client";
4119
- import * as B from ${JSON.stringify(path14.resolve(entryTsx))};
4315
+ import * as B from ${JSON.stringify(path15.resolve(entryTsx))};
4120
4316
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
4121
4317
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
4122
4318
  // Callbacks cannot ride the JSON config: specs NAME spy props and the
@@ -4397,10 +4593,10 @@ async function runSteps(page, spec, renderPose) {
4397
4593
  function recordingIsDark(task) {
4398
4594
  const rep = task.configs[0]?.rep;
4399
4595
  if (rep === void 0) return false;
4400
- const f = path14.join(task.set, rep, "get_screenshot.json");
4401
- if (!existsSync9(f)) return false;
4596
+ const f = path15.join(task.set, rep, "get_screenshot.json");
4597
+ if (!existsSync10(f)) return false;
4402
4598
  try {
4403
- const env = JSON.parse(readFileSync6(f, "utf8")).content.find((c) => c.type === "image");
4599
+ const env = JSON.parse(readFileSync7(f, "utf8")).content.find((c) => c.type === "image");
4404
4600
  if (env?.data === void 0) return false;
4405
4601
  const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
4406
4602
  let sum = 0;
@@ -4472,7 +4668,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
4472
4668
  const deadlineMs = timeoutMs + 1e4;
4473
4669
  const js = await compileMount(task, bundleDir);
4474
4670
  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");
4671
+ const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) => existsSync10(f)).map((f) => readFileSync7(f, "utf8")).join("\n");
4476
4672
  const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
4477
4673
  const browser = await chromium3.connect(server.wsEndpoint());
4478
4674
  const results = [];
@@ -4615,8 +4811,8 @@ var init_behavior = __esm({
4615
4811
  });
4616
4812
 
4617
4813
  // 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";
4814
+ import { readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "node:fs";
4815
+ import path16 from "node:path";
4620
4816
  function definedVars(tokensCss) {
4621
4817
  if (tokensCss === void 0) return void 0;
4622
4818
  const names = /* @__PURE__ */ new Set();
@@ -4625,19 +4821,19 @@ function definedVars(tokensCss) {
4625
4821
  }
4626
4822
  function recordedTokenMapState(setDir, reps) {
4627
4823
  const readMap = (file) => {
4628
- if (!existsSync10(file)) return void 0;
4824
+ if (!existsSync11(file)) return void 0;
4629
4825
  try {
4630
- const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync7(file, "utf8"))) || "{}");
4826
+ const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync8(file, "utf8"))) || "{}");
4631
4827
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4632
4828
  } catch {
4633
4829
  return {};
4634
4830
  }
4635
4831
  };
4636
- const setLevel = readMap(path15.join(setDir, "get_variable_defs.json"));
4832
+ const setLevel = readMap(path16.join(setDir, "get_variable_defs.json"));
4637
4833
  if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
4638
4834
  let recorded = false;
4639
4835
  for (const rep of reps) {
4640
- const m = readMap(path15.join(setDir, rep, "get_variable_defs.json"));
4836
+ const m = readMap(path16.join(setDir, rep, "get_variable_defs.json"));
4641
4837
  if (m === void 0) continue;
4642
4838
  recorded = true;
4643
4839
  if (Object.keys(m).length > 0) return "populated";
@@ -4702,11 +4898,11 @@ function fontStackFindings(sheets, coverage) {
4702
4898
  }
4703
4899
  async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
4704
4900
  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;
4901
+ const entryPath = path16.join(bundleDir, entry);
4902
+ const cssPath = path16.join(bundleDir, "styles.css");
4903
+ const tokensPath = path16.join(bundleDir, "tokens.css");
4904
+ const css = existsSync11(cssPath) ? readFileSync8(cssPath, "utf8") : "";
4905
+ const tokensCss = existsSync11(tokensPath) ? readFileSync8(tokensPath, "utf8") : void 0;
4710
4906
  findings.push(
4711
4907
  ...fontStackFindings(
4712
4908
  [
@@ -4716,11 +4912,11 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
4716
4912
  injectedFamilyWeights(fontManifest)
4717
4913
  )
4718
4914
  );
4719
- if (existsSync10(entryPath)) {
4915
+ if (existsSync11(entryPath)) {
4720
4916
  const workDir = newScratchDir("quality");
4721
4917
  try {
4722
- const tsxPath = path15.join(workDir, entry);
4723
- writeFileSync5(tsxPath, readFileSync7(entryPath, "utf8"));
4918
+ const tsxPath = path16.join(workDir, entry);
4919
+ writeFileSync5(tsxPath, readFileSync8(entryPath, "utf8"));
4724
4920
  for (const d of runTscStrict([tsxPath]).diagnostics) {
4725
4921
  findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
4726
4922
  }
@@ -4795,8 +4991,8 @@ var init_effect_geometry = __esm({
4795
4991
  });
4796
4992
 
4797
4993
  // 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";
4994
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
4995
+ import path17 from "node:path";
4800
4996
  import { build as build4 } from "esbuild";
4801
4997
  import { chromium as chromium4 } from "playwright-core";
4802
4998
  function getFontFaces2() {
@@ -4850,7 +5046,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
4850
5046
  }
4851
5047
  function metadataRoot(set, rep) {
4852
5048
  try {
4853
- const text = JSON.parse(readFileSync8(path16.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
5049
+ const text = JSON.parse(readFileSync9(path17.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
4854
5050
  return parseMetadataStructure(text);
4855
5051
  } catch {
4856
5052
  return void 0;
@@ -4905,19 +5101,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
4905
5101
  });
4906
5102
  }
4907
5103
  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");
5104
+ const text = JSON.parse(readFileSync9(path17.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
4909
5105
  const root = parseMetadataStructure(text);
4910
5106
  return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
4911
5107
  }
4912
5108
  function repRef(set, rep) {
4913
- const env = JSON.parse(readFileSync8(path16.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
5109
+ const env = JSON.parse(readFileSync9(path17.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
4914
5110
  return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
4915
5111
  }
4916
5112
  function repEffectExtents(set, rep) {
4917
- const file = path16.join(set, rep, "get_design_context.json");
4918
- if (!existsSync11(file)) return void 0;
5113
+ const file = path17.join(set, rep, "get_design_context.json");
5114
+ if (!existsSync12(file)) return void 0;
4919
5115
  try {
4920
- const text = JSON.parse(readFileSync8(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
5116
+ const text = JSON.parse(readFileSync9(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
4921
5117
  const extents = shadowExtents(text);
4922
5118
  return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
4923
5119
  } catch {
@@ -4928,13 +5124,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
4928
5124
  if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
4929
5125
  const CONFIGS2 = task.configs;
4930
5126
  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");
5127
+ const entryTsx = path17.join(bundleDir, task.entry);
5128
+ 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` }));
5129
+ const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
4934
5130
  const mountSrc = `
4935
5131
  import { createElement } from "react";
4936
5132
  import { createRoot } from "react-dom/client";
4937
- import * as B from ${JSON.stringify(path16.resolve(entryTsx))};
5133
+ import * as B from ${JSON.stringify(path17.resolve(entryTsx))};
4938
5134
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
4939
5135
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
4940
5136
  const root = document.getElementById("root");
@@ -5029,12 +5225,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
5029
5225
  return name === void 0 ? c : { ...c, name };
5030
5226
  });
5031
5227
  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));
5228
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
5229
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
5230
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
5035
5231
  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));
5232
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
5233
+ writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
5038
5234
  }
5039
5235
  }
5040
5236
  return {
@@ -5142,15 +5338,15 @@ var init_prelude = __esm({
5142
5338
  });
5143
5339
 
5144
5340
  // packages/verify/src/parity.ts
5145
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
5146
- import path17 from "node:path";
5341
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
5342
+ import path18 from "node:path";
5147
5343
  import { chromium as chromium6 } from "playwright-core";
5148
5344
  function getFontFaces3() {
5149
5345
  _fontFaces3 ??= fontFaceCss();
5150
5346
  return _fontFaces3;
5151
5347
  }
5152
- function hoverForcedConfigs(task) {
5153
- return task.configs.filter((c) => {
5348
+ function hoverForcedConfigs(authority) {
5349
+ return authority.filter((c) => {
5154
5350
  const forced = c.props["data-tendril-state"];
5155
5351
  return typeof forced === "string" && forced.split(/\s+/).includes("hover");
5156
5352
  });
@@ -5165,27 +5361,37 @@ function withoutHoverToken(props) {
5165
5361
  }
5166
5362
  return rest;
5167
5363
  }
5168
- async function checkHoverParity(task, bundleDir, opts = {}) {
5169
- const configs = hoverForcedConfigs(task);
5364
+ async function checkHoverParity(task, bundleDir, authority, opts = {}) {
5365
+ const configs = hoverForcedConfigs(authority);
5170
5366
  if (configs.length === 0) return [];
5171
5367
  const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
5172
5368
  const deadlineMs = timeoutMs + 1e4;
5173
5369
  const js = await compileMount(task, bundleDir);
5174
5370
  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");
5371
+ const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
5176
5372
  const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
5177
5373
  const browser = await chromium6.connect(server.wsEndpoint());
5178
5374
  const results = [];
5179
5375
  try {
5180
5376
  for (const cfg of configs) {
5181
- const shoot = async (props, realHover) => {
5377
+ const box = (() => {
5378
+ try {
5379
+ return repMeta(task.set, cfg.rep);
5380
+ } catch {
5381
+ return void 0;
5382
+ }
5383
+ })();
5384
+ 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}
5385
+ #root > *{min-width:${box.w}px;min-height:${box.h}px}`;
5386
+ const viewport = box === void 0 ? { width: 900, height: 700 } : { width: box.w + 48, height: box.h + 48 };
5387
+ const shoot = async (component, props, realHover) => {
5182
5388
  const html = `<!doctype html><html><head><meta charset="utf-8"><style>
5183
5389
  ${getFontFaces3()}
5184
5390
  ${css}
5185
5391
  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 } });
5392
+ ${rootCss}
5393
+ </style></head><body><div id="root"></div><script>window.__cfg=${JSON.stringify({ component, props })}</script><script>${js}</script></body></html>`;
5394
+ const page = await browser.newPage({ viewport });
5189
5395
  try {
5190
5396
  page.setDefaultTimeout(timeoutMs);
5191
5397
  await page.route("**/*", (route) => route.request().url().startsWith("data:") ? route.continue() : route.abort());
@@ -5199,15 +5405,30 @@ body{margin:0;padding:20px}
5199
5405
  } else {
5200
5406
  await page.waitForTimeout(400);
5201
5407
  }
5202
- return await page.screenshot({ clip: { x: 0, y: 0, width: 900, height: 700 } });
5408
+ return await page.screenshot({ clip: { x: 0, y: 0, width: viewport.width, height: viewport.height } });
5203
5409
  } finally {
5204
5410
  await page.close();
5205
5411
  }
5206
5412
  };
5207
5413
  const work = (async () => {
5208
- const forcedShot = await shoot(cfg.props, false);
5414
+ const adapter = task.configs.find((c) => c.rep === cfg.rep);
5415
+ if (adapter === void 0) {
5416
+ 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" };
5417
+ }
5418
+ const forcedShot = await shoot(adapter.component, cfg.props, false);
5209
5419
  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);
5420
+ if (JSON.stringify(adapter.props) !== JSON.stringify(cfg.props)) {
5421
+ const adapterShot = await shoot(adapter.component, adapter.props, false);
5422
+ if (!Buffer.isBuffer(adapterShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `adapter mount: ${adapterShot.error}` };
5423
+ if (Buffer.compare(adapterShot, forcedShot) !== 0) {
5424
+ return {
5425
+ id: `parity:${cfg.rep}`,
5426
+ pass: false,
5427
+ 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`
5428
+ };
5429
+ }
5430
+ }
5431
+ const realShot = await shoot(adapter.component, withoutHoverToken(cfg.props), true);
5211
5432
  if (!Buffer.isBuffer(realShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `real-hover mount: ${realShot.error}` };
5212
5433
  if (Buffer.compare(forcedShot, realShot) !== 0) {
5213
5434
  return {
@@ -5247,13 +5468,14 @@ var init_parity = __esm({
5247
5468
  init_behavior();
5248
5469
  init_font_faces();
5249
5470
  init_mount_limits();
5471
+ init_bundle_score();
5250
5472
  }
5251
5473
  });
5252
5474
 
5253
5475
  // packages/verify/src/composition.ts
5254
5476
  import { createRequire as createRequire2 } from "node:module";
5255
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
5256
- import path18 from "node:path";
5477
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
5478
+ import path19 from "node:path";
5257
5479
  import { build as build6 } from "esbuild";
5258
5480
  import { chromium as chromium7 } from "playwright-core";
5259
5481
  function getFontFaces4() {
@@ -5261,9 +5483,9 @@ function getFontFaces4() {
5261
5483
  return _fontFaces4;
5262
5484
  }
5263
5485
  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"));
5486
+ const entryTsx = path19.join(bundleDir, task.entry);
5487
+ if (!existsSync14(entryTsx)) return { error: `${task.entry} missing` };
5488
+ const requireFromVerify = createRequire2(path19.join(VERIFY_PKG_DIR, "package.json"));
5267
5489
  let realJsxPath;
5268
5490
  try {
5269
5491
  realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
@@ -5274,7 +5496,7 @@ async function compileInstrumentedMount(task, bundleDir) {
5274
5496
  import { createElement } from "react";
5275
5497
  import { createRoot } from "react-dom/client";
5276
5498
  import { __registerParts } from "react/jsx-runtime";
5277
- import * as B from ${JSON.stringify(path18.resolve(entryTsx))};
5499
+ import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
5278
5500
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
5279
5501
  const pairs: Array<[unknown, string]> = [];
5280
5502
  for (const name of cfg.partComponents) {
@@ -5329,7 +5551,7 @@ function expectedParts(task, roles, mainSlug) {
5329
5551
  }
5330
5552
  function interiorRegions(setDir, roles) {
5331
5553
  const mains = roles.main;
5332
- const withInterior = mains.filter((m) => existsSync13(path18.join(setDir, m, "get_metadata_interior.json")));
5554
+ const withInterior = mains.filter((m) => existsSync14(path19.join(setDir, m, "get_metadata_interior.json")));
5333
5555
  if (withInterior.length === 0) {
5334
5556
  return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
5335
5557
  }
@@ -5346,7 +5568,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
5346
5568
  if (typeof js !== "string") {
5347
5569
  return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
5348
5570
  }
5349
- const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
5571
+ const css = ["tokens.css", "styles.css"].map((f) => path19.join(bundleDir, f)).filter((f) => existsSync14(f)).map((f) => readFileSync11(f, "utf8")).join("\n");
5350
5572
  const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
5351
5573
  const browser = await chromium7.connect(server.wsEndpoint());
5352
5574
  const PAD = 4;
@@ -5451,7 +5673,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
5451
5673
  const deadlineMs = timeoutMs + 1e4;
5452
5674
  const js = await compileInstrumentedMount(task, bundleDir);
5453
5675
  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");
5676
+ const css = ["tokens.css", "styles.css"].map((f) => path19.join(bundleDir, f)).filter((f) => existsSync14(f)).map((f) => readFileSync11(f, "utf8")).join("\n");
5455
5677
  const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
5456
5678
  const browser = await chromium7.connect(server.wsEndpoint());
5457
5679
  try {
@@ -5541,17 +5763,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
5541
5763
  });
5542
5764
 
5543
5765
  // packages/verify/src/occlusion.ts
5544
- import { existsSync as existsSync14 } from "node:fs";
5545
- import path19 from "node:path";
5766
+ import { existsSync as existsSync15 } from "node:fs";
5767
+ import path20 from "node:path";
5546
5768
  import { build as build7 } from "esbuild";
5547
5769
  import { chromium as chromium8 } from "playwright-core";
5548
5770
  async function compileTwoUp(task, bundleDir) {
5549
- const entryTsx = path19.join(bundleDir, task.entry);
5550
- if (!existsSync14(entryTsx)) return { error: `${task.entry} missing` };
5771
+ const entryTsx = path20.join(bundleDir, task.entry);
5772
+ if (!existsSync15(entryTsx)) return { error: `${task.entry} missing` };
5551
5773
  const src = `
5552
5774
  import { createElement } from "react";
5553
5775
  import { createRoot } from "react-dom/client";
5554
- import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
5776
+ import * as B from ${JSON.stringify(path20.resolve(entryTsx))};
5555
5777
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
5556
5778
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
5557
5779
  for (const id of ["first", "second"]) {
@@ -5720,6 +5942,7 @@ var init_src4 = __esm({
5720
5942
  init_prelude();
5721
5943
  init_mount_limits();
5722
5944
  init_font_collection();
5945
+ init_font_discovery();
5723
5946
  init_font_faces();
5724
5947
  init_font_resolve();
5725
5948
  init_paths();
@@ -5730,23 +5953,23 @@ var init_src4 = __esm({
5730
5953
  });
5731
5954
 
5732
5955
  // packages/cli/src/environment.ts
5733
- import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
5734
- import path20 from "node:path";
5956
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
5957
+ import path21 from "node:path";
5735
5958
  import { createHash as createHash3 } from "node:crypto";
5736
5959
  import { fileURLToPath as fileURLToPath5 } from "node:url";
5737
5960
  function cliVersion() {
5738
5961
  try {
5739
- return JSON.parse(readFileSync11(path20.join(path20.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
5962
+ return JSON.parse(readFileSync12(path21.join(path21.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
5740
5963
  } catch {
5741
5964
  return "dev";
5742
5965
  }
5743
5966
  }
5744
5967
  function environmentStamp(taskFamilies) {
5745
- const manifestPath2 = path20.join(fontCacheDir(), "manifest.json");
5968
+ const manifestPath2 = path21.join(fontCacheDir(), "manifest.json");
5746
5969
  let fontsHash = null;
5747
- if (existsSync15(manifestPath2)) {
5970
+ if (existsSync16(manifestPath2)) {
5748
5971
  try {
5749
- const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
5972
+ const entries = JSON.parse(readFileSync12(manifestPath2, "utf8"));
5750
5973
  const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
5751
5974
  const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
5752
5975
  fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
@@ -5790,8 +6013,8 @@ var init_describe = __esm({
5790
6013
  });
5791
6014
 
5792
6015
  // packages/cli/src/env.ts
5793
- import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
5794
- import path21 from "node:path";
6016
+ import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
6017
+ import path22 from "node:path";
5795
6018
  function parseEnv(content) {
5796
6019
  const entries = /* @__PURE__ */ new Map();
5797
6020
  for (const line of content.split("\n")) {
@@ -5803,9 +6026,9 @@ function parseEnv(content) {
5803
6026
  function resolveCredential(name) {
5804
6027
  const fromProcess = process.env[name];
5805
6028
  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);
6029
+ const envPath = path22.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
6030
+ if (!existsSync17(envPath)) return void 0;
6031
+ return parseEnv(readFileSync13(envPath, "utf8")).get(name);
5809
6032
  }
5810
6033
  var init_env = __esm({
5811
6034
  "packages/cli/src/env.ts"() {
@@ -5865,17 +6088,17 @@ var init_output = __esm({
5865
6088
  });
5866
6089
 
5867
6090
  // packages/cli/src/entitlement.ts
5868
- import { chmodSync, existsSync as existsSync17, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
6091
+ import { chmodSync, existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
5869
6092
  import crypto from "node:crypto";
5870
- import os3 from "node:os";
5871
- import path22 from "node:path";
6093
+ import os4 from "node:os";
6094
+ import path23 from "node:path";
5872
6095
  function entitlementPath() {
5873
- return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path22.join(os3.homedir(), ".tendril", "entitlement.json");
6096
+ return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path23.join(os4.homedir(), ".tendril", "entitlement.json");
5874
6097
  }
5875
6098
  function readStoredEntitlement(file = entitlementPath()) {
5876
- if (!existsSync17(file)) return void 0;
6099
+ if (!existsSync18(file)) return void 0;
5877
6100
  try {
5878
- const parsed = JSON.parse(readFileSync13(file, "utf8"));
6101
+ const parsed = JSON.parse(readFileSync14(file, "utf8"));
5879
6102
  if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
5880
6103
  return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
5881
6104
  } catch {
@@ -5883,7 +6106,7 @@ function readStoredEntitlement(file = entitlementPath()) {
5883
6106
  }
5884
6107
  }
5885
6108
  function writeStoredEntitlement(stored, file = entitlementPath()) {
5886
- mkdirSync4(path22.dirname(file), { recursive: true });
6109
+ mkdirSync4(path23.dirname(file), { recursive: true });
5887
6110
  writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
5888
6111
  `);
5889
6112
  chmodSync(file, 384);
@@ -5968,9 +6191,9 @@ var init_entitlement = __esm({
5968
6191
 
5969
6192
  // packages/cli/src/commands/doctor.ts
5970
6193
  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";
6194
+ import { existsSync as existsSync19, readFileSync as readFileSync15, readdirSync as readdirSync4 } from "node:fs";
6195
+ import os5 from "node:os";
6196
+ import path24 from "node:path";
5974
6197
  function withDeadline(work, ms) {
5975
6198
  return Promise.race([
5976
6199
  work,
@@ -6030,19 +6253,19 @@ async function runDoctorChecks(options) {
6030
6253
  remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
6031
6254
  });
6032
6255
  }
6033
- const fontManifest = path23.join(fontCacheDir(), "manifest.json");
6256
+ const fontManifest = path24.join(fontCacheDir(), "manifest.json");
6034
6257
  checks.push(
6035
- existsSync18(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
6258
+ existsSync19(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync15(fontManifest, "utf8")).length} faces)` } : {
6036
6259
  name: "font-cache",
6037
6260
  ok: true,
6038
6261
  detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
6039
6262
  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
6263
  }
6041
6264
  );
6042
- const pluginRoot = path23.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
6043
- if (existsSync18(pluginRoot)) {
6265
+ const pluginRoot = path24.join(os5.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
6266
+ if (existsSync19(pluginRoot)) {
6044
6267
  try {
6045
- const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
6268
+ const versions = readdirSync4(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
6046
6269
  const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
6047
6270
  if (newest !== void 0) {
6048
6271
  const skewed = versionIsNewer(newest, cliVersion());
@@ -7566,9 +7789,9 @@ __export(record_exports, {
7566
7789
  runRecordPlan: () => runRecordPlan,
7567
7790
  runRecordStatus: () => runRecordStatus
7568
7791
  });
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";
7792
+ import { existsSync as existsSync21, mkdtempSync as mkdtempSync2, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "node:fs";
7793
+ import os6 from "node:os";
7794
+ import path27 from "node:path";
7572
7795
  import { writeFileSync as writeFileSync9 } from "node:fs";
7573
7796
  function recordsInteractionState(reports) {
7574
7797
  const evident = (s) => stateTokens(s).some((t) => INTERACTION_EVIDENCE_VALUES.has(t));
@@ -7591,7 +7814,7 @@ function interactionDisclosure(component, reports) {
7591
7814
  };
7592
7815
  }
7593
7816
  function symbolsFromMetadataEnvelope(file, sourceFrame) {
7594
- const env = JSON.parse(readFileSync16(file, "utf8"));
7817
+ const env = JSON.parse(readFileSync17(file, "utf8"));
7595
7818
  const text = env.content.map((c) => c.text ?? "").join("\n");
7596
7819
  const symbols = [];
7597
7820
  const walk2 = (node, ancestor) => {
@@ -7649,7 +7872,7 @@ function runRecordPlan(opts) {
7649
7872
  if (rawFile !== void 0) {
7650
7873
  try {
7651
7874
  const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
7652
- const tmp = path26.join(mkdtempSync2(path26.join(os5.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
7875
+ const tmp = path27.join(mkdtempSync2(path27.join(os6.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
7653
7876
  writeFileSync9(tmp, JSON.stringify(envelope));
7654
7877
  metadataEntries.push({ file: tmp });
7655
7878
  } catch (err) {
@@ -7671,7 +7894,7 @@ function runRecordPlan(opts) {
7671
7894
  let metadataTruncated = false;
7672
7895
  for (const { file, frame } of metadataEntries) {
7673
7896
  try {
7674
- const parsed = symbolsFromMetadataEnvelope(path26.resolve(file), frame);
7897
+ const parsed = symbolsFromMetadataEnvelope(path27.resolve(file), frame);
7675
7898
  symbols.push(...parsed.symbols);
7676
7899
  if (parsed.truncated) metadataTruncated = true;
7677
7900
  } catch (err) {
@@ -7705,7 +7928,7 @@ function runRecordPlan(opts) {
7705
7928
  if (symbols.length === 0) {
7706
7929
  const leads = metadataEntries.flatMap(({ file }) => {
7707
7930
  try {
7708
- const env = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
7931
+ const env = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
7709
7932
  return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
7710
7933
  } catch {
7711
7934
  return [];
@@ -7779,7 +8002,7 @@ function runRecordPlan(opts) {
7779
8002
  // a CLI flag and the MCP surface has no parameter for it.
7780
8003
  decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
7781
8004
  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`]
8005
+ userRuns: [`rm ${quoteArg(path27.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
7783
8006
  },
7784
8007
  {
7785
8008
  id: "larger-allowance",
@@ -7906,7 +8129,7 @@ function nextPayload(setDir) {
7906
8129
  const instruction = nextInstruction(setDir);
7907
8130
  const status = sessionStatus(setDir);
7908
8131
  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"))) {
8132
+ if (instruction === null && !existsSync21(path27.join(setDir, "get_variable_defs.json"))) {
7910
8133
  const manifest = loadManifest(setDir);
7911
8134
  const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
7912
8135
  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 };
@@ -7935,7 +8158,7 @@ function runRecordNext(opts) {
7935
8158
  const progress = payload["progress"];
7936
8159
  process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
7937
8160
  \u2192 ${payload["note"]}
7938
- \u2192 then: ${tendrilCommand(`record ingest --set ${path26.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
8161
+ \u2192 then: ${tendrilCommand(`record ingest --set ${path27.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
7939
8162
  `);
7940
8163
  });
7941
8164
  }
@@ -8009,7 +8232,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
8009
8232
  const skipped = [];
8010
8233
  const failed = [];
8011
8234
  for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
8012
- if (existsSync20(path26.join(setDir, rep, name))) {
8235
+ if (existsSync21(path27.join(setDir, rep, name))) {
8013
8236
  skipped.push(name);
8014
8237
  continue;
8015
8238
  }
@@ -8031,16 +8254,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
8031
8254
  }
8032
8255
  function rawEnvelopeFromFile(file, parts) {
8033
8256
  if (parts) {
8034
- const blocks = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
8257
+ const blocks = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
8035
8258
  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
8259
  return { content: blocks.map((text) => ({ type: "text", text })) };
8037
8260
  }
8038
- return { content: [{ type: "text", text: readFileSync16(path26.resolve(file), "utf8") }] };
8261
+ return { content: [{ type: "text", text: readFileSync17(path27.resolve(file), "utf8") }] };
8039
8262
  }
8040
8263
  async function runRecordIngest(opts) {
8041
8264
  let payload;
8042
8265
  try {
8043
- payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(path26.resolve(opts.file), "utf8"));
8266
+ payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync17(path27.resolve(opts.file), "utf8"));
8044
8267
  } catch (err) {
8045
8268
  fail(opts, ExitCode.InputValidation, {
8046
8269
  error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
@@ -8052,7 +8275,7 @@ async function runRecordIngest(opts) {
8052
8275
  fail(opts, ExitCode.InputValidation, {
8053
8276
  error: "--raw is for text tool responses; screenshots are binary",
8054
8277
  code: "envelope-invalid",
8055
- remediation: `Use \`${tendrilCommand(`record fetch --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
8278
+ remediation: `Use \`${tendrilCommand(`record fetch --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
8056
8279
  });
8057
8280
  }
8058
8281
  if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
@@ -8072,7 +8295,7 @@ async function runRecordIngest(opts) {
8072
8295
  remediation: REINGEST_GUIDANCE
8073
8296
  });
8074
8297
  }
8075
- writeFileSync9(path26.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
8298
+ writeFileSync9(path27.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
8076
8299
  `);
8077
8300
  emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
8078
8301
  process.stdout.write("set-level get_variable_defs ingested\n");
@@ -8088,7 +8311,7 @@ async function runRecordIngest(opts) {
8088
8311
  if (assets !== void 0) {
8089
8312
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
8090
8313
  `);
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>`)}
8314
+ 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
8315
  `);
8093
8316
  }
8094
8317
  });
@@ -8161,15 +8384,15 @@ async function runRecordIngestRep(opts) {
8161
8384
  if (assets !== void 0) {
8162
8385
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
8163
8386
  `);
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>`)}
8387
+ 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
8388
  `);
8166
8389
  }
8167
8390
  });
8168
8391
  }
8169
8392
  function runRecordAsset(opts) {
8170
8393
  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));
8394
+ const dir = path27.resolve(opts.dir);
8395
+ const names = readdirSync6(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
8173
8396
  if (names.length === 0) {
8174
8397
  fail(opts, ExitCode.InputValidation, {
8175
8398
  error: `no asset-*.<ext> files found in ${dir}`,
@@ -8180,7 +8403,7 @@ function runRecordAsset(opts) {
8180
8403
  const ingested = [];
8181
8404
  try {
8182
8405
  for (const name of names) {
8183
- ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path26.join(dir, name)));
8406
+ ingestAsset(opts.setDir, opts.rep, name, readFileSync17(path27.join(dir, name)));
8184
8407
  ingested.push(name);
8185
8408
  }
8186
8409
  } catch (err) {
@@ -8200,11 +8423,11 @@ function runRecordAsset(opts) {
8200
8423
  fail(opts, ExitCode.InputValidation, {
8201
8424
  error: "pass --name and --file for a single asset, or --dir for a batch",
8202
8425
  code: "asset-rejected",
8203
- remediation: tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
8426
+ remediation: tendrilCommand(`record asset --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
8204
8427
  });
8205
8428
  }
8206
8429
  try {
8207
- ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(path26.resolve(opts.file)));
8430
+ ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync17(path27.resolve(opts.file)));
8208
8431
  emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
8209
8432
  process.stdout.write(`ingested ${opts.rep}/${opts.name}
8210
8433
  `);
@@ -8240,7 +8463,7 @@ function narrowedRoles(derived, override) {
8240
8463
  function rolesFromFile(opts, file, derived) {
8241
8464
  let json;
8242
8465
  try {
8243
- json = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
8466
+ json = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
8244
8467
  } catch (err) {
8245
8468
  fail(opts, ExitCode.InputValidation, {
8246
8469
  error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -8278,11 +8501,11 @@ function rolesFromFile(opts, file, derived) {
8278
8501
  };
8279
8502
  }
8280
8503
  function runRecordFinish(opts) {
8281
- if (!existsSync20(path26.join(opts.setDir, "recording-set.json"))) {
8504
+ if (!existsSync21(path27.join(opts.setDir, "recording-set.json"))) {
8282
8505
  fail(opts, ExitCode.InputValidation, {
8283
8506
  error: `no recording-set.json in ${opts.setDir}`,
8284
8507
  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.`
8508
+ 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
8509
  });
8287
8510
  }
8288
8511
  const { manifest, raw } = readManifestFile(opts.setDir);
@@ -8310,17 +8533,17 @@ function runRecordFinish(opts) {
8310
8533
  fail(opts, ExitCode.ConfirmationRequired, {
8311
8534
  error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
8312
8535
  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.`
8536
+ 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
8537
  });
8315
8538
  }
8316
8539
  const merged = { ...raw, roles };
8317
- const { issues } = validateRecordingSet(merged, (rel) => existsSync20(path26.join(opts.setDir, rel)));
8540
+ const { issues } = validateRecordingSet(merged, (rel) => existsSync21(path27.join(opts.setDir, rel)));
8318
8541
  const errors = issues.filter((i) => i.severity === "error");
8319
8542
  if (errors.length > 0) {
8320
8543
  fail(opts, ExitCode.InputValidation, {
8321
8544
  error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
8322
8545
  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\`.`
8546
+ 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
8547
  });
8325
8548
  }
8326
8549
  for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
@@ -8554,8 +8777,8 @@ var init_engine_curated = __esm({
8554
8777
  });
8555
8778
 
8556
8779
  // 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";
8780
+ import { existsSync as existsSync22, mkdirSync as mkdirSync6, readFileSync as readFileSync18, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
8781
+ import path28 from "node:path";
8559
8782
  import { z as z11 } from "zod";
8560
8783
  function objective(scores, behaviors) {
8561
8784
  const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
@@ -8592,9 +8815,9 @@ ${absentLines.join("\n")}` : ""}
8592
8815
  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
8816
  }
8594
8817
  function archivePriorRun(outDir) {
8595
- if (!existsSync21(path27.join(outDir, "run-log.json")) && !existsSync21(path27.join(outDir, "loop-state.json"))) return void 0;
8818
+ if (!existsSync22(path28.join(outDir, "run-log.json")) && !existsSync22(path28.join(outDir, "loop-state.json"))) return void 0;
8596
8819
  let n = 1;
8597
- while (existsSync21(`${outDir}-prev-${n}`)) n += 1;
8820
+ while (existsSync22(`${outDir}-prev-${n}`)) n += 1;
8598
8821
  renameSync(outDir, `${outDir}-prev-${n}`);
8599
8822
  return `${outDir}-prev-${n}`;
8600
8823
  }
@@ -8603,14 +8826,14 @@ async function runEngineLoop(opts) {
8603
8826
  const plateau = opts.plateau ?? 2;
8604
8827
  const progress = opts.onProgress ?? (() => {
8605
8828
  });
8606
- const statePath = path27.join(opts.outDir, "loop-state.json");
8607
- const resuming = opts.resume === true && existsSync21(statePath);
8829
+ const statePath = path28.join(opts.outDir, "loop-state.json");
8830
+ const resuming = opts.resume === true && existsSync22(statePath);
8608
8831
  if (!resuming) {
8609
8832
  const archived = archivePriorRun(opts.outDir);
8610
8833
  if (archived !== void 0) progress(`previous run archived to ${archived}`);
8611
8834
  }
8612
8835
  mkdirSync6(opts.outDir, { recursive: true });
8613
- const scratch = path27.join(opts.outDir, ".candidate");
8836
+ const scratch = path28.join(opts.outDir, ".candidate");
8614
8837
  let attempts = [];
8615
8838
  let log = [];
8616
8839
  let best;
@@ -8618,7 +8841,7 @@ async function runEngineLoop(opts) {
8618
8841
  let nonAccepted = 0;
8619
8842
  let stopReason = "max-iterations";
8620
8843
  if (resuming) {
8621
- const restored = LoopStateSchema.parse(JSON.parse(readFileSync17(statePath, "utf8")));
8844
+ const restored = LoopStateSchema.parse(JSON.parse(readFileSync18(statePath, "utf8")));
8622
8845
  attempts = restored.attempts;
8623
8846
  log = restored.iterations;
8624
8847
  spentUsd = restored.spentUsd;
@@ -8638,7 +8861,7 @@ async function runEngineLoop(opts) {
8638
8861
  };
8639
8862
  const writeCandidate = (files) => {
8640
8863
  mkdirSync6(scratch, { recursive: true });
8641
- for (const [name, content] of Object.entries(files)) writeFileSync10(path27.join(scratch, name), content);
8864
+ for (const [name, content] of Object.entries(files)) writeFileSync10(path28.join(scratch, name), content);
8642
8865
  };
8643
8866
  const scoreCandidate = async (candidate, iter, usd, modelMs) => {
8644
8867
  writeCandidate(candidate.files);
@@ -8696,8 +8919,8 @@ async function runEngineLoop(opts) {
8696
8919
  const usd = candidate.usage?.usd ?? 0;
8697
8920
  spentUsd += usd;
8698
8921
  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);
8922
+ mkdirSync6(path28.join(opts.outDir, "responses"), { recursive: true });
8923
+ writeFileSync10(path28.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
8701
8924
  }
8702
8925
  if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
8703
8926
  const finish = candidate.usage?.finishReason ?? "?";
@@ -8723,10 +8946,10 @@ async function runEngineLoop(opts) {
8723
8946
  }
8724
8947
  }
8725
8948
  }
8726
- if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path27.join(opts.outDir, name), content);
8949
+ if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path28.join(opts.outDir, name), content);
8727
8950
  const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
8728
8951
  writeFileSync10(
8729
- path27.join(opts.outDir, "run-log.json"),
8952
+ path28.join(opts.outDir, "run-log.json"),
8730
8953
  `${JSON.stringify(
8731
8954
  {
8732
8955
  ...opts.meta,
@@ -8793,8 +9016,8 @@ var init_loop2 = __esm({
8793
9016
  });
8794
9017
 
8795
9018
  // packages/generate/src/brief.ts
8796
- import { existsSync as existsSync22, readFileSync as readFileSync18 } from "node:fs";
8797
- import path28 from "node:path";
9019
+ import { existsSync as existsSync23, readFileSync as readFileSync19 } from "node:fs";
9020
+ import path29 from "node:path";
8798
9021
  function singleAxes2(name) {
8799
9022
  const parsed = parseVariantAxes(name);
8800
9023
  if (parsed === void 0) return void 0;
@@ -9035,12 +9258,12 @@ function authorBehaviors(api, extras = {}) {
9035
9258
  return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
9036
9259
  }
9037
9260
  function envelopeText(file) {
9038
- return envelopeFirstTextPart(JSON.parse(readFileSync18(file, "utf8")));
9261
+ return envelopeFirstTextPart(JSON.parse(readFileSync19(file, "utf8")));
9039
9262
  }
9040
9263
  function dismissEvidence(setDir, repSlugs) {
9041
9264
  for (const slug of repSlugs) {
9042
- const f = path28.join(setDir, slug, "get_design_context.json");
9043
- if (!existsSync22(f)) continue;
9265
+ const f = path29.join(setDir, slug, "get_design_context.json");
9266
+ if (!existsSync23(f)) continue;
9044
9267
  const text = envelopeText(f);
9045
9268
  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
9269
  if (propHit !== null) return `emission prop "${propHit[1]}"`;
@@ -9084,13 +9307,13 @@ function recordedFontNeeds(setDir, opts = {}) {
9084
9307
  }
9085
9308
  };
9086
9309
  const manifest = loadManifest(setDir);
9087
- const setDefs = path28.join(setDir, "get_variable_defs.json");
9088
- if (existsSync22(setDefs)) fromDefs(envelopeText(setDefs));
9310
+ const setDefs = path29.join(setDir, "get_variable_defs.json");
9311
+ if (existsSync23(setDefs)) fromDefs(envelopeText(setDefs));
9089
9312
  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));
9313
+ const ctx = path29.join(setDir, rep.slug, "get_design_context.json");
9314
+ if (existsSync23(ctx)) fromEmission(envelopeText(ctx));
9315
+ const defs = path29.join(setDir, rep.slug, "get_variable_defs.json");
9316
+ if (existsSync23(defs)) fromDefs(envelopeText(defs));
9094
9317
  }
9095
9318
  return [...byFamily.entries()].map(([family, paired]) => {
9096
9319
  const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
@@ -9101,10 +9324,10 @@ function symbolFontGlyphCount(setDir, reps) {
9101
9324
  const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
9102
9325
  const glyphs = /* @__PURE__ */ new Set();
9103
9326
  for (const rep of reps) {
9104
- const file = path28.join(setDir, rep, "get_metadata.json");
9105
- if (!existsSync22(file)) continue;
9327
+ const file = path29.join(setDir, rep, "get_metadata.json");
9328
+ if (!existsSync23(file)) continue;
9106
9329
  try {
9107
- const text = JSON.parse(readFileSync18(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
9330
+ const text = JSON.parse(readFileSync19(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
9108
9331
  for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
9109
9332
  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
9333
  if (PUA.test(name)) glyphs.add(name);
@@ -9130,8 +9353,8 @@ function recordedTextSlots(setDir, repSlugs) {
9130
9353
  const propRep = [];
9131
9354
  const perRep = [];
9132
9355
  for (const slug of repSlugs) {
9133
- const f = path28.join(setDir, slug, "get_design_context.json");
9134
- if (!existsSync22(f)) continue;
9356
+ const f = path29.join(setDir, slug, "get_design_context.json");
9357
+ if (!existsSync23(f)) continue;
9135
9358
  const code = envelopeText(f);
9136
9359
  const props = /* @__PURE__ */ new Map();
9137
9360
  for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
@@ -9156,8 +9379,8 @@ function recordedTextSlots(setDir, repSlugs) {
9156
9379
  }
9157
9380
  const axisValuesBySlug = /* @__PURE__ */ new Map();
9158
9381
  for (const slug of repSlugs) {
9159
- const metaFile = path28.join(setDir, slug, "get_metadata.json");
9160
- if (!existsSync22(metaFile)) continue;
9382
+ const metaFile = path29.join(setDir, slug, "get_metadata.json");
9383
+ if (!existsSync23(metaFile)) continue;
9161
9384
  const name = symbolName(envelopeText(metaFile));
9162
9385
  if (name === void 0) continue;
9163
9386
  const values = /* @__PURE__ */ new Set();
@@ -9256,8 +9479,8 @@ function authorTaskFromSet(setDir, opts = {}) {
9256
9479
  const poses = [];
9257
9480
  const missing = [];
9258
9481
  for (const rep of manifest.reps) {
9259
- const metaFile = path28.join(setDir, rep.slug, "get_metadata.json");
9260
- if (!existsSync22(metaFile)) {
9482
+ const metaFile = path29.join(setDir, rep.slug, "get_metadata.json");
9483
+ if (!existsSync23(metaFile)) {
9261
9484
  missing.push(rep.slug);
9262
9485
  continue;
9263
9486
  }
@@ -9271,8 +9494,8 @@ function authorTaskFromSet(setDir, opts = {}) {
9271
9494
  if (missing.length > 0) {
9272
9495
  throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
9273
9496
  }
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);
9497
+ const setMeta = path29.join(setDir, "get_metadata.json");
9498
+ const latticeNames = manifest.latticeNames ?? (existsSync23(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
9276
9499
  const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
9277
9500
  const recordedFonts = recordedFontFamilies(setDir);
9278
9501
  const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
@@ -9455,10 +9678,10 @@ var init_brief = __esm({
9455
9678
  });
9456
9679
 
9457
9680
  // 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";
9681
+ import { existsSync as existsSync24, readFileSync as readFileSync20, readdirSync as readdirSync7 } from "node:fs";
9682
+ import path30 from "node:path";
9460
9683
  function repText(set, rep, tool) {
9461
- const env = JSON.parse(readFileSync19(path29.join(set, rep, `${tool}.json`), "utf8"));
9684
+ const env = JSON.parse(readFileSync20(path30.join(set, rep, `${tool}.json`), "utf8"));
9462
9685
  return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
9463
9686
  }
9464
9687
  function stripFigmaInstructions(emission) {
@@ -9518,18 +9741,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
9518
9741
  }
9519
9742
  function buildSegments(task, mode = "fenced") {
9520
9743
  const SET = task.set;
9744
+ let defsRecorded = existsSync24(path30.join(SET, "get_variable_defs.json"));
9521
9745
  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"))) || "{}";
9746
+ if (existsSync24(path30.join(SET, "get_variable_defs.json"))) {
9747
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync20(path30.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
9524
9748
  try {
9525
9749
  rawDefs = JSON.parse(text);
9526
9750
  } catch {
9527
9751
  }
9528
9752
  } else {
9529
9753
  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"))) || "{}";
9754
+ const f = path30.join(SET, cfg.rep, "get_variable_defs.json");
9755
+ if (!existsSync24(f)) continue;
9756
+ defsRecorded = true;
9757
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync20(f, "utf8"))) || "{}";
9533
9758
  try {
9534
9759
  for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
9535
9760
  } catch {
@@ -9537,25 +9762,27 @@ function buildSegments(task, mode = "fenced") {
9537
9762
  }
9538
9763
  }
9539
9764
  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"))) : "";
9765
+ const f = path30.join(SET, cfg.rep, "get_design_context.json");
9766
+ return existsSync24(f) ? envelopeFirstTextPart(JSON.parse(readFileSync20(f, "utf8"))) : "";
9542
9767
  });
9543
9768
  const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
9544
9769
  const defs = JSON.stringify(map, null, 1);
9545
9770
  const parts = [
9546
9771
  "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
- `
9772
+ defsRecorded ? `
9548
9773
  ## Design tokens (use these CSS custom property names)
9549
9774
  \`\`\`json
9550
9775
  ${defs}
9551
- \`\`\`${note}`
9776
+ \`\`\`${note}` : `
9777
+ ## Design tokens
9778
+ 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
9779
  ];
9553
9780
  for (const cfg of task.configs) {
9554
9781
  const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
9555
9782
  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}:
9783
+ const assets = readdirSync7(path30.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
9557
9784
  \`\`\`svg
9558
- ${readFileSync19(path29.join(SET, cfg.rep, f), "utf8")}
9785
+ ${readFileSync20(path30.join(SET, cfg.rep, f), "utf8")}
9559
9786
  \`\`\``).join("\n");
9560
9787
  parts.push(`
9561
9788
  ## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
@@ -9580,7 +9807,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
9580
9807
  } else {
9581
9808
  parts.push(`
9582
9809
  ## 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.`);
9810
+ 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
9811
  }
9585
9812
  return parts.join("\n");
9586
9813
  }
@@ -9648,8 +9875,8 @@ var init_adapter = __esm({
9648
9875
 
9649
9876
  // packages/generate/src/bundle-emit.ts
9650
9877
  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";
9878
+ import { copyFileSync, existsSync as existsSync25, mkdirSync as mkdirSync7, readFileSync as readFileSync21, readdirSync as readdirSync8, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
9879
+ import path31 from "node:path";
9653
9880
  function pinFromConfigs(configs) {
9654
9881
  const domains = /* @__PURE__ */ new Map();
9655
9882
  const kinds = /* @__PURE__ */ new Map();
@@ -9718,15 +9945,23 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9718
9945
  const notices = [];
9719
9946
  const licenseTexts = /* @__PURE__ */ new Map();
9720
9947
  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";
9948
+ const src = path31.join(cacheDir, path31.basename(face.file));
9949
+ const target = `./fonts/${path31.basename(face.file)}`;
9950
+ const format = FONT_FORMATS[path31.extname(face.file).toLowerCase()] ?? "truetype";
9724
9951
  const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
9725
9952
  const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
9726
9953
  const license = normalizeFontLicense(face.license);
9727
- const terms = REDISTRIBUTABLE.get(license);
9954
+ const terms = face.source.startsWith("system:") ? void 0 : REDISTRIBUTABLE.get(license);
9728
9955
  if (terms === void 0) {
9729
- if (face.source.startsWith("local:")) {
9956
+ if (face.source.startsWith("system:")) {
9957
+ lines.push(
9958
+ `/* '${family}' ${face.weight} came from this machine's installed system fonts (tendril fonts`,
9959
+ ` add-system; sha256 ${face.sha256.slice(0, 16)}\u2026). OS-bundled faces are never copied into`,
9960
+ " bundles regardless of any recorded licence \u2014 the consuming machine provides its own copy,",
9961
+ ` or you place one you licence at ${target} and uncomment: */`,
9962
+ `/* ${decl} */`
9963
+ );
9964
+ } else if (face.source.startsWith("local:")) {
9730
9965
  lines.push(
9731
9966
  `/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
9732
9967
  ` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
@@ -9750,14 +9985,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9750
9985
  `/* ${decl} */`
9751
9986
  );
9752
9987
  }
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)));
9988
+ } else if (existsSync25(src) && createHash4("sha256").update(readFileSync21(src)).digest("hex") === face.sha256) {
9989
+ mkdirSync7(path31.join(bundleDir, "fonts"), { recursive: true });
9990
+ copyFileSync(src, path31.join(bundleDir, "fonts", path31.basename(face.file)));
9756
9991
  licenseTexts.set(terms.file, terms.text);
9757
9992
  const upstream = upstreamAttribution(face);
9758
9993
  notices.push(
9759
9994
  "",
9760
- `${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path30.basename(face.file)}`,
9995
+ `${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path31.basename(face.file)}`,
9761
9996
  ` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
9762
9997
  ` source: ${face.source}`,
9763
9998
  ` sha256: ${face.sha256}`,
@@ -9771,9 +10006,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9771
10006
  }
9772
10007
  if (lines.length === 0) return null;
9773
10008
  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")}
10009
+ const fontsDir = path31.join(bundleDir, "fonts");
10010
+ for (const [file, text] of licenseTexts) writeFileSync11(path31.join(fontsDir, file), text);
10011
+ writeFileSync11(path31.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
9777
10012
  `);
9778
10013
  header.push(
9779
10014
  "/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
@@ -9785,10 +10020,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9785
10020
  `;
9786
10021
  }
9787
10022
  function countLatticeSymbols(setDir) {
9788
- const manifestFile = path30.join(setDir, "recording-set.json");
9789
- if (existsSync24(manifestFile)) {
10023
+ const manifestFile = path31.join(setDir, "recording-set.json");
10024
+ if (existsSync25(manifestFile)) {
9790
10025
  try {
9791
- const stored = JSON.parse(readFileSync20(manifestFile, "utf8"));
10026
+ const stored = JSON.parse(readFileSync21(manifestFile, "utf8"));
9792
10027
  if (stored.variantScope !== "component-set") return null;
9793
10028
  const lattice = stored.latticeNames;
9794
10029
  if (lattice !== void 0 && lattice.length > 0) return lattice.length;
@@ -9796,13 +10031,13 @@ function countLatticeSymbols(setDir) {
9796
10031
  }
9797
10032
  }
9798
10033
  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));
10034
+ path31.join(setDir, "get_metadata.json"),
10035
+ ...existsSync25(setDir) ? readdirSync8(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path31.join(setDir, f)) : []
10036
+ ].filter((f) => existsSync25(f));
9802
10037
  if (files.length === 0) return null;
9803
10038
  let count = 0;
9804
10039
  for (const f of files) {
9805
- const text = envelopeTextContent(JSON.parse(readFileSync20(f, "utf8")));
10040
+ const text = envelopeTextContent(JSON.parse(readFileSync21(f, "utf8")));
9806
10041
  count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
9807
10042
  }
9808
10043
  return count > 0 ? count : null;
@@ -9810,21 +10045,21 @@ function countLatticeSymbols(setDir) {
9810
10045
  function recordingSetHash(setDir, configs) {
9811
10046
  const relPaths = [];
9812
10047
  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);
10048
+ if (existsSync25(path31.join(setDir, name))) relPaths.push(name);
9814
10049
  }
9815
10050
  for (const cfg of configs) {
9816
10051
  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}`);
10052
+ if (existsSync25(path31.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
9818
10053
  }
9819
- if (existsSync24(path30.join(setDir, cfg.rep))) {
9820
- for (const asset of readdirSync7(path30.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
10054
+ if (existsSync25(path31.join(setDir, cfg.rep))) {
10055
+ for (const asset of readdirSync8(path31.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
9821
10056
  relPaths.push(`${cfg.rep}/${asset}`);
9822
10057
  }
9823
10058
  }
9824
10059
  }
9825
10060
  return hashRecordingSet(
9826
10061
  relPaths,
9827
- (p) => new Uint8Array(readFileSync20(path30.join(setDir, p))),
10062
+ (p) => new Uint8Array(readFileSync21(path31.join(setDir, p))),
9828
10063
  (chunks) => {
9829
10064
  const h = createHash4("sha256");
9830
10065
  for (const c of chunks) h.update(c);
@@ -9845,10 +10080,11 @@ function emitBundleV1(opts) {
9845
10080
  const certified = statuses.filter((s) => s.status === "certified").length;
9846
10081
  const pass = statuses.filter((s) => s.status !== "fail").length;
9847
10082
  const lattice = countLatticeSymbols(opts.task.set);
9848
- const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
10083
+ const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
9849
10084
  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"));
10085
+ const parity = opts.behaviors.filter((b) => b.id.startsWith("parity:"));
10086
+ const cssFiles = ["styles.css", "tokens.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f));
10087
+ const families = cssFontFamilies(cssFiles.map((f) => readFileSync21(f, "utf8")).join("\n"));
9852
10088
  const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
9853
10089
  family: f.family,
9854
10090
  weight: f.weight,
@@ -9877,7 +10113,7 @@ function emitBundleV1(opts) {
9877
10113
  // resolvable via verify's --set override).
9878
10114
  path: (() => {
9879
10115
  const base = process.env["INIT_CWD"] ?? process.cwd();
9880
- const rel = path30.relative(base, opts.task.set);
10116
+ const rel = path31.relative(base, opts.task.set);
9881
10117
  return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
9882
10118
  })(),
9883
10119
  component: opts.componentName,
@@ -9901,21 +10137,21 @@ function emitBundleV1(opts) {
9901
10137
  })
9902
10138
  };
9903
10139
  const written = [];
9904
- const manifestPath2 = path30.join(opts.bundleDir, "component.json");
10140
+ const manifestPath2 = path31.join(opts.bundleDir, "component.json");
9905
10141
  writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
9906
10142
  `);
9907
10143
  written.push(manifestPath2);
9908
- const stylesPath = path30.join(opts.bundleDir, "styles.css");
9909
- if (existsSync24(stylesPath)) {
10144
+ const stylesPath = path31.join(opts.bundleDir, "styles.css");
10145
+ if (existsSync25(stylesPath)) {
9910
10146
  const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
9911
- const current = readFileSync20(stylesPath, "utf8");
10147
+ const current = readFileSync21(stylesPath, "utf8");
9912
10148
  const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
9913
10149
  writeFileSync11(stylesPath, `${comment}
9914
10150
  ${stripped}`);
9915
10151
  written.push(stylesPath);
9916
10152
  }
9917
- const fontsCssPath = path30.join(opts.bundleDir, "fonts.css");
9918
- rmSync3(path30.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
10153
+ const fontsCssPath = path31.join(opts.bundleDir, "fonts.css");
10154
+ rmSync3(path31.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
9919
10155
  rmSync3(fontsCssPath, { force: true });
9920
10156
  const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
9921
10157
  if (fontsCss !== null) {
@@ -9923,7 +10159,7 @@ ${stripped}`);
9923
10159
  written.push(fontsCssPath);
9924
10160
  }
9925
10161
  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\``;
10162
+ 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
10163
  return { manifest, statusLine, written };
9928
10164
  }
9929
10165
  var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
@@ -10360,9 +10596,9 @@ var init_src7 = __esm({
10360
10596
  });
10361
10597
 
10362
10598
  // packages/cli/src/font-guidance.ts
10363
- import path31 from "node:path";
10599
+ import path32 from "node:path";
10364
10600
  function fontsUnprovenRemediation(setDir) {
10365
- const set = setDir === void 0 ? void 0 : path31.resolve(setDir);
10601
+ const set = setDir === void 0 ? void 0 : path32.resolve(setDir);
10366
10602
  if (set !== void 0) {
10367
10603
  try {
10368
10604
  const needs = recordedFontNeeds(set);
@@ -10430,13 +10666,15 @@ __export(fonts_exports, {
10430
10666
  DEFAULT_FONT_CACHE: () => DEFAULT_FONT_CACHE,
10431
10667
  familyMismatch: () => familyMismatch,
10432
10668
  runFontsAdd: () => runFontsAdd,
10669
+ runFontsAddSystem: () => runFontsAddSystem,
10670
+ runFontsDiscover: () => runFontsDiscover,
10433
10671
  runFontsRequired: () => runFontsRequired,
10434
10672
  runFontsResolve: () => runFontsResolve,
10435
10673
  runFontsResolveSet: () => runFontsResolveSet,
10436
10674
  runFontsStatus: () => runFontsStatus
10437
10675
  });
10438
- import { existsSync as existsSync25, readFileSync as readFileSync21 } from "node:fs";
10439
- import path32 from "node:path";
10676
+ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
10677
+ import path33 from "node:path";
10440
10678
  async function runFontsResolve(opts) {
10441
10679
  const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
10442
10680
  emitData(opts, result, () => {
@@ -10451,7 +10689,7 @@ async function runFontsResolve(opts) {
10451
10689
  }
10452
10690
  }
10453
10691
  async function runFontsResolveSet(opts) {
10454
- const setDir = path32.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
10692
+ const setDir = path33.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
10455
10693
  let needs = [];
10456
10694
  try {
10457
10695
  needs = recordedFontNeeds(setDir);
@@ -10514,6 +10752,12 @@ async function runFontsResolveSet(opts) {
10514
10752
  `);
10515
10753
  for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
10516
10754
  `);
10755
+ for (const fam of [...new Set(failures.map((f) => f.family))]) {
10756
+ process.stdout.write(
10757
+ ` 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)
10758
+ `
10759
+ );
10760
+ }
10517
10761
  });
10518
10762
  if (byteDrift.length > 0) {
10519
10763
  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 +10768,16 @@ async function runFontsResolveSet(opts) {
10524
10768
  }
10525
10769
  }
10526
10770
  function runFontsStatus(opts) {
10527
- const manifestPath2 = path32.join(opts.cacheDir, "manifest.json");
10528
- if (!existsSync25(manifestPath2)) {
10771
+ const manifestPath2 = path33.join(opts.cacheDir, "manifest.json");
10772
+ if (!existsSync26(manifestPath2)) {
10529
10773
  fail(opts, ExitCode.FontsUnproven, {
10530
10774
  error: `no font cache at ${opts.cacheDir}`,
10531
10775
  code: "fonts-unresolved",
10532
10776
  remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
10533
10777
  });
10534
10778
  }
10535
- const faces = JSON.parse(readFileSync21(manifestPath2, "utf8"));
10536
- const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path32.resolve(opts.lock), opts.cacheDir) : null;
10779
+ const faces = JSON.parse(readFileSync22(manifestPath2, "utf8"));
10780
+ const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path33.resolve(opts.lock), opts.cacheDir) : null;
10537
10781
  emitData(opts, { faces, lockVerdicts }, () => {
10538
10782
  for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
10539
10783
  `);
@@ -10577,13 +10821,13 @@ function familyMismatch(family, declared) {
10577
10821
  }
10578
10822
  function runFontsAdd(opts) {
10579
10823
  if (opts.set !== void 0) {
10580
- const declared = taskFontFamilies(path32.resolve(opts.set)) ?? [];
10824
+ const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
10581
10825
  const mismatch = familyMismatch(opts.family, declared);
10582
10826
  if (mismatch !== void 0) {
10583
10827
  fail(opts, ExitCode.InputValidation, {
10584
10828
  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
10829
  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.`
10830
+ 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
10831
  });
10588
10832
  }
10589
10833
  } else {
@@ -10610,6 +10854,73 @@ function runFontsAdd(opts) {
10610
10854
  `);
10611
10855
  });
10612
10856
  }
10857
+ function runFontsDiscover(opts) {
10858
+ const faces = opts.family !== void 0 ? systemFacesForFamily(opts.family) : discoverSystemFaces();
10859
+ const rows = faces.map((f) => ({ ...f, ...systemFaceRefusal(f.family) !== void 0 ? { refused: systemFaceRefusal(f.family) } : {} }));
10860
+ emitData(opts, { faces: rows, dirs: systemFontDirs() }, () => {
10861
+ if (rows.length === 0) {
10862
+ process.stdout.write(`no matching system faces found (scanned: ${systemFontDirs().join(", ")})
10863
+ `);
10864
+ return;
10865
+ }
10866
+ for (const f of rows) {
10867
+ const flags = [f.refused !== void 0 ? "REFUSED" : "", f.italic ? "italic" : "", f.variable ? "variable" : ""].filter((x) => x !== "").join(" ");
10868
+ process.stdout.write(`${f.family} \u2014 ${f.subfamily || "Regular"} (${f.weight})${flags === "" ? "" : ` [${flags}]`} ${f.file}${f.faceIndex !== void 0 ? ` #${f.faceIndex}` : ""}
10869
+ `);
10870
+ }
10871
+ const eligible = rows.filter((f) => f.refused === void 0 && !f.italic && !f.variable);
10872
+ if (eligible.length > 0 && opts.family !== void 0) {
10873
+ process.stdout.write(`
10874
+ cache them: ${tendrilCommand(`fonts add-system ${quoteArg(opts.family)}`)}
10875
+ `);
10876
+ }
10877
+ });
10878
+ }
10879
+ function runFontsAddSystem(opts) {
10880
+ if (opts.set !== void 0) {
10881
+ const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
10882
+ const mismatch = familyMismatch(opts.family, declared);
10883
+ if (mismatch !== void 0) {
10884
+ fail(opts, ExitCode.InputValidation, {
10885
+ 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.`,
10886
+ code: "font-family-not-declared",
10887
+ 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.`
10888
+ });
10889
+ }
10890
+ } else {
10891
+ warn(
10892
+ opts,
10893
+ `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`
10894
+ );
10895
+ }
10896
+ const result = addSystemFamily(opts.family, { cacheDir: opts.cacheDir, ...opts.weights !== void 0 ? { weights: opts.weights } : {} });
10897
+ if (result.refusal !== void 0) {
10898
+ fail(opts, ExitCode.InputValidation, {
10899
+ error: `"${opts.family}" is a refusal-class family: ${result.refusal}`,
10900
+ code: "font-family-refused",
10901
+ 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')}.`
10902
+ });
10903
+ }
10904
+ if (result.added.length === 0) {
10905
+ fail(opts, ExitCode.InputValidation, {
10906
+ 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`,
10907
+ code: "font-family-not-installed",
10908
+ remediation: `See what IS installed: ${tendrilCommand(`fonts discover ${quoteArg(opts.family)}`)} (or all faces with ${tendrilCommand("fonts discover")}).`
10909
+ });
10910
+ }
10911
+ for (const o of result.overwrote) {
10912
+ warn(
10913
+ opts,
10914
+ `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`
10915
+ );
10916
+ }
10917
+ emitData(opts, result, () => {
10918
+ 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
10919
+ `);
10920
+ for (const s of result.skipped) process.stdout.write(`skipped ${s.subfamily || "Regular"} ${s.weight}: ${s.reason}
10921
+ `);
10922
+ });
10923
+ }
10613
10924
  var init_fonts = __esm({
10614
10925
  "packages/cli/src/commands/fonts.ts"() {
10615
10926
  "use strict";
@@ -10640,15 +10951,24 @@ __export(verify_exports, {
10640
10951
  resolveComposition: () => resolveComposition,
10641
10952
  runVerify: () => runVerify
10642
10953
  });
10643
- import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
10644
- import path33 from "node:path";
10954
+ import { existsSync as existsSync27, readFileSync as readFileSync23 } from "node:fs";
10955
+ import path34 from "node:path";
10645
10956
  function interactionCoverage(behaviors) {
10646
- const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
10957
+ const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
10958
+ const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
10647
10959
  return {
10648
10960
  interactionChecks: interaction.length,
10649
10961
  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"
10962
+ preludeChecks: behaviors.length - interaction.length - parity.length,
10963
+ parityChecks: parity.length,
10964
+ parityPassed: parity.filter((b) => b.pass).length,
10965
+ // Asymmetric on purpose (review finding F3): a parity PASS never
10966
+ // makes operability "verified" (parity is pixel-parity, not
10967
+ // someone typing or clicking — the §0j miscount), but a parity
10968
+ // FAIL still forbids it — "verified" printed beside a broken hover
10969
+ // state is the one word doing too much lifting again, from the
10970
+ // other direction.
10971
+ operability: interaction.length > 0 && interaction.every((b) => b.pass) && parity.every((b) => b.pass) ? "verified" : "unverified"
10652
10972
  };
10653
10973
  }
10654
10974
  function operabilityReport(input) {
@@ -10681,6 +11001,16 @@ function operabilityReport(input) {
10681
11001
  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
11002
  };
10683
11003
  }
11004
+ const { parityChecks, parityPassed } = interactionCoverage(input.behaviors);
11005
+ if (parityPassed < parityChecks) {
11006
+ const failed = parityChecks - parityPassed;
11007
+ return {
11008
+ checks: interactionChecks,
11009
+ passed: interactionPassed,
11010
+ short: `${failed} of ${parityChecks} state-parity check(s) failed`,
11011
+ 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.`
11012
+ };
11013
+ }
10684
11014
  return { checks: interactionChecks, passed: interactionPassed };
10685
11015
  }
10686
11016
  function operabilityLine(state) {
@@ -10773,7 +11103,7 @@ function compositionReport(input) {
10773
11103
  function eyeCheck(bundleDir) {
10774
11104
  return {
10775
11105
  command: tendrilCommand(`inspect "${bundleDir}"`),
10776
- sheetPath: path33.join(bundleDir, "verify-evidence", "inspect.html"),
11106
+ sheetPath: path34.join(bundleDir, "verify-evidence", "inspect.html"),
10777
11107
  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
11108
  };
10779
11109
  }
@@ -10785,7 +11115,7 @@ function taskFromManifest(opts, manifest, setDir) {
10785
11115
  const adapterSlugs = Object.keys(manifest.propAdapter);
10786
11116
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
10787
11117
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
10788
- const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
11118
+ const registry = Object.values(TASKS).find((t) => path34.resolve(t.set) === path34.resolve(setDir));
10789
11119
  const authored = (() => {
10790
11120
  if (registry !== void 0) return void 0;
10791
11121
  try {
@@ -10811,6 +11141,10 @@ function taskFromManifest(opts, manifest, setDir) {
10811
11141
  task,
10812
11142
  unmapped,
10813
11143
  adapterOnly,
11144
+ // The parity AUTHORITY: CLI-owned configs for the same poses —
11145
+ // recording-derived (authored) or registry-declared — never the
11146
+ // adapter, which is the graded artifact framing itself (§0j).
11147
+ authorityConfigs: behaviorSource.configs,
10814
11148
  // Registry sets carry hand-declared behaviors and no derivation
10815
11149
  // runs, so their evidence is UNKNOWN, not empty: an empty list is
10816
11150
  // spent downstream as "the recording holds no interactive pose".
@@ -10820,19 +11154,19 @@ function taskFromManifest(opts, manifest, setDir) {
10820
11154
  }
10821
11155
  async function runVerify(opts) {
10822
11156
  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)) {
11157
+ const setOverride = opts.set !== void 0 ? path34.resolve(callerCwd, opts.set) : void 0;
11158
+ opts = { ...opts, bundleDir: path34.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
11159
+ if (!existsSync27(opts.bundleDir)) {
10826
11160
  fail(opts, ExitCode.InputValidation, {
10827
11161
  error: `bundle directory not found: ${opts.bundleDir}`,
10828
11162
  code: "bundle-missing",
10829
11163
  remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
10830
11164
  });
10831
11165
  }
10832
- const manifestPath2 = path33.join(opts.bundleDir, "component.json");
11166
+ const manifestPath2 = path34.join(opts.bundleDir, "component.json");
10833
11167
  let manifest;
10834
- if (existsSync26(manifestPath2)) {
10835
- const { manifest: parsed, issues } = readBundleManifest(readFileSync22(manifestPath2, "utf8"));
11168
+ if (existsSync27(manifestPath2)) {
11169
+ const { manifest: parsed, issues } = readBundleManifest(readFileSync23(manifestPath2, "utf8"));
10836
11170
  if (issues.length > 0) {
10837
11171
  fail(opts, ExitCode.InputValidation, {
10838
11172
  error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -10846,6 +11180,7 @@ async function runVerify(opts) {
10846
11180
  let unmapped = [];
10847
11181
  let interactionEvidence;
10848
11182
  let unmappedInteractionEvidence = [];
11183
+ let authorityConfigs;
10849
11184
  let availability = ROLES_NOT_RESOLVED;
10850
11185
  if (opts.task !== void 0) {
10851
11186
  const registry = TASKS[opts.task];
@@ -10859,21 +11194,21 @@ async function runVerify(opts) {
10859
11194
  task = registry;
10860
11195
  } else if (manifest !== void 0) {
10861
11196
  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);
11197
+ if (path34.isAbsolute(p)) return p;
11198
+ const fromRepo = path34.resolve(REPO_ROOT, p);
11199
+ if (existsSync27(fromRepo)) return fromRepo;
11200
+ return path34.resolve(callerCwd, p);
10866
11201
  };
10867
11202
  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))) {
11203
+ if (!existsSync27(path34.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path34.resolve(t.set) === path34.resolve(setDir))) {
10869
11204
  fail(opts, ExitCode.RecordingIncomplete, {
10870
11205
  error: `recording set not found or unmanifested: ${setDir}`,
10871
11206
  code: "recording-set-missing",
10872
11207
  remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
10873
11208
  });
10874
11209
  }
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"))) {
11210
+ const registry = Object.values(TASKS).find((t) => path34.resolve(t.set) === path34.resolve(setDir));
11211
+ if (registry !== void 0 && !existsSync27(path34.join(setDir, "recording-set.json"))) {
10877
11212
  const recordedSlugs = registry.configs.map((c) => c.rep);
10878
11213
  const adapterSlugs = Object.keys(manifest.propAdapter);
10879
11214
  unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
@@ -10882,10 +11217,12 @@ async function runVerify(opts) {
10882
11217
  entry: manifest.entry,
10883
11218
  configs: adapterSlugs.filter((s) => recordedSlugs.includes(s)).map((rep) => ({ rep, component: manifest.propAdapter[rep].component, props: manifest.propAdapter[rep].props }))
10884
11219
  };
11220
+ authorityConfigs = registry.configs;
10885
11221
  } else {
10886
11222
  const built = taskFromManifest(opts, manifest, setDir);
10887
11223
  task = built.task;
10888
11224
  unmapped = built.unmapped;
11225
+ authorityConfigs = built.authorityConfigs;
10889
11226
  interactionEvidence = built.interactionEvidence;
10890
11227
  unmappedInteractionEvidence = built.unmappedInteractionEvidence;
10891
11228
  for (const s of built.adapterOnly) warn(opts, `prop adapter maps "${s}" which is not in the recording set \u2014 ignored`);
@@ -10897,9 +11234,9 @@ async function runVerify(opts) {
10897
11234
  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
11235
  }
10899
11236
  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)));
11237
+ const p = path34.join(opts.bundleDir, name);
11238
+ if (!existsSync27(p)) continue;
11239
+ const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync23(p)));
10903
11240
  if (issues.length > 0) {
10904
11241
  fail(opts, ExitCode.InputValidation, {
10905
11242
  error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -10937,7 +11274,7 @@ async function runVerify(opts) {
10937
11274
  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
11275
  }
10939
11276
  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"))
11277
+ (c) => !existsSync27(path34.join(task.set, c.rep, "get_screenshot.json")) || !existsSync27(path34.join(task.set, c.rep, "get_metadata.json"))
10941
11278
  );
10942
11279
  if (missing.length > 0) {
10943
11280
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -10947,12 +11284,12 @@ async function runVerify(opts) {
10947
11284
  });
10948
11285
  }
10949
11286
  const bar = BARS2[opts.bar];
10950
- const evidenceDir = path33.join(opts.bundleDir, "verify-evidence");
11287
+ const evidenceDir = path34.join(opts.bundleDir, "verify-evidence");
10951
11288
  const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
10952
11289
  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");
11290
+ 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
11291
  const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
10955
- const parity = await checkHoverParity(task, opts.bundleDir);
11292
+ const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
10956
11293
  const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
10957
11294
  const structural = roles !== void 0 ? await checkStructuralComposition(task, opts.bundleDir, roles) : [];
10958
11295
  const regionsOut = roles !== void 0 ? interiorRegions(task.set, roles) : void 0;
@@ -11142,7 +11479,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
11142
11479
  }
11143
11480
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
11144
11481
  `);
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")));
11482
+ 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
11483
  if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
11147
11484
  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
11485
  `);
@@ -11232,18 +11569,18 @@ __export(engine_exports, {
11232
11569
  runEngineBrief: () => runEngineBrief,
11233
11570
  runEngineScore: () => runEngineScore
11234
11571
  });
11235
- import { appendFileSync, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
11236
- import path34 from "node:path";
11572
+ import { appendFileSync, existsSync as existsSync28, mkdirSync as mkdirSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync12 } from "node:fs";
11573
+ import path35 from "node:path";
11237
11574
  function resolveEngineTask(opts, callerCwd) {
11238
- const asPath = path34.resolve(callerCwd, opts.taskOrSet);
11239
- const isSet = existsSync27(path34.join(asPath, "recording-set.json"));
11575
+ const asPath = path35.resolve(callerCwd, opts.taskOrSet);
11576
+ const isSet = existsSync28(path35.join(asPath, "recording-set.json"));
11240
11577
  const registry = TASKS[opts.taskOrSet];
11241
11578
  if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
11242
11579
  if (isSet) {
11243
11580
  try {
11244
11581
  const authored = authorTaskFromSet(asPath);
11245
11582
  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 } };
11583
+ 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
11584
  } catch (err) {
11248
11585
  fail(opts, ExitCode.InputValidation, {
11249
11586
  error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -11270,9 +11607,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
11270
11607
  const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
11271
11608
  const segments = buildSegments(task, "files");
11272
11609
  let notRecorded;
11273
- const manifestPath2 = path34.join(task.set, "recording-set.json");
11274
- if (existsSync27(manifestPath2)) {
11275
- notRecorded = JSON.parse(readFileSync23(manifestPath2, "utf8")).notRecorded;
11610
+ const manifestPath2 = path35.join(task.set, "recording-set.json");
11611
+ if (existsSync28(manifestPath2)) {
11612
+ notRecorded = JSON.parse(readFileSync24(manifestPath2, "utf8")).notRecorded;
11276
11613
  }
11277
11614
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
11278
11615
 
@@ -11280,7 +11617,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
11280
11617
  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
11618
  ${notRecorded}` : "";
11282
11619
  let fontProvisioning;
11283
- if (existsSync27(manifestPath2)) {
11620
+ if (existsSync28(manifestPath2)) {
11284
11621
  const missingFams = unprovisionedFamilies(task.set);
11285
11622
  const unprovided = unprovisionedFaces(task.set);
11286
11623
  const weightOnly = missingFams.length === 0;
@@ -11306,9 +11643,9 @@ ${notRecorded}` : "";
11306
11643
 
11307
11644
  === TASK PAYLOAD (recorded truth, verbatim) ===
11308
11645
  ${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 });
11646
+ const payloadFile = path35.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
11647
+ const candidateDirSuggestion = path35.resolve(callerCwd, `tendril-out/${name}-candidate`);
11648
+ mkdirSync8(path35.dirname(payloadFile), { recursive: true });
11312
11649
  writeFileSync12(payloadFile, payload);
11313
11650
  emitData(
11314
11651
  opts,
@@ -11359,15 +11696,15 @@ ${segments}`;
11359
11696
  );
11360
11697
  }
11361
11698
  function appendScoreHistory(candidateDir, entry) {
11362
- appendFileSync(path34.join(candidateDir, "score-history.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
11699
+ appendFileSync(path35.join(candidateDir, "score-history.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
11363
11700
  `);
11364
11701
  }
11365
11702
  async function runEngineScore(opts) {
11366
11703
  requireEntitlement(opts);
11367
11704
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
11368
- const candidateDir = path34.resolve(callerCwd, opts.candidateDir);
11705
+ const candidateDir = path35.resolve(callerCwd, opts.candidateDir);
11369
11706
  const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
11370
- if (!existsSync27(candidateDir)) {
11707
+ if (!existsSync28(candidateDir)) {
11371
11708
  fail(opts, ExitCode.InputValidation, {
11372
11709
  error: `candidate directory not found: ${candidateDir}`,
11373
11710
  code: "candidate-missing",
@@ -11392,10 +11729,10 @@ async function runEngineScore(opts) {
11392
11729
  for (const g of missingWeights(task.set)) {
11393
11730
  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
11731
  }
11395
- if (opts.rebind !== true && existsSync27(path34.join(candidateDir, "component.json"))) {
11732
+ if (opts.rebind !== true && existsSync28(path35.join(candidateDir, "component.json"))) {
11396
11733
  const prior = (() => {
11397
11734
  try {
11398
- const read = readBundleManifest(readFileSync23(path34.join(candidateDir, "component.json"), "utf8"));
11735
+ const read = readBundleManifest(readFileSync24(path35.join(candidateDir, "component.json"), "utf8"));
11399
11736
  return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
11400
11737
  } catch {
11401
11738
  return { unreadable: true };
@@ -11417,9 +11754,9 @@ async function runEngineScore(opts) {
11417
11754
  }
11418
11755
  }
11419
11756
  const bar = BARS3[opts.bar];
11420
- const evidenceDir = path34.join(candidateDir, "verify-evidence");
11757
+ const evidenceDir = path35.join(candidateDir, "verify-evidence");
11421
11758
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
11422
- const parity = await checkHoverParity(task, candidateDir);
11759
+ const parity = await checkHoverParity(task, candidateDir, task.configs);
11423
11760
  const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
11424
11761
  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
11762
  const obj = objective(scores, behaviors);
@@ -11460,7 +11797,17 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
11460
11797
  environment: environmentStamp(taskFontFamilies(task.set)),
11461
11798
  substitutedFamilies
11462
11799
  });
11463
- appendScoreHistory(candidateDir, { event: "round-scored", bar: opts.bar, pass: obj[0], total, certified: certifiedReps.length, floor: obj[1], mean: obj[2] });
11800
+ appendScoreHistory(candidateDir, {
11801
+ event: "round-scored",
11802
+ bar: opts.bar,
11803
+ pass: obj[0],
11804
+ total,
11805
+ pixelConfigsPassing: scores.filter((s) => s.pass).length,
11806
+ pixelConfigs: scores.length,
11807
+ certified: certifiedReps.length,
11808
+ floor: obj[1],
11809
+ mean: obj[2]
11810
+ });
11464
11811
  const coverage = interactionCoverage(behaviors);
11465
11812
  const operability = operabilityReport({ behaviors, interactionEvidence });
11466
11813
  const evidenceUnverified = (interactionEvidence?.length ?? 0) > 0 && coverage.interactionChecks === 0;
@@ -11575,11 +11922,11 @@ var codeconnect_exports = {};
11575
11922
  __export(codeconnect_exports, {
11576
11923
  runCodeConnect: () => runCodeConnect
11577
11924
  });
11578
- import { existsSync as existsSync28, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
11579
- import path35 from "node:path";
11925
+ import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
11926
+ import path36 from "node:path";
11580
11927
  function runCodeConnect(opts) {
11581
11928
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
11582
- const bundleDir = path35.resolve(callerCwd, opts.bundleDir);
11929
+ const bundleDir = path36.resolve(callerCwd, opts.bundleDir);
11583
11930
  let url;
11584
11931
  try {
11585
11932
  url = new URL(opts.figmaUrl);
@@ -11595,7 +11942,7 @@ function runCodeConnect(opts) {
11595
11942
  }
11596
11943
  let manifest;
11597
11944
  try {
11598
- const read = readBundleManifest(readFileSync24(path35.join(bundleDir, "component.json"), "utf8"));
11945
+ const read = readBundleManifest(readFileSync25(path36.join(bundleDir, "component.json"), "utf8"));
11599
11946
  if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
11600
11947
  manifest = read.manifest;
11601
11948
  } catch (err) {
@@ -11605,8 +11952,8 @@ function runCodeConnect(opts) {
11605
11952
  remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
11606
11953
  });
11607
11954
  }
11608
- const setDir = path35.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
11609
- if (!existsSync28(path35.join(setDir, "recording-set.json"))) {
11955
+ const setDir = path36.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
11956
+ if (!existsSync29(path36.join(setDir, "recording-set.json"))) {
11610
11957
  fail(opts, ExitCode.InputValidation, {
11611
11958
  error: `recording set not found at ${setDir}`,
11612
11959
  code: "codeconnect-no-set",
@@ -11627,10 +11974,10 @@ function runCodeConnect(opts) {
11627
11974
  const component = api.component;
11628
11975
  const recManifest = loadManifest(setDir);
11629
11976
  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;
11977
+ const meta = path36.join(setDir, r.slug, "get_metadata.json");
11978
+ if (!existsSync29(meta)) return void 0;
11632
11979
  try {
11633
- return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync24(meta, "utf8"))))?.[1];
11980
+ return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync25(meta, "utf8"))))?.[1];
11634
11981
  } catch {
11635
11982
  return void 0;
11636
11983
  }
@@ -11688,7 +12035,7 @@ function runCodeConnect(opts) {
11688
12035
  axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
11689
12036
  fragmentVars.push(varName);
11690
12037
  }
11691
- const entryRel = path35.relative(callerCwd, path35.join(bundleDir, manifest.entry));
12038
+ const entryRel = path36.relative(callerCwd, path36.join(bundleDir, manifest.entry));
11692
12039
  const trust = manifest.trustStatement.split("\n")[0] ?? "";
11693
12040
  const lines = [
11694
12041
  `// url=${opts.figmaUrl}`,
@@ -11709,7 +12056,7 @@ function runCodeConnect(opts) {
11709
12056
  `}`,
11710
12057
  ``
11711
12058
  ].join("\n");
11712
- const outFile = path35.resolve(callerCwd, opts.out ?? path35.join(bundleDir, `${component}.figma.ts`));
12059
+ const outFile = path36.resolve(callerCwd, opts.out ?? path36.join(bundleDir, `${component}.figma.ts`));
11713
12060
  writeFileSync13(outFile, lines);
11714
12061
  emitData(
11715
12062
  opts,
@@ -11749,17 +12096,17 @@ var init_codeconnect = __esm({
11749
12096
 
11750
12097
  // packages/mcp/src/server.ts
11751
12098
  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";
12099
+ import { existsSync as existsSync30, mkdtempSync as mkdtempSync3, readFileSync as readFileSync26, readdirSync as readdirSync9, writeFileSync as writeFileSync14 } from "node:fs";
12100
+ import os7 from "node:os";
12101
+ import path37 from "node:path";
11755
12102
  import { fileURLToPath as fileURLToPath6 } from "node:url";
11756
12103
  import { z as z12 } from "zod";
11757
12104
  function sourceHash() {
11758
- const dir = path36.dirname(fileURLToPath6(import.meta.url));
12105
+ const dir = path37.dirname(fileURLToPath6(import.meta.url));
11759
12106
  const h = createHash5("sha256");
11760
- for (const f of readdirSync8(dir).filter((n) => n.endsWith(".ts")).sort()) {
12107
+ for (const f of readdirSync9(dir).filter((n) => n.endsWith(".ts")).sort()) {
11761
12108
  h.update(f);
11762
- h.update(readFileSync25(path36.join(dir, f)));
12109
+ h.update(readFileSync26(path37.join(dir, f)));
11763
12110
  }
11764
12111
  return h.digest("hex").slice(0, 16);
11765
12112
  }
@@ -11767,10 +12114,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
11767
12114
  var init_server = __esm({
11768
12115
  "packages/mcp/src/server.ts"() {
11769
12116
  "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] };
12117
+ REPO_ROOT3 = path37.resolve(path37.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
12118
+ CLI_BIN = path37.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
12119
+ BUNDLED_CLI = path37.join(path37.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
12120
+ CLI_SPAWN = existsSync30(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
11774
12121
  str = (d) => z12.string().describe(d);
11775
12122
  optStr = (d) => z12.string().optional().describe(d);
11776
12123
  TOOLS = [
@@ -11800,7 +12147,7 @@ var init_server = __esm({
11800
12147
  const single = i["metadata"];
11801
12148
  const parts = i["metadataParts"];
11802
12149
  if (single !== void 0 || parts !== void 0) {
11803
- const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
12150
+ const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
11804
12151
  if (single !== void 0) {
11805
12152
  writeFileSync14(tmp, single);
11806
12153
  argvOut.push("--metadata-raw-file", tmp);
@@ -11817,7 +12164,7 @@ var init_server = __esm({
11817
12164
  },
11818
12165
  {
11819
12166
  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.",
12167
+ 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>__* (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.",
11821
12168
  schema: z12.object({
11822
12169
  write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
11823
12170
  }),
@@ -11871,7 +12218,7 @@ var init_server = __esm({
11871
12218
  const bridge = (label, single, parts) => {
11872
12219
  if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
11873
12220
  if (single === void 0 && parts === void 0) return;
11874
- const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
12221
+ const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
11875
12222
  if (single !== void 0) {
11876
12223
  writeFileSync14(tmp, single);
11877
12224
  argvOut.push(`--${label}-file`, tmp);
@@ -11914,7 +12261,7 @@ var init_server = __esm({
11914
12261
  const file = i["file"];
11915
12262
  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
12263
  if (file !== void 0) return [...base, "--file", file];
11917
- const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
12264
+ const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
11918
12265
  if (text !== void 0) {
11919
12266
  writeFileSync14(tmp, text);
11920
12267
  return [...base, "--file", tmp, "--raw"];
@@ -11977,7 +12324,7 @@ var init_server = __esm({
11977
12324
  },
11978
12325
  {
11979
12326
  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.",
12327
+ 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
12328
  schema: z12.object({
11982
12329
  taskOrSet: str("reference task name or recording-set directory"),
11983
12330
  candidateDir: str("directory containing the proposed bundle files"),
@@ -12067,15 +12414,16 @@ __export(permissions_exports, {
12067
12414
  PERMISSIONS_DESCRIPTION: () => PERMISSIONS_DESCRIPTION,
12068
12415
  buildPermissions: () => buildPermissions,
12069
12416
  mergeAllowlist: () => mergeAllowlist,
12070
- runPermissions: () => runPermissions
12417
+ runPermissions: () => runPermissions,
12418
+ writeSelection: () => writeSelection
12071
12419
  });
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) {
12420
+ import { existsSync as existsSync31, mkdirSync as mkdirSync9, readFileSync as readFileSync27, writeFileSync as writeFileSync15 } from "node:fs";
12421
+ import os8 from "node:os";
12422
+ import path38 from "node:path";
12423
+ function mergeAllowlist(file, entries, denyEntries = []) {
12076
12424
  let settings = {};
12077
- if (existsSync30(file) && readFileSync26(file, "utf8").trim() !== "") {
12078
- settings = JSON.parse(readFileSync26(file, "utf8"));
12425
+ if (existsSync31(file) && readFileSync27(file, "utf8").trim() !== "") {
12426
+ settings = JSON.parse(readFileSync27(file, "utf8"));
12079
12427
  if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
12080
12428
  }
12081
12429
  const permissions = settings["permissions"] ??= {};
@@ -12085,13 +12433,21 @@ function mergeAllowlist(file, entries) {
12085
12433
  const present = new Set(allow.filter((x) => typeof x === "string"));
12086
12434
  const added = entries.filter((e) => !present.has(e));
12087
12435
  const alreadyPresent = entries.filter((e) => present.has(e));
12088
- if (added.length > 0) {
12436
+ let denyAdded = [];
12437
+ if (denyEntries.length > 0) {
12438
+ const deny = permissions["deny"] ??= [];
12439
+ if (!Array.isArray(deny)) throw new Error("permissions.deny is not an array");
12440
+ const denyPresent = new Set(deny.filter((x) => typeof x === "string"));
12441
+ denyAdded = denyEntries.filter((e) => !denyPresent.has(e));
12442
+ deny.push(...denyAdded);
12443
+ }
12444
+ if (added.length > 0 || denyAdded.length > 0) {
12089
12445
  allow.push(...added);
12090
- mkdirSync9(path37.dirname(file), { recursive: true });
12446
+ mkdirSync9(path38.dirname(file), { recursive: true });
12091
12447
  writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
12092
12448
  `);
12093
12449
  }
12094
- return { added, alreadyPresent };
12450
+ return { added, alreadyPresent, denyAdded };
12095
12451
  }
12096
12452
  async function buildPermissions(options) {
12097
12453
  const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
@@ -12111,10 +12467,18 @@ async function buildPermissions(options) {
12111
12467
  ...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${TENDRIL_PLUGIN_PREFIX}__${n}`),
12112
12468
  ...figmaTools.map((name) => `${FIGMA_PLUGIN_PREFIX}__${name}`)
12113
12469
  ],
12470
+ shellEntries: [...SHELL_READONLY_ENTRIES, ...SHELL_PIPELINE_ENTRIES],
12471
+ projectFileEntries: PROJECT_FILE_ENTRIES,
12472
+ projectDenyEntries: PROJECT_DENY_ENTRIES,
12473
+ manualCautionEntries: MANUAL_CAUTION_ENTRIES,
12474
+ 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
12475
  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
12476
  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).`
12116
12477
  };
12117
12478
  }
12479
+ function writeSelection(result, user) {
12480
+ return user ? { entries: [...result.toolEntries], denyEntries: [] } : { entries: [...result.toolEntries, ...result.shellEntries, ...result.projectFileEntries], denyEntries: [...result.projectDenyEntries] };
12481
+ }
12118
12482
  async function runPermissions(flags) {
12119
12483
  if (flags.describe) {
12120
12484
  printDescription(PERMISSIONS_DESCRIPTION);
@@ -12123,21 +12487,25 @@ async function runPermissions(flags) {
12123
12487
  const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
12124
12488
  if (flags.write) {
12125
12489
  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");
12490
+ const file = flags.user ? path38.join(os8.homedir(), ".claude", "settings.json") : path38.join(base, ".claude", "settings.local.json");
12491
+ const { entries, denyEntries } = writeSelection(result, flags.user === true);
12127
12492
  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
- `);
12493
+ emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
12494
+ process.stdout.write(
12495
+ `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}
12496
+ `
12497
+ );
12131
12498
  });
12132
12499
  return;
12133
12500
  }
12134
12501
  try {
12135
- const { added, alreadyPresent } = mergeAllowlist(file, result.toolEntries);
12136
- emitData(flags, { ...result, written: { file, added, alreadyPresent } }, () => {
12502
+ const { added, alreadyPresent, denyAdded } = mergeAllowlist(file, entries, denyEntries);
12503
+ emitData(flags, { ...result, written: { file, added, alreadyPresent, denyAdded } }, () => {
12137
12504
  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
12505
+ added.length === 0 && denyAdded.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
12506
+ ` : `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)` : ""}
12507
+ ${flags.user ? "" : ` \u26A0 ${result.shellEntriesNote}
12508
+ `} restart or reopen the Claude Code session to pick up settings changes
12141
12509
  `
12142
12510
  );
12143
12511
  });
@@ -12162,6 +12530,24 @@ Or paste into .claude/settings.json under permissions.allow:
12162
12530
 
12163
12531
  ${quoted(result.toolEntries)}
12164
12532
 
12533
+ The SHELL surface a run actually uses:
12534
+
12535
+ ${quoted(result.shellEntries)}
12536
+
12537
+ \u26A0 ${result.shellEntriesNote}
12538
+
12539
+ Project-scoped file access (written only to the project-local file):
12540
+
12541
+ ${quoted(result.projectFileEntries)}
12542
+
12543
+ \u2026with these under permissions.deny (so the grant above can never rewrite the settings file or .git):
12544
+
12545
+ ${quoted(result.projectDenyEntries)}
12546
+
12547
+ Deliberate manual additions \u2014 clobber- or exec-capable, never auto-written:
12548
+
12549
+ ${quoted(result.manualCautionEntries)}
12550
+
12165
12551
  Shorter but broader \u2014 one entry per server:
12166
12552
 
12167
12553
  ${quoted(result.serverEntries)}
@@ -12173,7 +12559,7 @@ ${result.directConfigNote}
12173
12559
  );
12174
12560
  });
12175
12561
  }
12176
- var FIGMA_TOOL_FALLBACK, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX;
12562
+ 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;
12177
12563
  var init_permissions = __esm({
12178
12564
  "packages/cli/src/commands/permissions.ts"() {
12179
12565
  "use strict";
@@ -12185,14 +12571,44 @@ var init_permissions = __esm({
12185
12571
  init_output();
12186
12572
  init_doctor();
12187
12573
  FIGMA_TOOL_FALLBACK = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_motion_context", "get_figjam"];
12574
+ SHELL_READONLY_ENTRIES = [
12575
+ "Bash(date:*)",
12576
+ "Bash(ls:*)",
12577
+ "Bash(stat:*)",
12578
+ "Bash(wc:*)",
12579
+ "Bash(head:*)",
12580
+ "Bash(tail:*)",
12581
+ "Bash(cat:*)",
12582
+ "Bash(shasum:*)",
12583
+ "Bash(grep:*)"
12584
+ ];
12585
+ SHELL_PIPELINE_ENTRIES = [
12586
+ // The skill's own rule: candidate dirs are created with a bare
12587
+ // `mkdir -p`, no compounds — one grant covers the whole batch.
12588
+ "Bash(mkdir -p:*)",
12589
+ // The published CLI, PER SUBCOMMAND — never `tendril *`. The CLI
12590
+ // writes the settings file through Node fs, not the host's Write
12591
+ // tool, so the protected-path guard that stops `Write(./**)` from
12592
+ // touching .claude/ does NOT stop `tendril permissions --write`: a
12593
+ // blanket grant would let an allowlisted agent silently widen its own
12594
+ // allowlist (review finding, confirmed from the host's docs).
12595
+ // `permissions`, `activate` and `init` are deliberately absent.
12596
+ ...["verify", "inspect", "doctor", "record", "engine", "fonts", "generate"].flatMap((sub) => [
12597
+ `Bash(tendril ${sub}:*)`,
12598
+ `Bash(npx -y -p @tendrilapp/cli@latest tendril ${sub}:*)`
12599
+ ])
12600
+ ];
12601
+ PROJECT_FILE_ENTRIES = ["Write(./**)", "Edit(./**)"];
12602
+ PROJECT_DENY_ENTRIES = ["Write(./.claude/**)", "Edit(./.claude/**)", "Write(./.git/**)", "Edit(./.git/**)"];
12603
+ MANUAL_CAUTION_ENTRIES = ["Bash(cp -R:*)", "Bash(find:*)"];
12188
12604
  PERMISSIONS_DESCRIPTION = {
12189
12605
  name: "permissions",
12190
12606
  summary: "Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools).",
12191
12607
  args: [],
12192
12608
  flags: [
12193
12609
  { 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" },
12610
+ { 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)" },
12611
+ { 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
12612
  { flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
12197
12613
  { flag: "--json", description: "Machine-readable output" }
12198
12614
  ],
@@ -12201,7 +12617,7 @@ var init_permissions = __esm({
12201
12617
  serverEntries: "string[] \u2014 one entry per MCP server (allows every tool it serves)",
12202
12618
  toolEntries: "string[] \u2014 per-tool entries for selective allowlists",
12203
12619
  directConfigNote: "string \u2014 prefix rewrite for non-plugin (claude mcp add) installs",
12204
- written: "with --write: { file, added, alreadyPresent }"
12620
+ written: "with --write: { file, added, alreadyPresent, denyAdded }"
12205
12621
  },
12206
12622
  exitCodes: { 0: "printed or written", 3: "with --write: the target file exists but is not JSON this command can safely rewrite" },
12207
12623
  examples: ["tendril permissions --claude --write", "tendril permissions --claude", "tendril permissions --claude --json"]
@@ -12217,24 +12633,24 @@ __export(inspect_exports, {
12217
12633
  INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
12218
12634
  runInspect: () => runInspect
12219
12635
  });
12220
- import { existsSync as existsSync31, readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
12221
- import path38 from "node:path";
12636
+ import { existsSync as existsSync32, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
12637
+ import path39 from "node:path";
12222
12638
  async function runInspect(opts) {
12223
12639
  if (opts.describe) {
12224
12640
  printDescription(INSPECT_DESCRIPTION);
12225
12641
  return;
12226
12642
  }
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)) {
12643
+ const bundleDir = path39.resolve(opts.bundleDir);
12644
+ const evidenceDir = path39.join(bundleDir, "verify-evidence");
12645
+ const manifestPath2 = path39.join(bundleDir, "component.json");
12646
+ if (!existsSync32(evidenceDir) || !existsSync32(manifestPath2)) {
12231
12647
  fail(opts, ExitCode.InputValidation, {
12232
- error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync31(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
12648
+ error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync32(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
12233
12649
  code: "no-evidence",
12234
12650
  remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
12235
12651
  });
12236
12652
  }
12237
- const { manifest } = readBundleManifest(readFileSync27(manifestPath2, "utf8"));
12653
+ const { manifest } = readBundleManifest(readFileSync28(manifestPath2, "utf8"));
12238
12654
  if (manifest === void 0) {
12239
12655
  fail(opts, ExitCode.InputValidation, {
12240
12656
  error: "component.json did not parse as a bundle manifest",
@@ -12242,8 +12658,8 @@ async function runInspect(opts) {
12242
12658
  remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
12243
12659
  });
12244
12660
  }
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`)));
12661
+ const setDir = path39.resolve(opts.set ?? manifest.provenance.recordingSet.path);
12662
+ const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync32(path39.join(evidenceDir, `${rep}-ref.png`)) && existsSync32(path39.join(evidenceDir, `${rep}-render.png`)));
12247
12663
  if (reps.length === 0) {
12248
12664
  fail(opts, ExitCode.InputValidation, {
12249
12665
  error: "verify-evidence holds no ref/render pairs for this bundle's configs",
@@ -12254,15 +12670,15 @@ async function runInspect(opts) {
12254
12670
  let crops = 0;
12255
12671
  const sections = [];
12256
12672
  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`)));
12673
+ const ref = new Uint8Array(readFileSync28(path39.join(evidenceDir, `${rep}-ref.png`)));
12674
+ const render = new Uint8Array(readFileSync28(path39.join(evidenceDir, `${rep}-render.png`)));
12259
12675
  const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
12260
12676
  const cells = [];
12261
12677
  for (const [i, n] of nodes.entries()) {
12262
12678
  const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
12263
12679
  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));
12680
+ writeFileSync16(path39.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
12681
+ writeFileSync16(path39.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
12266
12682
  } catch {
12267
12683
  continue;
12268
12684
  }
@@ -12275,7 +12691,7 @@ async function runInspect(opts) {
12275
12691
  `<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
12692
  );
12277
12693
  }
12278
- const sheet = path38.join(evidenceDir, "inspect.html");
12694
+ const sheet = path39.join(evidenceDir, "inspect.html");
12279
12695
  writeFileSync16(
12280
12696
  sheet,
12281
12697
  `<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
@@ -12353,17 +12769,17 @@ __export(generate_recorded_exports, {
12353
12769
  runGenerateRecorded: () => runGenerateRecorded
12354
12770
  });
12355
12771
  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";
12772
+ import { existsSync as existsSync33, readFileSync as readFileSync29 } from "node:fs";
12773
+ import path40 from "node:path";
12358
12774
  async function runGenerateRecorded(opts) {
12359
12775
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
12360
- const outDirAbs = path39.resolve(callerCwd, opts.out);
12361
- const recordedAsPath = path39.resolve(callerCwd, opts.recorded);
12776
+ const outDirAbs = path40.resolve(callerCwd, opts.out);
12777
+ const recordedAsPath = path40.resolve(callerCwd, opts.recorded);
12362
12778
  let task;
12363
12779
  let taskName;
12364
12780
  let authoredApi;
12365
12781
  let composition;
12366
- const isSet = existsSync32(path39.join(recordedAsPath, "recording-set.json"));
12782
+ const isSet = existsSync33(path40.join(recordedAsPath, "recording-set.json"));
12367
12783
  const registry = TASKS[opts.recorded];
12368
12784
  if (registry !== void 0 && !isSet) {
12369
12785
  task = registry;
@@ -12372,7 +12788,7 @@ async function runGenerateRecorded(opts) {
12372
12788
  try {
12373
12789
  const authored = authorTaskFromSet(recordedAsPath);
12374
12790
  task = authored.task;
12375
- taskName = path39.basename(recordedAsPath);
12791
+ taskName = path40.basename(recordedAsPath);
12376
12792
  authoredApi = authored.api;
12377
12793
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
12378
12794
  if (roles.success) composition = roles.data;
@@ -12406,7 +12822,7 @@ async function runGenerateRecorded(opts) {
12406
12822
  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
12823
  }
12408
12824
  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"))
12825
+ (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
12826
  );
12411
12827
  if (missing.length > 0) {
12412
12828
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -12476,8 +12892,8 @@ async function runGenerateRecorded(opts) {
12476
12892
  ` : `${line}
12477
12893
  `);
12478
12894
  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)})
12895
+ emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path40.join(outDirAbs, taskName) }, () => {
12896
+ process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path40.join(outDirAbs, taskName)})
12481
12897
  `);
12482
12898
  });
12483
12899
  return;
@@ -12500,10 +12916,10 @@ async function runGenerateRecorded(opts) {
12500
12916
  });
12501
12917
  }
12502
12918
  }
12503
- const bundleDir = path39.join(outDirAbs, taskName);
12504
- if (existsSync32(path39.join(bundleDir, "component.json"))) {
12919
+ const bundleDir = path40.join(outDirAbs, taskName);
12920
+ if (existsSync33(path40.join(bundleDir, "component.json"))) {
12505
12921
  try {
12506
- const prior = readBundleManifest(readFileSync28(path39.join(bundleDir, "component.json"), "utf8")).manifest;
12922
+ const prior = readBundleManifest(readFileSync29(path40.join(bundleDir, "component.json"), "utf8")).manifest;
12507
12923
  if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
12508
12924
  fail(opts, ExitCode.InputValidation, {
12509
12925
  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 +13085,7 @@ init_invocation();
12669
13085
  init_output();
12670
13086
  import { intro, isCancel, outro, password } from "@clack/prompts";
12671
13087
  import fs from "node:fs";
12672
- import path24 from "node:path";
13088
+ import path25 from "node:path";
12673
13089
  var INIT_DESCRIPTION = {
12674
13090
  name: "init",
12675
13091
  summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
@@ -12706,7 +13122,7 @@ async function runInit(flags) {
12706
13122
  printDescription(INIT_DESCRIPTION);
12707
13123
  return;
12708
13124
  }
12709
- const envPath = path24.resolve(process.cwd(), ".env");
13125
+ const envPath = path25.resolve(process.cwd(), ".env");
12710
13126
  const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
12711
13127
  let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
12712
13128
  let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
@@ -12727,7 +13143,7 @@ async function runInit(flags) {
12727
13143
  next.set(ENV_KEYS.figma, figmaToken);
12728
13144
  next.set(ENV_KEYS.openrouter, openrouterKey);
12729
13145
  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");
13146
+ const gitignorePath = path25.resolve(process.cwd(), ".gitignore");
12731
13147
  const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
12732
13148
  const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
12733
13149
  if (flags.dryRun) {
@@ -12783,14 +13199,14 @@ init_invocation();
12783
13199
  init_output();
12784
13200
  init_entitlement();
12785
13201
  import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
12786
- import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync19 } from "node:fs";
13202
+ import { readFileSync as readFileSync16, readdirSync as readdirSync5, existsSync as existsSync20 } from "node:fs";
12787
13203
 
12788
13204
  // packages/cli/src/pipeline.ts
12789
13205
  init_src2();
12790
13206
  init_src4();
12791
13207
  init_src6();
12792
13208
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
12793
- import path25 from "node:path";
13209
+ import path26 from "node:path";
12794
13210
 
12795
13211
  // packages/cli/src/assets-module.ts
12796
13212
  init_src();
@@ -13126,7 +13542,7 @@ async function runGenerationPipeline(input) {
13126
13542
  });
13127
13543
  const written = [];
13128
13544
  if (!input.dryRun) {
13129
- const dir = path25.resolve(input.outDir, semantics.componentName);
13545
+ const dir = path26.resolve(input.outDir, semantics.componentName);
13130
13546
  mkdirSync5(dir, { recursive: true });
13131
13547
  const files = {
13132
13548
  // Bundle-local tokens: THE emission the component's CSS resolves
@@ -13150,13 +13566,13 @@ async function runGenerationPipeline(input) {
13150
13566
  `
13151
13567
  };
13152
13568
  for (const [name, content] of Object.entries(files)) {
13153
- const filePath = path25.join(dir, name);
13569
+ const filePath = path26.join(dir, name);
13154
13570
  writeFileSync8(filePath, content);
13155
13571
  written.push(filePath);
13156
13572
  }
13157
13573
  for (const artifact of emitTokenArtifacts(input.mapping)) {
13158
- const filePath = path25.resolve(input.outDir, artifact.path);
13159
- mkdirSync5(path25.dirname(filePath), { recursive: true });
13574
+ const filePath = path26.resolve(input.outDir, artifact.path);
13575
+ mkdirSync5(path26.dirname(filePath), { recursive: true });
13160
13576
  writeFileSync8(filePath, artifact.content);
13161
13577
  written.push(filePath);
13162
13578
  }
@@ -13215,7 +13631,7 @@ var GENERATE_DESCRIPTION = {
13215
13631
  function resolveProvidedSource(flags, contextFile) {
13216
13632
  let raw;
13217
13633
  try {
13218
- raw = readFileSync15(contextFile, "utf8");
13634
+ raw = readFileSync16(contextFile, "utf8");
13219
13635
  } catch {
13220
13636
  fail(flags, ExitCode.InputValidation, {
13221
13637
  error: `Cannot read context file "${contextFile}".`,
@@ -13335,11 +13751,11 @@ token mapping (${mapping.flat.length} variables):
13335
13751
  let initialCode;
13336
13752
  let initialSemantics;
13337
13753
  try {
13338
- if (existsSync19(flags.out)) {
13339
- for (const entry of readdirSync4(flags.out)) {
13754
+ if (existsSync20(flags.out)) {
13755
+ for (const entry of readdirSync5(flags.out)) {
13340
13756
  const cjPath = `${flags.out}/${entry}/component.json`;
13341
- if (!existsSync19(cjPath)) continue;
13342
- const cj = JSON.parse(readFileSync15(cjPath, "utf8"));
13757
+ if (!existsSync20(cjPath)) continue;
13758
+ const cj = JSON.parse(readFileSync16(cjPath, "utf8"));
13343
13759
  if (cj.name !== void 0 && Array.isArray(cj.props)) {
13344
13760
  previousApi = JSON.stringify({
13345
13761
  componentName: cj.name,
@@ -13347,14 +13763,14 @@ token mapping (${mapping.flat.length} variables):
13347
13763
  });
13348
13764
  const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
13349
13765
  const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
13350
- if (flags.refine && existsSync19(tsxPath) && existsSync19(cssPath)) {
13766
+ if (flags.refine && existsSync20(tsxPath) && existsSync20(cssPath)) {
13351
13767
  initialCode = {
13352
- tsx: readFileSync15(tsxPath, "utf8"),
13353
- css: readFileSync15(cssPath, "utf8")
13768
+ tsx: readFileSync16(tsxPath, "utf8"),
13769
+ css: readFileSync16(cssPath, "utf8")
13354
13770
  };
13355
13771
  const semPath = `${flags.out}/${entry}/semantics.json`;
13356
- if (existsSync19(semPath)) {
13357
- initialSemantics = JSON.parse(readFileSync15(semPath, "utf8"));
13772
+ if (existsSync20(semPath)) {
13773
+ initialSemantics = JSON.parse(readFileSync16(semPath, "utf8"));
13358
13774
  }
13359
13775
  }
13360
13776
  break;
@@ -13631,6 +14047,23 @@ function buildProgram() {
13631
14047
  ...local["face"] !== void 0 ? { face: Number(local["face"]) } : {}
13632
14048
  });
13633
14049
  });
14050
+ 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) => {
14051
+ const flags = globalFlags(cmd.parent.parent);
14052
+ const { runFontsDiscover: runFontsDiscover2 } = await Promise.resolve().then(() => (init_fonts(), fonts_exports));
14053
+ runFontsDiscover2({ ...flags, cacheDir: cmd.opts()["cache"] ?? DEFAULT_FONT_CACHE, ...family !== void 0 ? { family } : {} });
14054
+ });
14055
+ 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) => {
14056
+ const flags = globalFlags(cmd.parent.parent);
14057
+ const local = cmd.opts();
14058
+ const { runFontsAddSystem: runFontsAddSystem2 } = await Promise.resolve().then(() => (init_fonts(), fonts_exports));
14059
+ runFontsAddSystem2({
14060
+ ...flags,
14061
+ family,
14062
+ cacheDir: local["cache"] ?? DEFAULT_FONT_CACHE,
14063
+ ...local["weights"] !== void 0 ? { weights: local["weights"].map(Number) } : {},
14064
+ ...local["set"] !== void 0 ? { set: local["set"] } : {}
14065
+ });
14066
+ });
13634
14067
  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
14068
  const flags = globalFlags(cmd.parent.parent);
13636
14069
  const local = cmd.opts();