@tendrilapp/cli 0.1.26 → 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
@@ -1028,6 +1028,14 @@ function planSet(setDir, component, symbols, opts = {}) {
1028
1028
  const { manifest: existing, raw } = readManifestFile(setDir);
1029
1029
  const wantsNewDefaults = opts.defaults !== void 0 && JSON.stringify(opts.defaults) !== JSON.stringify(existing.defaults ?? {});
1030
1030
  const anythingRecorded = existing.reps.some((r) => RECORD_TOOLS.some((t) => existsSync(path.join(setDir, r.slug, `${t}.json`))));
1031
+ const scopeUpgraded = opts.variantScope === "component-set" && existing.variantScope !== "component-set";
1032
+ if (opts.variantScope === "component-set") {
1033
+ raw["variantScope"] = "component-set";
1034
+ const variants = symbols.map((s) => s.name).filter((n) => n.includes("="));
1035
+ if (variants.length > 0 && (scopeUpgraded || variants.length > (existing.latticeNames ?? []).length)) {
1036
+ raw["latticeNames"] = variants;
1037
+ }
1038
+ }
1031
1039
  if (!wantsNewDefaults || anythingRecorded) {
1032
1040
  if (opts.sample !== true) {
1033
1041
  const known = new Set(existing.reps.map((r) => r.nodeId));
@@ -1049,6 +1057,10 @@ function planSet(setDir, component, symbols, opts = {}) {
1049
1057
  return { manifest: manifest2, plan: { reps: [], notRecorded: [] }, resumed: true, toppedUp: appended.map((a) => ({ slug: a.slug, nodeId: a.nodeId })) };
1050
1058
  }
1051
1059
  }
1060
+ if (scopeUpgraded) {
1061
+ const manifest2 = writeManifest(setDir, raw);
1062
+ return { manifest: manifest2, plan: { reps: [], notRecorded: [] }, resumed: true };
1063
+ }
1052
1064
  return { manifest: existing, plan: { reps: [], notRecorded: [] }, resumed: true };
1053
1065
  }
1054
1066
  }
@@ -1325,8 +1337,8 @@ var init_src = __esm({
1325
1337
  function variableNameToPath(name) {
1326
1338
  return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
1327
1339
  }
1328
- function tokenPathToCssVar(path40) {
1329
- return `--${path40.join("-")}`;
1340
+ function tokenPathToCssVar(path41) {
1341
+ return `--${path41.join("-")}`;
1330
1342
  }
1331
1343
  function toDtcgToken(variable, defaultMode) {
1332
1344
  const modes = Object.keys(variable.valuesByMode);
@@ -1370,11 +1382,11 @@ function toDtcgToken(variable, defaultMode) {
1370
1382
  }
1371
1383
  function mapVariablesToDtcg(variables, defaultMode = "light") {
1372
1384
  const entries = variables.map((variable) => {
1373
- const path40 = variableNameToPath(variable.name);
1374
- if (path40.length === 0) {
1385
+ const path41 = variableNameToPath(variable.name);
1386
+ if (path41.length === 0) {
1375
1387
  throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
1376
1388
  }
1377
- return { variable, path: path40 };
1389
+ return { variable, path: path41 };
1378
1390
  });
1379
1391
  const groupPrefixes = /* @__PURE__ */ new Set();
1380
1392
  for (const e of entries) {
@@ -1395,21 +1407,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
1395
1407
  }
1396
1408
  const tokens = {};
1397
1409
  const flat = [];
1398
- for (const { variable, path: path40 } of entries) {
1410
+ for (const { variable, path: path41 } of entries) {
1399
1411
  const token = toDtcgToken(variable, defaultMode);
1400
1412
  let group = tokens;
1401
- for (const segment of path40.slice(0, -1)) {
1413
+ for (const segment of path41.slice(0, -1)) {
1402
1414
  const existing = group[segment];
1403
1415
  group = existing ?? (group[segment] = {});
1404
1416
  }
1405
- const leaf = path40[path40.length - 1];
1417
+ const leaf = path41[path41.length - 1];
1406
1418
  if (group[leaf] !== void 0) {
1407
- 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})`);
1408
1420
  }
1409
1421
  group[leaf] = token;
1410
1422
  flat.push({
1411
- path: path40.join("."),
1412
- cssVar: tokenPathToCssVar(path40),
1423
+ path: path41.join("."),
1424
+ cssVar: tokenPathToCssVar(path41),
1413
1425
  type: token.$type,
1414
1426
  value: token.$value
1415
1427
  });
@@ -1598,9 +1610,9 @@ function boundId(value) {
1598
1610
  return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
1599
1611
  }
1600
1612
  function resolveBinding(ctx, id) {
1601
- const path40 = ctx.pathById.get(id);
1602
- if (path40 === void 0) ctx.unresolved.add(id);
1603
- return path40;
1613
+ const path41 = ctx.pathById.get(id);
1614
+ if (path41 === void 0) ctx.unresolved.add(id);
1615
+ return path41;
1604
1616
  }
1605
1617
  function parseVariantProps(name) {
1606
1618
  if (!name.includes("=")) return void 0;
@@ -1635,8 +1647,8 @@ function walk(ctx, raw) {
1635
1647
  if (!isObject(paint) || paint["visible"] === false) continue;
1636
1648
  const id = boundId(paint);
1637
1649
  if (id !== void 0) {
1638
- const path40 = resolveBinding(ctx, id);
1639
- if (path40 !== void 0) tokens.add(path40);
1650
+ const path41 = resolveBinding(ctx, id);
1651
+ if (path41 !== void 0) tokens.add(path41);
1640
1652
  } else if (typeof paint["color"] === "string") {
1641
1653
  ctx.hardcoded.push({ node: name, property, value: paint["color"] });
1642
1654
  }
@@ -1644,8 +1656,8 @@ function walk(ctx, raw) {
1644
1656
  }
1645
1657
  const radiusId = boundId(raw["cornerRadius"]);
1646
1658
  if (radiusId !== void 0) {
1647
- const path40 = resolveBinding(ctx, radiusId);
1648
- if (path40 !== void 0) tokens.add(path40);
1659
+ const path41 = resolveBinding(ctx, radiusId);
1660
+ if (path41 !== void 0) tokens.add(path41);
1649
1661
  } else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
1650
1662
  ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
1651
1663
  }
@@ -1655,10 +1667,10 @@ function walk(ctx, raw) {
1655
1667
  layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
1656
1668
  const gapId = boundId(raw["itemSpacing"]);
1657
1669
  if (gapId !== void 0) {
1658
- const path40 = resolveBinding(ctx, gapId);
1659
- if (path40 !== void 0) {
1660
- layout.gap = path40;
1661
- tokens.add(path40);
1670
+ const path41 = resolveBinding(ctx, gapId);
1671
+ if (path41 !== void 0) {
1672
+ layout.gap = path41;
1673
+ tokens.add(path41);
1662
1674
  }
1663
1675
  } else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
1664
1676
  ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
@@ -1667,10 +1679,10 @@ function walk(ctx, raw) {
1667
1679
  for (const field of PADDING_FIELDS) {
1668
1680
  const id = boundId(raw[field]);
1669
1681
  if (id !== void 0) {
1670
- const path40 = resolveBinding(ctx, id);
1671
- if (path40 !== void 0) {
1672
- paddingPaths.push(path40);
1673
- tokens.add(path40);
1682
+ const path41 = resolveBinding(ctx, id);
1683
+ if (path41 !== void 0) {
1684
+ paddingPaths.push(path41);
1685
+ tokens.add(path41);
1674
1686
  }
1675
1687
  } else if (typeof raw[field] === "number" && raw[field] !== 0) {
1676
1688
  ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
@@ -1921,7 +1933,7 @@ function tendrilCommand(args) {
1921
1933
  return `${tendrilInvocation()} ${args}`;
1922
1934
  }
1923
1935
  function quoteArg(value) {
1924
- if (/["`$\\]/.test(value)) throw new Error(`refusing to emit a shell argument containing quote/backslash/backtick/$: ${value}`);
1936
+ if (/["`$]/.test(value)) throw new Error(`refusing to emit a shell argument containing a double quote, backtick or $: ${value}`);
1925
1937
  return /[\s'*?[\]()&;|<>#~]/.test(value) ? `"${value}"` : value;
1926
1938
  }
1927
1939
  var NPX_INVOCATION, cached;
@@ -3486,6 +3498,7 @@ function isCollection(bytes) {
3486
3498
  }
3487
3499
  function nameTableStrings(view, bytes, nameOffset, nameLength) {
3488
3500
  const out = /* @__PURE__ */ new Map();
3501
+ const rank = /* @__PURE__ */ new Map();
3489
3502
  if (nameOffset + 6 > bytes.length) return out;
3490
3503
  const count = view.getUint16(nameOffset + 2);
3491
3504
  const stringOffset = nameOffset + view.getUint16(nameOffset + 4);
@@ -3499,14 +3512,21 @@ function nameTableStrings(view, bytes, nameOffset, nameLength) {
3499
3512
  const offset = stringOffset + view.getUint16(rec + 10);
3500
3513
  if (offset + length > bytes.length || offset + length > nameOffset + nameLength) continue;
3501
3514
  const slice = bytes.subarray(offset, offset + length);
3502
- const isUtf16 = platformId === 3 || platformId === 0 || encodingId === 1;
3515
+ const languageId = view.getUint16(rec + 4);
3503
3516
  let value = "";
3504
- if (isUtf16) {
3517
+ if (platformId === 3 || platformId === 0) {
3505
3518
  for (let j = 0; j + 1 < slice.length; j += 2) value += String.fromCharCode(slice[j] << 8 | slice[j + 1]);
3506
- } else {
3519
+ } else if (platformId === 1 && encodingId === 0) {
3507
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);
3508
3529
  }
3509
- if (value !== "" && !out.has(nameId)) out.set(nameId, value);
3510
3530
  }
3511
3531
  return out;
3512
3532
  }
@@ -3586,15 +3606,151 @@ var init_font_collection = __esm({
3586
3606
  }
3587
3607
  });
3588
3608
 
3589
- // packages/verify/src/font-resolve.ts
3590
- import { createHash } from "node:crypto";
3591
- 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";
3592
3611
  import os2 from "node:os";
3593
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";
3594
3750
  function fontCacheDir() {
3595
3751
  const env = process.env["TENDRIL_FONT_CACHE"];
3596
- if (env !== void 0 && env !== "") return path10.resolve(env);
3597
- 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");
3598
3754
  }
3599
3755
  function normalizeFontLicense(value) {
3600
3756
  return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
@@ -3659,29 +3815,29 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
3659
3815
  }
3660
3816
  const bytes = new Uint8Array(await fileRes.arrayBuffer());
3661
3817
  const sha256 = createHash("sha256").update(bytes).digest("hex");
3662
- 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`);
3663
3819
  writeFileSync3(file, bytes);
3664
3820
  resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
3665
3821
  } catch (err) {
3666
3822
  failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
3667
3823
  }
3668
3824
  }
3669
- const mPath = path10.join(cacheDir, "manifest.json");
3670
- const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3671
- 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) }));
3672
3828
  const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
3673
3829
  if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
3674
3830
  `);
3675
3831
  return { resolved, failures };
3676
3832
  }
3677
- function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex) {
3678
- const src = path10.resolve(filePath);
3679
- if (!existsSync6(src)) throw new Error(`font file not found: ${src}`);
3680
- 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();
3681
3837
  if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
3682
3838
  throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
3683
3839
  }
3684
- let bytes = new Uint8Array(readFileSync3(src));
3840
+ let bytes = new Uint8Array(readFileSync4(src));
3685
3841
  if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
3686
3842
  let storedExt = ext;
3687
3843
  if (isCollection(bytes)) {
@@ -3691,7 +3847,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
3691
3847
  const all = listCollectionFaces(bytes);
3692
3848
  const shown = (candidates.length > 0 ? candidates : all).map((f) => ` --face ${f.index} ${f.family ?? "(unnamed)"}${f.subfamily !== void 0 ? ` ${f.subfamily}` : ""}`).join("\n");
3693
3849
  throw new Error(
3694
- `${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>:
3695
3851
  ${shown}`
3696
3852
  );
3697
3853
  }
@@ -3700,20 +3856,20 @@ ${shown}`
3700
3856
  }
3701
3857
  mkdirSync2(cacheDir, { recursive: true });
3702
3858
  const sha256 = createHash("sha256").update(bytes).digest("hex");
3703
- 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}`);
3704
3860
  writeFileSync3(file, bytes);
3705
- const face = { family, weight, source: `local:${path10.basename(src)}`, sha256, file, license: "unknown" };
3706
- const mPath = path10.join(cacheDir, "manifest.json");
3707
- const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3708
- 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) }];
3709
3865
  writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
3710
3866
  `);
3711
3867
  return face;
3712
3868
  }
3713
3869
  function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
3714
- const lock = JSON.parse(readFileSync3(lockPath, "utf8"));
3715
- const mPath = path10.join(cacheDir, "manifest.json");
3716
- 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")) : [];
3717
3873
  return lock.map((l) => {
3718
3874
  const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
3719
3875
  if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
@@ -3721,11 +3877,11 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
3721
3877
  });
3722
3878
  }
3723
3879
  function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3724
- const mPath = path10.join(cacheDir, "manifest.json");
3725
- if (!existsSync6(mPath)) return [];
3880
+ const mPath = path11.join(cacheDir, "manifest.json");
3881
+ if (!existsSync7(mPath)) return [];
3726
3882
  let entries;
3727
3883
  try {
3728
- entries = JSON.parse(readFileSync3(mPath, "utf8"));
3884
+ entries = JSON.parse(readFileSync4(mPath, "utf8"));
3729
3885
  } catch {
3730
3886
  return [];
3731
3887
  }
@@ -3739,8 +3895,8 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3739
3895
  return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
3740
3896
  }
3741
3897
  function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
3742
- const mPath = path10.join(cacheDir, "manifest.json");
3743
- 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")) : [];
3744
3900
  const wanted = new Set(families.map((f) => f.toLowerCase()));
3745
3901
  return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
3746
3902
  family: f.family,
@@ -3753,11 +3909,11 @@ function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
3753
3909
  }));
3754
3910
  }
3755
3911
  function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
3756
- const mPath = path10.join(cacheDir, "manifest.json");
3757
- if (!existsSync6(mPath)) return [];
3912
+ const mPath = path11.join(cacheDir, "manifest.json");
3913
+ if (!existsSync7(mPath)) return [];
3758
3914
  let entries;
3759
3915
  try {
3760
- entries = JSON.parse(readFileSync3(mPath, "utf8"));
3916
+ entries = JSON.parse(readFileSync4(mPath, "utf8"));
3761
3917
  } catch {
3762
3918
  return [];
3763
3919
  }
@@ -3766,31 +3922,83 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
3766
3922
  ).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
3767
3923
  }
3768
3924
  function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3769
- const mPath = path10.join(cacheDir, "manifest.json");
3770
- if (!existsSync6(mPath)) return [];
3925
+ const mPath = path11.join(cacheDir, "manifest.json");
3926
+ if (!existsSync7(mPath)) return [];
3771
3927
  let entries;
3772
3928
  try {
3773
- entries = JSON.parse(readFileSync3(mPath, "utf8"));
3929
+ entries = JSON.parse(readFileSync4(mPath, "utf8"));
3774
3930
  } catch {
3775
3931
  return [];
3776
3932
  }
3777
3933
  const byFamily = /* @__PURE__ */ new Map();
3778
3934
  for (const e of entries) {
3779
3935
  if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
3780
- const file = path10.isAbsolute(e.file) && existsSync6(e.file) ? e.file : path10.resolve(cacheDir, path10.basename(e.file));
3781
- if (!existsSync6(file)) continue;
3782
- 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;
3783
3939
  const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
3784
3940
  set.add(e.weight);
3785
3941
  byFamily.set(e.family, set);
3786
3942
  }
3787
3943
  return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
3788
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
+ }
3789
3996
  var DEFAULT_FONT_CACHE, UA, FONT_LICENSES, GOOGLE_LICENSE_IDS, GOOGLE_FONT_FILE_PREFIX;
3790
3997
  var init_font_resolve = __esm({
3791
3998
  "packages/verify/src/font-resolve.ts"() {
3792
3999
  "use strict";
3793
4000
  init_font_collection();
4001
+ init_font_discovery();
3794
4002
  DEFAULT_FONT_CACHE = fontCacheDir();
3795
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";
3796
4004
  FONT_LICENSES = ["OFL-1.1", "Apache-2.0", "UFL-1.0", "proprietary", "unknown"];
@@ -3806,17 +4014,17 @@ var init_font_resolve = __esm({
3806
4014
 
3807
4015
  // packages/verify/src/font-faces.ts
3808
4016
  import { createHash as createHash2 } from "node:crypto";
3809
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
3810
- import path11 from "node:path";
4017
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
4018
+ import path12 from "node:path";
3811
4019
  function injectedGroups(manifestPath2) {
3812
- if (!existsSync7(manifestPath2)) return { groups: [], shared: false };
3813
- const claimed = JSON.parse(readFileSync4(manifestPath2, "utf8"));
3814
- 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));
3815
4023
  const byFile = /* @__PURE__ */ new Map();
3816
4024
  for (const f of claimed) {
3817
4025
  const file = resolveFile(f.file);
3818
- if (!existsSync7(file)) continue;
3819
- 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;
3820
4028
  const k = `${f.family}:${f.file}`;
3821
4029
  const e = byFile.get(k) ?? { family: f.family, weights: [], file };
3822
4030
  e.weights.push(f.weight);
@@ -3825,14 +4033,14 @@ function injectedGroups(manifestPath2) {
3825
4033
  const groups = [...byFile.values()];
3826
4034
  return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
3827
4035
  }
3828
- function fontFaceCss(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
4036
+ function fontFaceCss(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
3829
4037
  const { groups, shared } = injectedGroups(manifestPath2);
3830
4038
  return groups.map((e) => {
3831
4039
  const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
3832
- 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'); }`;
3833
4041
  }).join("\n");
3834
4042
  }
3835
- function injectedFamilyWeights(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
4043
+ function injectedFamilyWeights(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
3836
4044
  const { groups, shared } = injectedGroups(manifestPath2);
3837
4045
  const out = /* @__PURE__ */ new Map();
3838
4046
  for (const g of groups) {
@@ -3857,17 +4065,17 @@ var init_font_faces = __esm({
3857
4065
  });
3858
4066
 
3859
4067
  // packages/verify/src/admission.ts
3860
- import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync8, writeFileSync as writeFileSync4 } from "node:fs";
3861
- 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";
3862
4070
  import { build as build2 } from "esbuild";
3863
4071
  import postcss from "postcss";
3864
4072
  import tailwindcss from "tailwindcss";
3865
4073
  import { chromium as chromium2 } from "playwright-core";
3866
4074
  function fontWeightsByFamily() {
3867
- const mPath = path12.join(fontCacheDir(), "manifest.json");
4075
+ const mPath = path13.join(fontCacheDir(), "manifest.json");
3868
4076
  const out = /* @__PURE__ */ new Map();
3869
- if (!existsSync8(mPath)) return out;
3870
- 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")))
3871
4079
  out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
3872
4080
  return out;
3873
4081
  }
@@ -3887,7 +4095,7 @@ var init_admission = __esm({
3887
4095
  });
3888
4096
 
3889
4097
  // packages/verify/src/tasks.ts
3890
- import path13 from "node:path";
4098
+ import path14 from "node:path";
3891
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;
3892
4100
  var init_tasks = __esm({
3893
4101
  "packages/verify/src/tasks.ts"() {
@@ -4047,7 +4255,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4047
4255
  ];
4048
4256
  TASKS = {
4049
4257
  calendar: {
4050
- set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
4258
+ set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
4051
4259
  entry: "Calendar.tsx",
4052
4260
  configs: CALENDAR_CONFIGS,
4053
4261
  systemApi: CALENDAR_API,
@@ -4055,7 +4263,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4055
4263
  prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
4056
4264
  },
4057
4265
  "shadcn-button": {
4058
- set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
4266
+ set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
4059
4267
  entry: "Button.tsx",
4060
4268
  configs: BUTTON_CONFIGS,
4061
4269
  systemApi: BUTTON_API,
@@ -4063,7 +4271,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4063
4271
  prelude: { controls: ["> *"], textInputs: [] }
4064
4272
  },
4065
4273
  combobox: {
4066
- set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
4274
+ set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
4067
4275
  entry: "ComboBox.tsx",
4068
4276
  configs: COMBO_CONFIGS,
4069
4277
  systemApi: COMBO_API,
@@ -4071,7 +4279,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
4071
4279
  prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
4072
4280
  },
4073
4281
  modal: {
4074
- set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
4282
+ set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
4075
4283
  entry: "Modal.tsx",
4076
4284
  configs: MODAL_CONFIGS,
4077
4285
  systemApi: MODAL_API,
@@ -4089,8 +4297,8 @@ __export(behavior_exports, {
4089
4297
  compileMount: () => compileMount,
4090
4298
  recordingIsDark: () => recordingIsDark
4091
4299
  });
4092
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
4093
- import path14 from "node:path";
4300
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
4301
+ import path15 from "node:path";
4094
4302
  import { build as build3 } from "esbuild";
4095
4303
  import { chromium as chromium3 } from "playwright-core";
4096
4304
  import { PNG as PNG2 } from "pngjs";
@@ -4099,12 +4307,12 @@ function getFontFaces() {
4099
4307
  return _fontFaces;
4100
4308
  }
4101
4309
  async function compileMount(task, bundleDir) {
4102
- const entryTsx = path14.join(bundleDir, task.entry);
4103
- 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` };
4104
4312
  const mountSrc = `
4105
4313
  import { createElement } from "react";
4106
4314
  import { createRoot } from "react-dom/client";
4107
- import * as B from ${JSON.stringify(path14.resolve(entryTsx))};
4315
+ import * as B from ${JSON.stringify(path15.resolve(entryTsx))};
4108
4316
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
4109
4317
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
4110
4318
  // Callbacks cannot ride the JSON config: specs NAME spy props and the
@@ -4314,6 +4522,19 @@ async function runSteps(page, spec, renderPose) {
4314
4522
  // a data-* or a name= is a place to park a string, which is
4315
4523
  // the dodge this check exists to catch.
4316
4524
  const PERCEIVED = ['placeholder', 'alt', 'aria-label', 'title', 'value'];
4525
+ // 'value' counts ONLY where the platform RENDERS it as the
4526
+ // control's text: text-entry inputs, textarea, and the
4527
+ // input-typed buttons whose value IS their label. Nearly
4528
+ // every element carries a value PROPERTY \u2014 a visible
4529
+ // checkbox or <button> with value={sentinel} would satisfy
4530
+ // an unrestricted check while no user ever perceives the
4531
+ // string, which reopens the exact dodge this check exists
4532
+ // to close (post-release review, 2026-08-14). password is
4533
+ // excluded because its value renders as dots, not as the
4534
+ // string. Fail-closed allowlist, not a blocklist.
4535
+ const VALUE_RENDERS = ['text', 'search', 'email', 'url', 'tel', 'number', 'submit', 'reset', 'button'];
4536
+ const valueRenders = (el) =>
4537
+ el instanceof HTMLTextAreaElement || (el instanceof HTMLInputElement && VALUE_RENDERS.includes(el.type));
4317
4538
  const visible = (el) => {
4318
4539
  const r = el.getBoundingClientRect();
4319
4540
  const cs = getComputedStyle(el);
@@ -4326,7 +4547,8 @@ async function runSteps(page, spec, renderPose) {
4326
4547
  PERCEIVED.some((a) => {
4327
4548
  // The LIVE value for form controls: React's defaultValue
4328
4549
  // sets the property, and the attribute may never appear.
4329
- const v = a === 'value' && 'value' in el ? el.value : el.getAttribute(a);
4550
+ if (a === 'value' && !valueRenders(el)) return false;
4551
+ const v = a === 'value' ? el.value : el.getAttribute(a);
4330
4552
  return typeof v === 'string' && v.includes(needle);
4331
4553
  }),
4332
4554
  );
@@ -4371,10 +4593,10 @@ async function runSteps(page, spec, renderPose) {
4371
4593
  function recordingIsDark(task) {
4372
4594
  const rep = task.configs[0]?.rep;
4373
4595
  if (rep === void 0) return false;
4374
- const f = path14.join(task.set, rep, "get_screenshot.json");
4375
- if (!existsSync9(f)) return false;
4596
+ const f = path15.join(task.set, rep, "get_screenshot.json");
4597
+ if (!existsSync10(f)) return false;
4376
4598
  try {
4377
- 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");
4378
4600
  if (env?.data === void 0) return false;
4379
4601
  const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
4380
4602
  let sum = 0;
@@ -4446,7 +4668,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
4446
4668
  const deadlineMs = timeoutMs + 1e4;
4447
4669
  const js = await compileMount(task, bundleDir);
4448
4670
  if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
4449
- 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");
4450
4672
  const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
4451
4673
  const browser = await chromium3.connect(server.wsEndpoint());
4452
4674
  const results = [];
@@ -4589,8 +4811,8 @@ var init_behavior = __esm({
4589
4811
  });
4590
4812
 
4591
4813
  // packages/verify/src/bundle-quality.ts
4592
- import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "node:fs";
4593
- 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";
4594
4816
  function definedVars(tokensCss) {
4595
4817
  if (tokensCss === void 0) return void 0;
4596
4818
  const names = /* @__PURE__ */ new Set();
@@ -4599,19 +4821,19 @@ function definedVars(tokensCss) {
4599
4821
  }
4600
4822
  function recordedTokenMapState(setDir, reps) {
4601
4823
  const readMap = (file) => {
4602
- if (!existsSync10(file)) return void 0;
4824
+ if (!existsSync11(file)) return void 0;
4603
4825
  try {
4604
- const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync7(file, "utf8"))) || "{}");
4826
+ const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync8(file, "utf8"))) || "{}");
4605
4827
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4606
4828
  } catch {
4607
4829
  return {};
4608
4830
  }
4609
4831
  };
4610
- const setLevel = readMap(path15.join(setDir, "get_variable_defs.json"));
4832
+ const setLevel = readMap(path16.join(setDir, "get_variable_defs.json"));
4611
4833
  if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
4612
4834
  let recorded = false;
4613
4835
  for (const rep of reps) {
4614
- const m = readMap(path15.join(setDir, rep, "get_variable_defs.json"));
4836
+ const m = readMap(path16.join(setDir, rep, "get_variable_defs.json"));
4615
4837
  if (m === void 0) continue;
4616
4838
  recorded = true;
4617
4839
  if (Object.keys(m).length > 0) return "populated";
@@ -4676,11 +4898,11 @@ function fontStackFindings(sheets, coverage) {
4676
4898
  }
4677
4899
  async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
4678
4900
  const findings = [];
4679
- const entryPath = path15.join(bundleDir, entry);
4680
- const cssPath = path15.join(bundleDir, "styles.css");
4681
- const tokensPath = path15.join(bundleDir, "tokens.css");
4682
- const css = existsSync10(cssPath) ? readFileSync7(cssPath, "utf8") : "";
4683
- 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;
4684
4906
  findings.push(
4685
4907
  ...fontStackFindings(
4686
4908
  [
@@ -4690,11 +4912,11 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
4690
4912
  injectedFamilyWeights(fontManifest)
4691
4913
  )
4692
4914
  );
4693
- if (existsSync10(entryPath)) {
4915
+ if (existsSync11(entryPath)) {
4694
4916
  const workDir = newScratchDir("quality");
4695
4917
  try {
4696
- const tsxPath = path15.join(workDir, entry);
4697
- writeFileSync5(tsxPath, readFileSync7(entryPath, "utf8"));
4918
+ const tsxPath = path16.join(workDir, entry);
4919
+ writeFileSync5(tsxPath, readFileSync8(entryPath, "utf8"));
4698
4920
  for (const d of runTscStrict([tsxPath]).diagnostics) {
4699
4921
  findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
4700
4922
  }
@@ -4769,8 +4991,8 @@ var init_effect_geometry = __esm({
4769
4991
  });
4770
4992
 
4771
4993
  // packages/verify/src/bundle-score.ts
4772
- import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
4773
- 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";
4774
4996
  import { build as build4 } from "esbuild";
4775
4997
  import { chromium as chromium4 } from "playwright-core";
4776
4998
  function getFontFaces2() {
@@ -4824,7 +5046,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
4824
5046
  }
4825
5047
  function metadataRoot(set, rep) {
4826
5048
  try {
4827
- 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");
4828
5050
  return parseMetadataStructure(text);
4829
5051
  } catch {
4830
5052
  return void 0;
@@ -4879,19 +5101,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
4879
5101
  });
4880
5102
  }
4881
5103
  function repMeta(set, rep) {
4882
- 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");
4883
5105
  const root = parseMetadataStructure(text);
4884
5106
  return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
4885
5107
  }
4886
5108
  function repRef(set, rep) {
4887
- 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");
4888
5110
  return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
4889
5111
  }
4890
5112
  function repEffectExtents(set, rep) {
4891
- const file = path16.join(set, rep, "get_design_context.json");
4892
- if (!existsSync11(file)) return void 0;
5113
+ const file = path17.join(set, rep, "get_design_context.json");
5114
+ if (!existsSync12(file)) return void 0;
4893
5115
  try {
4894
- 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");
4895
5117
  const extents = shadowExtents(text);
4896
5118
  return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
4897
5119
  } catch {
@@ -4902,13 +5124,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
4902
5124
  if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
4903
5125
  const CONFIGS2 = task.configs;
4904
5126
  const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
4905
- const entryTsx = path16.join(bundleDir, task.entry);
4906
- 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` }));
4907
- 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");
4908
5130
  const mountSrc = `
4909
5131
  import { createElement } from "react";
4910
5132
  import { createRoot } from "react-dom/client";
4911
- import * as B from ${JSON.stringify(path16.resolve(entryTsx))};
5133
+ import * as B from ${JSON.stringify(path17.resolve(entryTsx))};
4912
5134
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
4913
5135
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
4914
5136
  const root = document.getElementById("root");
@@ -5003,12 +5225,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
5003
5225
  return name === void 0 ? c : { ...c, name };
5004
5226
  });
5005
5227
  if (opts.evidenceDir !== void 0) {
5006
- writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
5007
- writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
5008
- 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));
5009
5231
  for (const [i, c] of absent.slice(0, 3).entries()) {
5010
- writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
5011
- 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));
5012
5234
  }
5013
5235
  }
5014
5236
  return {
@@ -5116,15 +5338,15 @@ var init_prelude = __esm({
5116
5338
  });
5117
5339
 
5118
5340
  // packages/verify/src/parity.ts
5119
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
5120
- import path17 from "node:path";
5341
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
5342
+ import path18 from "node:path";
5121
5343
  import { chromium as chromium6 } from "playwright-core";
5122
5344
  function getFontFaces3() {
5123
5345
  _fontFaces3 ??= fontFaceCss();
5124
5346
  return _fontFaces3;
5125
5347
  }
5126
- function hoverForcedConfigs(task) {
5127
- return task.configs.filter((c) => {
5348
+ function hoverForcedConfigs(authority) {
5349
+ return authority.filter((c) => {
5128
5350
  const forced = c.props["data-tendril-state"];
5129
5351
  return typeof forced === "string" && forced.split(/\s+/).includes("hover");
5130
5352
  });
@@ -5139,27 +5361,37 @@ function withoutHoverToken(props) {
5139
5361
  }
5140
5362
  return rest;
5141
5363
  }
5142
- async function checkHoverParity(task, bundleDir, opts = {}) {
5143
- const configs = hoverForcedConfigs(task);
5364
+ async function checkHoverParity(task, bundleDir, authority, opts = {}) {
5365
+ const configs = hoverForcedConfigs(authority);
5144
5366
  if (configs.length === 0) return [];
5145
5367
  const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
5146
5368
  const deadlineMs = timeoutMs + 1e4;
5147
5369
  const js = await compileMount(task, bundleDir);
5148
5370
  if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
5149
- 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");
5150
5372
  const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
5151
5373
  const browser = await chromium6.connect(server.wsEndpoint());
5152
5374
  const results = [];
5153
5375
  try {
5154
5376
  for (const cfg of configs) {
5155
- 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) => {
5156
5388
  const html = `<!doctype html><html><head><meta charset="utf-8"><style>
5157
5389
  ${getFontFaces3()}
5158
5390
  ${css}
5159
5391
  body{margin:0;padding:20px}
5160
- #root{position:static;display:inline-block}
5161
- </style></head><body><div id="root"></div><script>window.__cfg=${JSON.stringify({ component: cfg.component, props })}</script><script>${js}</script></body></html>`;
5162
- 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 });
5163
5395
  try {
5164
5396
  page.setDefaultTimeout(timeoutMs);
5165
5397
  await page.route("**/*", (route) => route.request().url().startsWith("data:") ? route.continue() : route.abort());
@@ -5173,15 +5405,30 @@ body{margin:0;padding:20px}
5173
5405
  } else {
5174
5406
  await page.waitForTimeout(400);
5175
5407
  }
5176
- 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 } });
5177
5409
  } finally {
5178
5410
  await page.close();
5179
5411
  }
5180
5412
  };
5181
5413
  const work = (async () => {
5182
- 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);
5183
5419
  if (!Buffer.isBuffer(forcedShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `forced mount: ${forcedShot.error}` };
5184
- 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);
5185
5432
  if (!Buffer.isBuffer(realShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `real-hover mount: ${realShot.error}` };
5186
5433
  if (Buffer.compare(forcedShot, realShot) !== 0) {
5187
5434
  return {
@@ -5221,13 +5468,14 @@ var init_parity = __esm({
5221
5468
  init_behavior();
5222
5469
  init_font_faces();
5223
5470
  init_mount_limits();
5471
+ init_bundle_score();
5224
5472
  }
5225
5473
  });
5226
5474
 
5227
5475
  // packages/verify/src/composition.ts
5228
5476
  import { createRequire as createRequire2 } from "node:module";
5229
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
5230
- import path18 from "node:path";
5477
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
5478
+ import path19 from "node:path";
5231
5479
  import { build as build6 } from "esbuild";
5232
5480
  import { chromium as chromium7 } from "playwright-core";
5233
5481
  function getFontFaces4() {
@@ -5235,9 +5483,9 @@ function getFontFaces4() {
5235
5483
  return _fontFaces4;
5236
5484
  }
5237
5485
  async function compileInstrumentedMount(task, bundleDir) {
5238
- const entryTsx = path18.join(bundleDir, task.entry);
5239
- if (!existsSync13(entryTsx)) return { error: `${task.entry} missing` };
5240
- 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"));
5241
5489
  let realJsxPath;
5242
5490
  try {
5243
5491
  realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
@@ -5248,7 +5496,7 @@ async function compileInstrumentedMount(task, bundleDir) {
5248
5496
  import { createElement } from "react";
5249
5497
  import { createRoot } from "react-dom/client";
5250
5498
  import { __registerParts } from "react/jsx-runtime";
5251
- import * as B from ${JSON.stringify(path18.resolve(entryTsx))};
5499
+ import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
5252
5500
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
5253
5501
  const pairs: Array<[unknown, string]> = [];
5254
5502
  for (const name of cfg.partComponents) {
@@ -5303,7 +5551,7 @@ function expectedParts(task, roles, mainSlug) {
5303
5551
  }
5304
5552
  function interiorRegions(setDir, roles) {
5305
5553
  const mains = roles.main;
5306
- 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")));
5307
5555
  if (withInterior.length === 0) {
5308
5556
  return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
5309
5557
  }
@@ -5320,7 +5568,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
5320
5568
  if (typeof js !== "string") {
5321
5569
  return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
5322
5570
  }
5323
- 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");
5324
5572
  const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
5325
5573
  const browser = await chromium7.connect(server.wsEndpoint());
5326
5574
  const PAD = 4;
@@ -5425,7 +5673,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
5425
5673
  const deadlineMs = timeoutMs + 1e4;
5426
5674
  const js = await compileInstrumentedMount(task, bundleDir);
5427
5675
  if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
5428
- 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");
5429
5677
  const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
5430
5678
  const browser = await chromium7.connect(server.wsEndpoint());
5431
5679
  try {
@@ -5515,17 +5763,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
5515
5763
  });
5516
5764
 
5517
5765
  // packages/verify/src/occlusion.ts
5518
- import { existsSync as existsSync14 } from "node:fs";
5519
- import path19 from "node:path";
5766
+ import { existsSync as existsSync15 } from "node:fs";
5767
+ import path20 from "node:path";
5520
5768
  import { build as build7 } from "esbuild";
5521
5769
  import { chromium as chromium8 } from "playwright-core";
5522
5770
  async function compileTwoUp(task, bundleDir) {
5523
- const entryTsx = path19.join(bundleDir, task.entry);
5524
- 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` };
5525
5773
  const src = `
5526
5774
  import { createElement } from "react";
5527
5775
  import { createRoot } from "react-dom/client";
5528
- import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
5776
+ import * as B from ${JSON.stringify(path20.resolve(entryTsx))};
5529
5777
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
5530
5778
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
5531
5779
  for (const id of ["first", "second"]) {
@@ -5694,6 +5942,7 @@ var init_src4 = __esm({
5694
5942
  init_prelude();
5695
5943
  init_mount_limits();
5696
5944
  init_font_collection();
5945
+ init_font_discovery();
5697
5946
  init_font_faces();
5698
5947
  init_font_resolve();
5699
5948
  init_paths();
@@ -5704,23 +5953,23 @@ var init_src4 = __esm({
5704
5953
  });
5705
5954
 
5706
5955
  // packages/cli/src/environment.ts
5707
- import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
5708
- import path20 from "node:path";
5956
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
5957
+ import path21 from "node:path";
5709
5958
  import { createHash as createHash3 } from "node:crypto";
5710
5959
  import { fileURLToPath as fileURLToPath5 } from "node:url";
5711
5960
  function cliVersion() {
5712
5961
  try {
5713
- 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";
5714
5963
  } catch {
5715
5964
  return "dev";
5716
5965
  }
5717
5966
  }
5718
5967
  function environmentStamp(taskFamilies) {
5719
- const manifestPath2 = path20.join(fontCacheDir(), "manifest.json");
5968
+ const manifestPath2 = path21.join(fontCacheDir(), "manifest.json");
5720
5969
  let fontsHash = null;
5721
- if (existsSync15(manifestPath2)) {
5970
+ if (existsSync16(manifestPath2)) {
5722
5971
  try {
5723
- const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
5972
+ const entries = JSON.parse(readFileSync12(manifestPath2, "utf8"));
5724
5973
  const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
5725
5974
  const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
5726
5975
  fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
@@ -5764,8 +6013,8 @@ var init_describe = __esm({
5764
6013
  });
5765
6014
 
5766
6015
  // packages/cli/src/env.ts
5767
- import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
5768
- import path21 from "node:path";
6016
+ import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
6017
+ import path22 from "node:path";
5769
6018
  function parseEnv(content) {
5770
6019
  const entries = /* @__PURE__ */ new Map();
5771
6020
  for (const line of content.split("\n")) {
@@ -5777,9 +6026,9 @@ function parseEnv(content) {
5777
6026
  function resolveCredential(name) {
5778
6027
  const fromProcess = process.env[name];
5779
6028
  if (fromProcess) return fromProcess;
5780
- const envPath = path21.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
5781
- if (!existsSync16(envPath)) return void 0;
5782
- 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);
5783
6032
  }
5784
6033
  var init_env = __esm({
5785
6034
  "packages/cli/src/env.ts"() {
@@ -5839,17 +6088,17 @@ var init_output = __esm({
5839
6088
  });
5840
6089
 
5841
6090
  // packages/cli/src/entitlement.ts
5842
- 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";
5843
6092
  import crypto from "node:crypto";
5844
- import os3 from "node:os";
5845
- import path22 from "node:path";
6093
+ import os4 from "node:os";
6094
+ import path23 from "node:path";
5846
6095
  function entitlementPath() {
5847
- 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");
5848
6097
  }
5849
6098
  function readStoredEntitlement(file = entitlementPath()) {
5850
- if (!existsSync17(file)) return void 0;
6099
+ if (!existsSync18(file)) return void 0;
5851
6100
  try {
5852
- const parsed = JSON.parse(readFileSync13(file, "utf8"));
6101
+ const parsed = JSON.parse(readFileSync14(file, "utf8"));
5853
6102
  if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
5854
6103
  return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
5855
6104
  } catch {
@@ -5857,7 +6106,7 @@ function readStoredEntitlement(file = entitlementPath()) {
5857
6106
  }
5858
6107
  }
5859
6108
  function writeStoredEntitlement(stored, file = entitlementPath()) {
5860
- mkdirSync4(path22.dirname(file), { recursive: true });
6109
+ mkdirSync4(path23.dirname(file), { recursive: true });
5861
6110
  writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
5862
6111
  `);
5863
6112
  chmodSync(file, 384);
@@ -5942,9 +6191,9 @@ var init_entitlement = __esm({
5942
6191
 
5943
6192
  // packages/cli/src/commands/doctor.ts
5944
6193
  import { spawnSync } from "node:child_process";
5945
- import { existsSync as existsSync18, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
5946
- import os4 from "node:os";
5947
- 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";
5948
6197
  function withDeadline(work, ms) {
5949
6198
  return Promise.race([
5950
6199
  work,
@@ -6004,19 +6253,19 @@ async function runDoctorChecks(options) {
6004
6253
  remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
6005
6254
  });
6006
6255
  }
6007
- const fontManifest = path23.join(fontCacheDir(), "manifest.json");
6256
+ const fontManifest = path24.join(fontCacheDir(), "manifest.json");
6008
6257
  checks.push(
6009
- 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)` } : {
6010
6259
  name: "font-cache",
6011
6260
  ok: true,
6012
6261
  detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
6013
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.`
6014
6263
  }
6015
6264
  );
6016
- const pluginRoot = path23.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
6017
- if (existsSync18(pluginRoot)) {
6265
+ const pluginRoot = path24.join(os5.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
6266
+ if (existsSync19(pluginRoot)) {
6018
6267
  try {
6019
- const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
6268
+ const versions = readdirSync4(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
6020
6269
  const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
6021
6270
  if (newest !== void 0) {
6022
6271
  const skewed = versionIsNewer(newest, cliVersion());
@@ -7540,9 +7789,9 @@ __export(record_exports, {
7540
7789
  runRecordPlan: () => runRecordPlan,
7541
7790
  runRecordStatus: () => runRecordStatus
7542
7791
  });
7543
- import { existsSync as existsSync20, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
7544
- import os5 from "node:os";
7545
- 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";
7546
7795
  import { writeFileSync as writeFileSync9 } from "node:fs";
7547
7796
  function recordsInteractionState(reports) {
7548
7797
  const evident = (s) => stateTokens(s).some((t) => INTERACTION_EVIDENCE_VALUES.has(t));
@@ -7565,7 +7814,7 @@ function interactionDisclosure(component, reports) {
7565
7814
  };
7566
7815
  }
7567
7816
  function symbolsFromMetadataEnvelope(file, sourceFrame) {
7568
- const env = JSON.parse(readFileSync16(file, "utf8"));
7817
+ const env = JSON.parse(readFileSync17(file, "utf8"));
7569
7818
  const text = env.content.map((c) => c.text ?? "").join("\n");
7570
7819
  const symbols = [];
7571
7820
  const walk2 = (node, ancestor) => {
@@ -7623,7 +7872,7 @@ function runRecordPlan(opts) {
7623
7872
  if (rawFile !== void 0) {
7624
7873
  try {
7625
7874
  const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
7626
- 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");
7627
7876
  writeFileSync9(tmp, JSON.stringify(envelope));
7628
7877
  metadataEntries.push({ file: tmp });
7629
7878
  } catch (err) {
@@ -7645,7 +7894,7 @@ function runRecordPlan(opts) {
7645
7894
  let metadataTruncated = false;
7646
7895
  for (const { file, frame } of metadataEntries) {
7647
7896
  try {
7648
- const parsed = symbolsFromMetadataEnvelope(path26.resolve(file), frame);
7897
+ const parsed = symbolsFromMetadataEnvelope(path27.resolve(file), frame);
7649
7898
  symbols.push(...parsed.symbols);
7650
7899
  if (parsed.truncated) metadataTruncated = true;
7651
7900
  } catch (err) {
@@ -7679,7 +7928,7 @@ function runRecordPlan(opts) {
7679
7928
  if (symbols.length === 0) {
7680
7929
  const leads = metadataEntries.flatMap(({ file }) => {
7681
7930
  try {
7682
- const env = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
7931
+ const env = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
7683
7932
  return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
7684
7933
  } catch {
7685
7934
  return [];
@@ -7753,7 +8002,7 @@ function runRecordPlan(opts) {
7753
8002
  // a CLI flag and the MCP surface has no parameter for it.
7754
8003
  decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
7755
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.",
7756
- 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`]
7757
8006
  },
7758
8007
  {
7759
8008
  id: "larger-allowance",
@@ -7880,7 +8129,7 @@ function nextPayload(setDir) {
7880
8129
  const instruction = nextInstruction(setDir);
7881
8130
  const status = sessionStatus(setDir);
7882
8131
  const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
7883
- if (instruction === null && !existsSync20(path26.join(setDir, "get_variable_defs.json"))) {
8132
+ if (instruction === null && !existsSync21(path27.join(setDir, "get_variable_defs.json"))) {
7884
8133
  const manifest = loadManifest(setDir);
7885
8134
  const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
7886
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 };
@@ -7909,7 +8158,7 @@ function runRecordNext(opts) {
7909
8158
  const progress = payload["progress"];
7910
8159
  process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
7911
8160
  \u2192 ${payload["note"]}
7912
- \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>`)}
7913
8162
  `);
7914
8163
  });
7915
8164
  }
@@ -7983,7 +8232,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
7983
8232
  const skipped = [];
7984
8233
  const failed = [];
7985
8234
  for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
7986
- if (existsSync20(path26.join(setDir, rep, name))) {
8235
+ if (existsSync21(path27.join(setDir, rep, name))) {
7987
8236
  skipped.push(name);
7988
8237
  continue;
7989
8238
  }
@@ -8005,16 +8254,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
8005
8254
  }
8006
8255
  function rawEnvelopeFromFile(file, parts) {
8007
8256
  if (parts) {
8008
- const blocks = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
8257
+ const blocks = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
8009
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");
8010
8259
  return { content: blocks.map((text) => ({ type: "text", text })) };
8011
8260
  }
8012
- return { content: [{ type: "text", text: readFileSync16(path26.resolve(file), "utf8") }] };
8261
+ return { content: [{ type: "text", text: readFileSync17(path27.resolve(file), "utf8") }] };
8013
8262
  }
8014
8263
  async function runRecordIngest(opts) {
8015
8264
  let payload;
8016
8265
  try {
8017
- 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"));
8018
8267
  } catch (err) {
8019
8268
  fail(opts, ExitCode.InputValidation, {
8020
8269
  error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
@@ -8026,7 +8275,7 @@ async function runRecordIngest(opts) {
8026
8275
  fail(opts, ExitCode.InputValidation, {
8027
8276
  error: "--raw is for text tool responses; screenshots are binary",
8028
8277
  code: "envelope-invalid",
8029
- 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.`
8030
8279
  });
8031
8280
  }
8032
8281
  if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
@@ -8046,7 +8295,7 @@ async function runRecordIngest(opts) {
8046
8295
  remediation: REINGEST_GUIDANCE
8047
8296
  });
8048
8297
  }
8049
- 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)}
8050
8299
  `);
8051
8300
  emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
8052
8301
  process.stdout.write("set-level get_variable_defs ingested\n");
@@ -8062,7 +8311,7 @@ async function runRecordIngest(opts) {
8062
8311
  if (assets !== void 0) {
8063
8312
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
8064
8313
  `);
8065
- 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>`)}
8066
8315
  `);
8067
8316
  }
8068
8317
  });
@@ -8135,15 +8384,15 @@ async function runRecordIngestRep(opts) {
8135
8384
  if (assets !== void 0) {
8136
8385
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
8137
8386
  `);
8138
- 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>`)}
8139
8388
  `);
8140
8389
  }
8141
8390
  });
8142
8391
  }
8143
8392
  function runRecordAsset(opts) {
8144
8393
  if (opts.dir !== void 0) {
8145
- const dir = path26.resolve(opts.dir);
8146
- 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));
8147
8396
  if (names.length === 0) {
8148
8397
  fail(opts, ExitCode.InputValidation, {
8149
8398
  error: `no asset-*.<ext> files found in ${dir}`,
@@ -8154,7 +8403,7 @@ function runRecordAsset(opts) {
8154
8403
  const ingested = [];
8155
8404
  try {
8156
8405
  for (const name of names) {
8157
- ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path26.join(dir, name)));
8406
+ ingestAsset(opts.setDir, opts.rep, name, readFileSync17(path27.join(dir, name)));
8158
8407
  ingested.push(name);
8159
8408
  }
8160
8409
  } catch (err) {
@@ -8174,11 +8423,11 @@ function runRecordAsset(opts) {
8174
8423
  fail(opts, ExitCode.InputValidation, {
8175
8424
  error: "pass --name and --file for a single asset, or --dir for a batch",
8176
8425
  code: "asset-rejected",
8177
- 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>`)
8178
8427
  });
8179
8428
  }
8180
8429
  try {
8181
- 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)));
8182
8431
  emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
8183
8432
  process.stdout.write(`ingested ${opts.rep}/${opts.name}
8184
8433
  `);
@@ -8214,7 +8463,7 @@ function narrowedRoles(derived, override) {
8214
8463
  function rolesFromFile(opts, file, derived) {
8215
8464
  let json;
8216
8465
  try {
8217
- json = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
8466
+ json = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
8218
8467
  } catch (err) {
8219
8468
  fail(opts, ExitCode.InputValidation, {
8220
8469
  error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -8252,11 +8501,11 @@ function rolesFromFile(opts, file, derived) {
8252
8501
  };
8253
8502
  }
8254
8503
  function runRecordFinish(opts) {
8255
- if (!existsSync20(path26.join(opts.setDir, "recording-set.json"))) {
8504
+ if (!existsSync21(path27.join(opts.setDir, "recording-set.json"))) {
8256
8505
  fail(opts, ExitCode.InputValidation, {
8257
8506
  error: `no recording-set.json in ${opts.setDir}`,
8258
8507
  code: "no-recording-set",
8259
- 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.`
8260
8509
  });
8261
8510
  }
8262
8511
  const { manifest, raw } = readManifestFile(opts.setDir);
@@ -8284,17 +8533,17 @@ function runRecordFinish(opts) {
8284
8533
  fail(opts, ExitCode.ConfirmationRequired, {
8285
8534
  error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
8286
8535
  code: "roles-confirmation-not-interactive",
8287
- 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.`
8288
8537
  });
8289
8538
  }
8290
8539
  const merged = { ...raw, roles };
8291
- const { issues } = validateRecordingSet(merged, (rel) => existsSync20(path26.join(opts.setDir, rel)));
8540
+ const { issues } = validateRecordingSet(merged, (rel) => existsSync21(path27.join(opts.setDir, rel)));
8292
8541
  const errors = issues.filter((i) => i.severity === "error");
8293
8542
  if (errors.length > 0) {
8294
8543
  fail(opts, ExitCode.InputValidation, {
8295
8544
  error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
8296
8545
  code: "recording-set-invalid",
8297
- 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\`.`
8298
8547
  });
8299
8548
  }
8300
8549
  for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
@@ -8528,8 +8777,8 @@ var init_engine_curated = __esm({
8528
8777
  });
8529
8778
 
8530
8779
  // packages/generate/src/loop.ts
8531
- import { existsSync as existsSync21, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
8532
- 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";
8533
8782
  import { z as z11 } from "zod";
8534
8783
  function objective(scores, behaviors) {
8535
8784
  const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
@@ -8566,9 +8815,9 @@ ${absentLines.join("\n")}` : ""}
8566
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."}`;
8567
8816
  }
8568
8817
  function archivePriorRun(outDir) {
8569
- 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;
8570
8819
  let n = 1;
8571
- while (existsSync21(`${outDir}-prev-${n}`)) n += 1;
8820
+ while (existsSync22(`${outDir}-prev-${n}`)) n += 1;
8572
8821
  renameSync(outDir, `${outDir}-prev-${n}`);
8573
8822
  return `${outDir}-prev-${n}`;
8574
8823
  }
@@ -8577,14 +8826,14 @@ async function runEngineLoop(opts) {
8577
8826
  const plateau = opts.plateau ?? 2;
8578
8827
  const progress = opts.onProgress ?? (() => {
8579
8828
  });
8580
- const statePath = path27.join(opts.outDir, "loop-state.json");
8581
- 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);
8582
8831
  if (!resuming) {
8583
8832
  const archived = archivePriorRun(opts.outDir);
8584
8833
  if (archived !== void 0) progress(`previous run archived to ${archived}`);
8585
8834
  }
8586
8835
  mkdirSync6(opts.outDir, { recursive: true });
8587
- const scratch = path27.join(opts.outDir, ".candidate");
8836
+ const scratch = path28.join(opts.outDir, ".candidate");
8588
8837
  let attempts = [];
8589
8838
  let log = [];
8590
8839
  let best;
@@ -8592,7 +8841,7 @@ async function runEngineLoop(opts) {
8592
8841
  let nonAccepted = 0;
8593
8842
  let stopReason = "max-iterations";
8594
8843
  if (resuming) {
8595
- const restored = LoopStateSchema.parse(JSON.parse(readFileSync17(statePath, "utf8")));
8844
+ const restored = LoopStateSchema.parse(JSON.parse(readFileSync18(statePath, "utf8")));
8596
8845
  attempts = restored.attempts;
8597
8846
  log = restored.iterations;
8598
8847
  spentUsd = restored.spentUsd;
@@ -8612,7 +8861,7 @@ async function runEngineLoop(opts) {
8612
8861
  };
8613
8862
  const writeCandidate = (files) => {
8614
8863
  mkdirSync6(scratch, { recursive: true });
8615
- 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);
8616
8865
  };
8617
8866
  const scoreCandidate = async (candidate, iter, usd, modelMs) => {
8618
8867
  writeCandidate(candidate.files);
@@ -8670,8 +8919,8 @@ async function runEngineLoop(opts) {
8670
8919
  const usd = candidate.usage?.usd ?? 0;
8671
8920
  spentUsd += usd;
8672
8921
  if (candidate.raw !== void 0) {
8673
- mkdirSync6(path27.join(opts.outDir, "responses"), { recursive: true });
8674
- 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);
8675
8924
  }
8676
8925
  if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
8677
8926
  const finish = candidate.usage?.finishReason ?? "?";
@@ -8697,10 +8946,10 @@ async function runEngineLoop(opts) {
8697
8946
  }
8698
8947
  }
8699
8948
  }
8700
- 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);
8701
8950
  const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
8702
8951
  writeFileSync10(
8703
- path27.join(opts.outDir, "run-log.json"),
8952
+ path28.join(opts.outDir, "run-log.json"),
8704
8953
  `${JSON.stringify(
8705
8954
  {
8706
8955
  ...opts.meta,
@@ -8767,8 +9016,8 @@ var init_loop2 = __esm({
8767
9016
  });
8768
9017
 
8769
9018
  // packages/generate/src/brief.ts
8770
- import { existsSync as existsSync22, readFileSync as readFileSync18 } from "node:fs";
8771
- import path28 from "node:path";
9019
+ import { existsSync as existsSync23, readFileSync as readFileSync19 } from "node:fs";
9020
+ import path29 from "node:path";
8772
9021
  function singleAxes2(name) {
8773
9022
  const parsed = parseVariantAxes(name);
8774
9023
  if (parsed === void 0) return void 0;
@@ -9009,12 +9258,12 @@ function authorBehaviors(api, extras = {}) {
9009
9258
  return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
9010
9259
  }
9011
9260
  function envelopeText(file) {
9012
- return envelopeFirstTextPart(JSON.parse(readFileSync18(file, "utf8")));
9261
+ return envelopeFirstTextPart(JSON.parse(readFileSync19(file, "utf8")));
9013
9262
  }
9014
9263
  function dismissEvidence(setDir, repSlugs) {
9015
9264
  for (const slug of repSlugs) {
9016
- const f = path28.join(setDir, slug, "get_design_context.json");
9017
- if (!existsSync22(f)) continue;
9265
+ const f = path29.join(setDir, slug, "get_design_context.json");
9266
+ if (!existsSync23(f)) continue;
9018
9267
  const text = envelopeText(f);
9019
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);
9020
9269
  if (propHit !== null) return `emission prop "${propHit[1]}"`;
@@ -9058,13 +9307,13 @@ function recordedFontNeeds(setDir, opts = {}) {
9058
9307
  }
9059
9308
  };
9060
9309
  const manifest = loadManifest(setDir);
9061
- const setDefs = path28.join(setDir, "get_variable_defs.json");
9062
- if (existsSync22(setDefs)) fromDefs(envelopeText(setDefs));
9310
+ const setDefs = path29.join(setDir, "get_variable_defs.json");
9311
+ if (existsSync23(setDefs)) fromDefs(envelopeText(setDefs));
9063
9312
  for (const rep of manifest.reps) {
9064
- const ctx = path28.join(setDir, rep.slug, "get_design_context.json");
9065
- if (existsSync22(ctx)) fromEmission(envelopeText(ctx));
9066
- const defs = path28.join(setDir, rep.slug, "get_variable_defs.json");
9067
- 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));
9068
9317
  }
9069
9318
  return [...byFamily.entries()].map(([family, paired]) => {
9070
9319
  const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
@@ -9075,10 +9324,10 @@ function symbolFontGlyphCount(setDir, reps) {
9075
9324
  const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
9076
9325
  const glyphs = /* @__PURE__ */ new Set();
9077
9326
  for (const rep of reps) {
9078
- const file = path28.join(setDir, rep, "get_metadata.json");
9079
- if (!existsSync22(file)) continue;
9327
+ const file = path29.join(setDir, rep, "get_metadata.json");
9328
+ if (!existsSync23(file)) continue;
9080
9329
  try {
9081
- 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");
9082
9331
  for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
9083
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)));
9084
9333
  if (PUA.test(name)) glyphs.add(name);
@@ -9104,8 +9353,8 @@ function recordedTextSlots(setDir, repSlugs) {
9104
9353
  const propRep = [];
9105
9354
  const perRep = [];
9106
9355
  for (const slug of repSlugs) {
9107
- const f = path28.join(setDir, slug, "get_design_context.json");
9108
- if (!existsSync22(f)) continue;
9356
+ const f = path29.join(setDir, slug, "get_design_context.json");
9357
+ if (!existsSync23(f)) continue;
9109
9358
  const code = envelopeText(f);
9110
9359
  const props = /* @__PURE__ */ new Map();
9111
9360
  for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
@@ -9130,8 +9379,8 @@ function recordedTextSlots(setDir, repSlugs) {
9130
9379
  }
9131
9380
  const axisValuesBySlug = /* @__PURE__ */ new Map();
9132
9381
  for (const slug of repSlugs) {
9133
- const metaFile = path28.join(setDir, slug, "get_metadata.json");
9134
- if (!existsSync22(metaFile)) continue;
9382
+ const metaFile = path29.join(setDir, slug, "get_metadata.json");
9383
+ if (!existsSync23(metaFile)) continue;
9135
9384
  const name = symbolName(envelopeText(metaFile));
9136
9385
  if (name === void 0) continue;
9137
9386
  const values = /* @__PURE__ */ new Set();
@@ -9230,8 +9479,8 @@ function authorTaskFromSet(setDir, opts = {}) {
9230
9479
  const poses = [];
9231
9480
  const missing = [];
9232
9481
  for (const rep of manifest.reps) {
9233
- const metaFile = path28.join(setDir, rep.slug, "get_metadata.json");
9234
- if (!existsSync22(metaFile)) {
9482
+ const metaFile = path29.join(setDir, rep.slug, "get_metadata.json");
9483
+ if (!existsSync23(metaFile)) {
9235
9484
  missing.push(rep.slug);
9236
9485
  continue;
9237
9486
  }
@@ -9245,8 +9494,8 @@ function authorTaskFromSet(setDir, opts = {}) {
9245
9494
  if (missing.length > 0) {
9246
9495
  throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
9247
9496
  }
9248
- const setMeta = path28.join(setDir, "get_metadata.json");
9249
- 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);
9250
9499
  const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
9251
9500
  const recordedFonts = recordedFontFamilies(setDir);
9252
9501
  const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
@@ -9429,10 +9678,10 @@ var init_brief = __esm({
9429
9678
  });
9430
9679
 
9431
9680
  // packages/generate/src/segments.ts
9432
- import { existsSync as existsSync23, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
9433
- 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";
9434
9683
  function repText(set, rep, tool) {
9435
- 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"));
9436
9685
  return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
9437
9686
  }
9438
9687
  function stripFigmaInstructions(emission) {
@@ -9492,18 +9741,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
9492
9741
  }
9493
9742
  function buildSegments(task, mode = "fenced") {
9494
9743
  const SET = task.set;
9744
+ let defsRecorded = existsSync24(path30.join(SET, "get_variable_defs.json"));
9495
9745
  let rawDefs = {};
9496
- if (existsSync23(path29.join(SET, "get_variable_defs.json"))) {
9497
- 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"))) || "{}";
9498
9748
  try {
9499
9749
  rawDefs = JSON.parse(text);
9500
9750
  } catch {
9501
9751
  }
9502
9752
  } else {
9503
9753
  for (const cfg of task.configs) {
9504
- const f = path29.join(SET, cfg.rep, "get_variable_defs.json");
9505
- if (!existsSync23(f)) continue;
9506
- 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"))) || "{}";
9507
9758
  try {
9508
9759
  for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
9509
9760
  } catch {
@@ -9511,25 +9762,27 @@ function buildSegments(task, mode = "fenced") {
9511
9762
  }
9512
9763
  }
9513
9764
  const emissionTexts = task.configs.map((cfg) => {
9514
- const f = path29.join(SET, cfg.rep, "get_design_context.json");
9515
- 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"))) : "";
9516
9767
  });
9517
9768
  const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
9518
9769
  const defs = JSON.stringify(map, null, 1);
9519
9770
  const parts = [
9520
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).",
9521
- `
9772
+ defsRecorded ? `
9522
9773
  ## Design tokens (use these CSS custom property names)
9523
9774
  \`\`\`json
9524
9775
  ${defs}
9525
- \`\`\`${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}`
9526
9779
  ];
9527
9780
  for (const cfg of task.configs) {
9528
9781
  const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
9529
9782
  const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
9530
- 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}:
9531
9784
  \`\`\`svg
9532
- ${readFileSync19(path29.join(SET, cfg.rep, f), "utf8")}
9785
+ ${readFileSync20(path30.join(SET, cfg.rep, f), "utf8")}
9533
9786
  \`\`\``).join("\n");
9534
9787
  parts.push(`
9535
9788
  ## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
@@ -9554,7 +9807,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
9554
9807
  } else {
9555
9808
  parts.push(`
9556
9809
  ## Output format
9557
- 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.`);
9558
9811
  }
9559
9812
  return parts.join("\n");
9560
9813
  }
@@ -9622,8 +9875,8 @@ var init_adapter = __esm({
9622
9875
 
9623
9876
  // packages/generate/src/bundle-emit.ts
9624
9877
  import { createHash as createHash4 } from "node:crypto";
9625
- import { copyFileSync, existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync20, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
9626
- 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";
9627
9880
  function pinFromConfigs(configs) {
9628
9881
  const domains = /* @__PURE__ */ new Map();
9629
9882
  const kinds = /* @__PURE__ */ new Map();
@@ -9692,15 +9945,23 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9692
9945
  const notices = [];
9693
9946
  const licenseTexts = /* @__PURE__ */ new Map();
9694
9947
  for (const face of faces) {
9695
- const src = path30.join(cacheDir, path30.basename(face.file));
9696
- const target = `./fonts/${path30.basename(face.file)}`;
9697
- 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";
9698
9951
  const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
9699
9952
  const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
9700
9953
  const license = normalizeFontLicense(face.license);
9701
- const terms = REDISTRIBUTABLE.get(license);
9954
+ const terms = face.source.startsWith("system:") ? void 0 : REDISTRIBUTABLE.get(license);
9702
9955
  if (terms === void 0) {
9703
- 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:")) {
9704
9965
  lines.push(
9705
9966
  `/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
9706
9967
  ` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
@@ -9724,14 +9985,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9724
9985
  `/* ${decl} */`
9725
9986
  );
9726
9987
  }
9727
- } else if (existsSync24(src) && createHash4("sha256").update(readFileSync20(src)).digest("hex") === face.sha256) {
9728
- mkdirSync7(path30.join(bundleDir, "fonts"), { recursive: true });
9729
- 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)));
9730
9991
  licenseTexts.set(terms.file, terms.text);
9731
9992
  const upstream = upstreamAttribution(face);
9732
9993
  notices.push(
9733
9994
  "",
9734
- `${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)}`,
9735
9996
  ` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
9736
9997
  ` source: ${face.source}`,
9737
9998
  ` sha256: ${face.sha256}`,
@@ -9745,9 +10006,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9745
10006
  }
9746
10007
  if (lines.length === 0) return null;
9747
10008
  if (notices.length > 0) {
9748
- const fontsDir = path30.join(bundleDir, "fonts");
9749
- for (const [file, text] of licenseTexts) writeFileSync11(path30.join(fontsDir, file), text);
9750
- 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")}
9751
10012
  `);
9752
10013
  header.push(
9753
10014
  "/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
@@ -9759,10 +10020,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
9759
10020
  `;
9760
10021
  }
9761
10022
  function countLatticeSymbols(setDir) {
9762
- const manifestFile = path30.join(setDir, "recording-set.json");
9763
- if (existsSync24(manifestFile)) {
10023
+ const manifestFile = path31.join(setDir, "recording-set.json");
10024
+ if (existsSync25(manifestFile)) {
9764
10025
  try {
9765
- const stored = JSON.parse(readFileSync20(manifestFile, "utf8"));
10026
+ const stored = JSON.parse(readFileSync21(manifestFile, "utf8"));
9766
10027
  if (stored.variantScope !== "component-set") return null;
9767
10028
  const lattice = stored.latticeNames;
9768
10029
  if (lattice !== void 0 && lattice.length > 0) return lattice.length;
@@ -9770,13 +10031,13 @@ function countLatticeSymbols(setDir) {
9770
10031
  }
9771
10032
  }
9772
10033
  const files = [
9773
- path30.join(setDir, "get_metadata.json"),
9774
- ...existsSync24(setDir) ? readdirSync7(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path30.join(setDir, f)) : []
9775
- ].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));
9776
10037
  if (files.length === 0) return null;
9777
10038
  let count = 0;
9778
10039
  for (const f of files) {
9779
- const text = envelopeTextContent(JSON.parse(readFileSync20(f, "utf8")));
10040
+ const text = envelopeTextContent(JSON.parse(readFileSync21(f, "utf8")));
9780
10041
  count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
9781
10042
  }
9782
10043
  return count > 0 ? count : null;
@@ -9784,21 +10045,21 @@ function countLatticeSymbols(setDir) {
9784
10045
  function recordingSetHash(setDir, configs) {
9785
10046
  const relPaths = [];
9786
10047
  for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
9787
- if (existsSync24(path30.join(setDir, name))) relPaths.push(name);
10048
+ if (existsSync25(path31.join(setDir, name))) relPaths.push(name);
9788
10049
  }
9789
10050
  for (const cfg of configs) {
9790
10051
  for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
9791
- 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}`);
9792
10053
  }
9793
- if (existsSync24(path30.join(setDir, cfg.rep))) {
9794
- 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-"))) {
9795
10056
  relPaths.push(`${cfg.rep}/${asset}`);
9796
10057
  }
9797
10058
  }
9798
10059
  }
9799
10060
  return hashRecordingSet(
9800
10061
  relPaths,
9801
- (p) => new Uint8Array(readFileSync20(path30.join(setDir, p))),
10062
+ (p) => new Uint8Array(readFileSync21(path31.join(setDir, p))),
9802
10063
  (chunks) => {
9803
10064
  const h = createHash4("sha256");
9804
10065
  for (const c of chunks) h.update(c);
@@ -9819,10 +10080,11 @@ function emitBundleV1(opts) {
9819
10080
  const certified = statuses.filter((s) => s.status === "certified").length;
9820
10081
  const pass = statuses.filter((s) => s.status !== "fail").length;
9821
10082
  const lattice = countLatticeSymbols(opts.task.set);
9822
- 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:"));
9823
10084
  const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
9824
- const cssFiles = ["styles.css", "tokens.css"].map((f) => path30.join(opts.bundleDir, f)).filter((f) => existsSync24(f));
9825
- 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"));
9826
10088
  const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
9827
10089
  family: f.family,
9828
10090
  weight: f.weight,
@@ -9851,7 +10113,7 @@ function emitBundleV1(opts) {
9851
10113
  // resolvable via verify's --set override).
9852
10114
  path: (() => {
9853
10115
  const base = process.env["INIT_CWD"] ?? process.cwd();
9854
- const rel = path30.relative(base, opts.task.set);
10116
+ const rel = path31.relative(base, opts.task.set);
9855
10117
  return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
9856
10118
  })(),
9857
10119
  component: opts.componentName,
@@ -9875,21 +10137,21 @@ function emitBundleV1(opts) {
9875
10137
  })
9876
10138
  };
9877
10139
  const written = [];
9878
- const manifestPath2 = path30.join(opts.bundleDir, "component.json");
10140
+ const manifestPath2 = path31.join(opts.bundleDir, "component.json");
9879
10141
  writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
9880
10142
  `);
9881
10143
  written.push(manifestPath2);
9882
- const stylesPath = path30.join(opts.bundleDir, "styles.css");
9883
- if (existsSync24(stylesPath)) {
10144
+ const stylesPath = path31.join(opts.bundleDir, "styles.css");
10145
+ if (existsSync25(stylesPath)) {
9884
10146
  const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
9885
- const current = readFileSync20(stylesPath, "utf8");
10147
+ const current = readFileSync21(stylesPath, "utf8");
9886
10148
  const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
9887
10149
  writeFileSync11(stylesPath, `${comment}
9888
10150
  ${stripped}`);
9889
10151
  written.push(stylesPath);
9890
10152
  }
9891
- const fontsCssPath = path30.join(opts.bundleDir, "fonts.css");
9892
- 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 });
9893
10155
  rmSync3(fontsCssPath, { force: true });
9894
10156
  const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
9895
10157
  if (fontsCss !== null) {
@@ -9897,7 +10159,7 @@ ${stripped}`);
9897
10159
  written.push(fontsCssPath);
9898
10160
  }
9899
10161
  const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
9900
- 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\``;
9901
10163
  return { manifest, statusLine, written };
9902
10164
  }
9903
10165
  var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
@@ -10334,9 +10596,9 @@ var init_src7 = __esm({
10334
10596
  });
10335
10597
 
10336
10598
  // packages/cli/src/font-guidance.ts
10337
- import path31 from "node:path";
10599
+ import path32 from "node:path";
10338
10600
  function fontsUnprovenRemediation(setDir) {
10339
- const set = setDir === void 0 ? void 0 : path31.resolve(setDir);
10601
+ const set = setDir === void 0 ? void 0 : path32.resolve(setDir);
10340
10602
  if (set !== void 0) {
10341
10603
  try {
10342
10604
  const needs = recordedFontNeeds(set);
@@ -10404,13 +10666,15 @@ __export(fonts_exports, {
10404
10666
  DEFAULT_FONT_CACHE: () => DEFAULT_FONT_CACHE,
10405
10667
  familyMismatch: () => familyMismatch,
10406
10668
  runFontsAdd: () => runFontsAdd,
10669
+ runFontsAddSystem: () => runFontsAddSystem,
10670
+ runFontsDiscover: () => runFontsDiscover,
10407
10671
  runFontsRequired: () => runFontsRequired,
10408
10672
  runFontsResolve: () => runFontsResolve,
10409
10673
  runFontsResolveSet: () => runFontsResolveSet,
10410
10674
  runFontsStatus: () => runFontsStatus
10411
10675
  });
10412
- import { existsSync as existsSync25, readFileSync as readFileSync21 } from "node:fs";
10413
- import path32 from "node:path";
10676
+ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
10677
+ import path33 from "node:path";
10414
10678
  async function runFontsResolve(opts) {
10415
10679
  const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
10416
10680
  emitData(opts, result, () => {
@@ -10425,7 +10689,7 @@ async function runFontsResolve(opts) {
10425
10689
  }
10426
10690
  }
10427
10691
  async function runFontsResolveSet(opts) {
10428
- 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);
10429
10693
  let needs = [];
10430
10694
  try {
10431
10695
  needs = recordedFontNeeds(setDir);
@@ -10488,6 +10752,12 @@ async function runFontsResolveSet(opts) {
10488
10752
  `);
10489
10753
  for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
10490
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
+ }
10491
10761
  });
10492
10762
  if (byteDrift.length > 0) {
10493
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`);
@@ -10498,16 +10768,16 @@ async function runFontsResolveSet(opts) {
10498
10768
  }
10499
10769
  }
10500
10770
  function runFontsStatus(opts) {
10501
- const manifestPath2 = path32.join(opts.cacheDir, "manifest.json");
10502
- if (!existsSync25(manifestPath2)) {
10771
+ const manifestPath2 = path33.join(opts.cacheDir, "manifest.json");
10772
+ if (!existsSync26(manifestPath2)) {
10503
10773
  fail(opts, ExitCode.FontsUnproven, {
10504
10774
  error: `no font cache at ${opts.cacheDir}`,
10505
10775
  code: "fonts-unresolved",
10506
10776
  remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
10507
10777
  });
10508
10778
  }
10509
- const faces = JSON.parse(readFileSync21(manifestPath2, "utf8"));
10510
- 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;
10511
10781
  emitData(opts, { faces, lockVerdicts }, () => {
10512
10782
  for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
10513
10783
  `);
@@ -10551,13 +10821,13 @@ function familyMismatch(family, declared) {
10551
10821
  }
10552
10822
  function runFontsAdd(opts) {
10553
10823
  if (opts.set !== void 0) {
10554
- const declared = taskFontFamilies(path32.resolve(opts.set)) ?? [];
10824
+ const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
10555
10825
  const mismatch = familyMismatch(opts.family, declared);
10556
10826
  if (mismatch !== void 0) {
10557
10827
  fail(opts, ExitCode.InputValidation, {
10558
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.`,
10559
10829
  code: "font-family-not-declared",
10560
- 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.`
10561
10831
  });
10562
10832
  }
10563
10833
  } else {
@@ -10584,6 +10854,73 @@ function runFontsAdd(opts) {
10584
10854
  `);
10585
10855
  });
10586
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
+ }
10587
10924
  var init_fonts = __esm({
10588
10925
  "packages/cli/src/commands/fonts.ts"() {
10589
10926
  "use strict";
@@ -10614,15 +10951,24 @@ __export(verify_exports, {
10614
10951
  resolveComposition: () => resolveComposition,
10615
10952
  runVerify: () => runVerify
10616
10953
  });
10617
- import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
10618
- import path33 from "node:path";
10954
+ import { existsSync as existsSync27, readFileSync as readFileSync23 } from "node:fs";
10955
+ import path34 from "node:path";
10619
10956
  function interactionCoverage(behaviors) {
10620
- 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:"));
10621
10959
  return {
10622
10960
  interactionChecks: interaction.length,
10623
10961
  interactionPassed: interaction.filter((b) => b.pass).length,
10624
- preludeChecks: behaviors.length - interaction.length,
10625
- 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"
10626
10972
  };
10627
10973
  }
10628
10974
  function operabilityReport(input) {
@@ -10655,6 +11001,16 @@ function operabilityReport(input) {
10655
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.`
10656
11002
  };
10657
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
+ }
10658
11014
  return { checks: interactionChecks, passed: interactionPassed };
10659
11015
  }
10660
11016
  function operabilityLine(state) {
@@ -10747,7 +11103,7 @@ function compositionReport(input) {
10747
11103
  function eyeCheck(bundleDir) {
10748
11104
  return {
10749
11105
  command: tendrilCommand(`inspect "${bundleDir}"`),
10750
- sheetPath: path33.join(bundleDir, "verify-evidence", "inspect.html"),
11106
+ sheetPath: path34.join(bundleDir, "verify-evidence", "inspect.html"),
10751
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."
10752
11108
  };
10753
11109
  }
@@ -10759,7 +11115,7 @@ function taskFromManifest(opts, manifest, setDir) {
10759
11115
  const adapterSlugs = Object.keys(manifest.propAdapter);
10760
11116
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
10761
11117
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
10762
- 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));
10763
11119
  const authored = (() => {
10764
11120
  if (registry !== void 0) return void 0;
10765
11121
  try {
@@ -10785,6 +11141,10 @@ function taskFromManifest(opts, manifest, setDir) {
10785
11141
  task,
10786
11142
  unmapped,
10787
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,
10788
11148
  // Registry sets carry hand-declared behaviors and no derivation
10789
11149
  // runs, so their evidence is UNKNOWN, not empty: an empty list is
10790
11150
  // spent downstream as "the recording holds no interactive pose".
@@ -10794,19 +11154,19 @@ function taskFromManifest(opts, manifest, setDir) {
10794
11154
  }
10795
11155
  async function runVerify(opts) {
10796
11156
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
10797
- const setOverride = opts.set !== void 0 ? path33.resolve(callerCwd, opts.set) : void 0;
10798
- opts = { ...opts, bundleDir: path33.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
10799
- 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)) {
10800
11160
  fail(opts, ExitCode.InputValidation, {
10801
11161
  error: `bundle directory not found: ${opts.bundleDir}`,
10802
11162
  code: "bundle-missing",
10803
11163
  remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
10804
11164
  });
10805
11165
  }
10806
- const manifestPath2 = path33.join(opts.bundleDir, "component.json");
11166
+ const manifestPath2 = path34.join(opts.bundleDir, "component.json");
10807
11167
  let manifest;
10808
- if (existsSync26(manifestPath2)) {
10809
- const { manifest: parsed, issues } = readBundleManifest(readFileSync22(manifestPath2, "utf8"));
11168
+ if (existsSync27(manifestPath2)) {
11169
+ const { manifest: parsed, issues } = readBundleManifest(readFileSync23(manifestPath2, "utf8"));
10810
11170
  if (issues.length > 0) {
10811
11171
  fail(opts, ExitCode.InputValidation, {
10812
11172
  error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -10820,6 +11180,7 @@ async function runVerify(opts) {
10820
11180
  let unmapped = [];
10821
11181
  let interactionEvidence;
10822
11182
  let unmappedInteractionEvidence = [];
11183
+ let authorityConfigs;
10823
11184
  let availability = ROLES_NOT_RESOLVED;
10824
11185
  if (opts.task !== void 0) {
10825
11186
  const registry = TASKS[opts.task];
@@ -10833,21 +11194,21 @@ async function runVerify(opts) {
10833
11194
  task = registry;
10834
11195
  } else if (manifest !== void 0) {
10835
11196
  const resolveSetDir = (p) => {
10836
- if (path33.isAbsolute(p)) return p;
10837
- const fromRepo = path33.resolve(REPO_ROOT, p);
10838
- if (existsSync26(fromRepo)) return fromRepo;
10839
- 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);
10840
11201
  };
10841
11202
  const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
10842
- 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))) {
10843
11204
  fail(opts, ExitCode.RecordingIncomplete, {
10844
11205
  error: `recording set not found or unmanifested: ${setDir}`,
10845
11206
  code: "recording-set-missing",
10846
11207
  remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
10847
11208
  });
10848
11209
  }
10849
- const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
10850
- 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"))) {
10851
11212
  const recordedSlugs = registry.configs.map((c) => c.rep);
10852
11213
  const adapterSlugs = Object.keys(manifest.propAdapter);
10853
11214
  unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
@@ -10856,10 +11217,12 @@ async function runVerify(opts) {
10856
11217
  entry: manifest.entry,
10857
11218
  configs: adapterSlugs.filter((s) => recordedSlugs.includes(s)).map((rep) => ({ rep, component: manifest.propAdapter[rep].component, props: manifest.propAdapter[rep].props }))
10858
11219
  };
11220
+ authorityConfigs = registry.configs;
10859
11221
  } else {
10860
11222
  const built = taskFromManifest(opts, manifest, setDir);
10861
11223
  task = built.task;
10862
11224
  unmapped = built.unmapped;
11225
+ authorityConfigs = built.authorityConfigs;
10863
11226
  interactionEvidence = built.interactionEvidence;
10864
11227
  unmappedInteractionEvidence = built.unmappedInteractionEvidence;
10865
11228
  for (const s of built.adapterOnly) warn(opts, `prop adapter maps "${s}" which is not in the recording set \u2014 ignored`);
@@ -10871,9 +11234,9 @@ async function runVerify(opts) {
10871
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`);
10872
11235
  }
10873
11236
  for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
10874
- const p = path33.join(opts.bundleDir, name);
10875
- if (!existsSync26(p)) continue;
10876
- 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)));
10877
11240
  if (issues.length > 0) {
10878
11241
  fail(opts, ExitCode.InputValidation, {
10879
11242
  error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -10911,7 +11274,7 @@ async function runVerify(opts) {
10911
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)`);
10912
11275
  }
10913
11276
  const missing = task.configs.filter(
10914
- (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"))
10915
11278
  );
10916
11279
  if (missing.length > 0) {
10917
11280
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -10921,12 +11284,12 @@ async function runVerify(opts) {
10921
11284
  });
10922
11285
  }
10923
11286
  const bar = BARS2[opts.bar];
10924
- const evidenceDir = path33.join(opts.bundleDir, "verify-evidence");
11287
+ const evidenceDir = path34.join(opts.bundleDir, "verify-evidence");
10925
11288
  const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
10926
11289
  const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
10927
- 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");
10928
11291
  const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
10929
- const parity = await checkHoverParity(task, opts.bundleDir);
11292
+ const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
10930
11293
  const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
10931
11294
  const structural = roles !== void 0 ? await checkStructuralComposition(task, opts.bundleDir, roles) : [];
10932
11295
  const regionsOut = roles !== void 0 ? interiorRegions(task.set, roles) : void 0;
@@ -11116,7 +11479,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
11116
11479
  }
11117
11480
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
11118
11481
  `);
11119
- 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")));
11120
11483
  if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
11121
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)
11122
11485
  `);
@@ -11206,18 +11569,18 @@ __export(engine_exports, {
11206
11569
  runEngineBrief: () => runEngineBrief,
11207
11570
  runEngineScore: () => runEngineScore
11208
11571
  });
11209
- import { appendFileSync, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
11210
- 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";
11211
11574
  function resolveEngineTask(opts, callerCwd) {
11212
- const asPath = path34.resolve(callerCwd, opts.taskOrSet);
11213
- 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"));
11214
11577
  const registry = TASKS[opts.taskOrSet];
11215
11578
  if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
11216
11579
  if (isSet) {
11217
11580
  try {
11218
11581
  const authored = authorTaskFromSet(asPath);
11219
11582
  for (const d of authored.disclosures) warn(opts, d);
11220
- 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 } };
11221
11584
  } catch (err) {
11222
11585
  fail(opts, ExitCode.InputValidation, {
11223
11586
  error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -11244,9 +11607,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
11244
11607
  const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
11245
11608
  const segments = buildSegments(task, "files");
11246
11609
  let notRecorded;
11247
- const manifestPath2 = path34.join(task.set, "recording-set.json");
11248
- if (existsSync27(manifestPath2)) {
11249
- 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;
11250
11613
  }
11251
11614
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
11252
11615
 
@@ -11254,7 +11617,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
11254
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.
11255
11618
  ${notRecorded}` : "";
11256
11619
  let fontProvisioning;
11257
- if (existsSync27(manifestPath2)) {
11620
+ if (existsSync28(manifestPath2)) {
11258
11621
  const missingFams = unprovisionedFamilies(task.set);
11259
11622
  const unprovided = unprovisionedFaces(task.set);
11260
11623
  const weightOnly = missingFams.length === 0;
@@ -11280,9 +11643,9 @@ ${notRecorded}` : "";
11280
11643
 
11281
11644
  === TASK PAYLOAD (recorded truth, verbatim) ===
11282
11645
  ${segments}`;
11283
- const payloadFile = path34.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
11284
- const candidateDirSuggestion = path34.resolve(callerCwd, `tendril-out/${name}-candidate`);
11285
- 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 });
11286
11649
  writeFileSync12(payloadFile, payload);
11287
11650
  emitData(
11288
11651
  opts,
@@ -11333,15 +11696,15 @@ ${segments}`;
11333
11696
  );
11334
11697
  }
11335
11698
  function appendScoreHistory(candidateDir, entry) {
11336
- 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 })}
11337
11700
  `);
11338
11701
  }
11339
11702
  async function runEngineScore(opts) {
11340
11703
  requireEntitlement(opts);
11341
11704
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
11342
- const candidateDir = path34.resolve(callerCwd, opts.candidateDir);
11705
+ const candidateDir = path35.resolve(callerCwd, opts.candidateDir);
11343
11706
  const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
11344
- if (!existsSync27(candidateDir)) {
11707
+ if (!existsSync28(candidateDir)) {
11345
11708
  fail(opts, ExitCode.InputValidation, {
11346
11709
  error: `candidate directory not found: ${candidateDir}`,
11347
11710
  code: "candidate-missing",
@@ -11366,10 +11729,10 @@ async function runEngineScore(opts) {
11366
11729
  for (const g of missingWeights(task.set)) {
11367
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)`);
11368
11731
  }
11369
- if (opts.rebind !== true && existsSync27(path34.join(candidateDir, "component.json"))) {
11732
+ if (opts.rebind !== true && existsSync28(path35.join(candidateDir, "component.json"))) {
11370
11733
  const prior = (() => {
11371
11734
  try {
11372
- const read = readBundleManifest(readFileSync23(path34.join(candidateDir, "component.json"), "utf8"));
11735
+ const read = readBundleManifest(readFileSync24(path35.join(candidateDir, "component.json"), "utf8"));
11373
11736
  return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
11374
11737
  } catch {
11375
11738
  return { unreadable: true };
@@ -11391,9 +11754,9 @@ async function runEngineScore(opts) {
11391
11754
  }
11392
11755
  }
11393
11756
  const bar = BARS3[opts.bar];
11394
- const evidenceDir = path34.join(candidateDir, "verify-evidence");
11757
+ const evidenceDir = path35.join(candidateDir, "verify-evidence");
11395
11758
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
11396
- const parity = await checkHoverParity(task, candidateDir);
11759
+ const parity = await checkHoverParity(task, candidateDir, task.configs);
11397
11760
  const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
11398
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)";
11399
11762
  const obj = objective(scores, behaviors);
@@ -11434,7 +11797,17 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
11434
11797
  environment: environmentStamp(taskFontFamilies(task.set)),
11435
11798
  substitutedFamilies
11436
11799
  });
11437
- 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
+ });
11438
11811
  const coverage = interactionCoverage(behaviors);
11439
11812
  const operability = operabilityReport({ behaviors, interactionEvidence });
11440
11813
  const evidenceUnverified = (interactionEvidence?.length ?? 0) > 0 && coverage.interactionChecks === 0;
@@ -11549,11 +11922,11 @@ var codeconnect_exports = {};
11549
11922
  __export(codeconnect_exports, {
11550
11923
  runCodeConnect: () => runCodeConnect
11551
11924
  });
11552
- import { existsSync as existsSync28, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
11553
- 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";
11554
11927
  function runCodeConnect(opts) {
11555
11928
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
11556
- const bundleDir = path35.resolve(callerCwd, opts.bundleDir);
11929
+ const bundleDir = path36.resolve(callerCwd, opts.bundleDir);
11557
11930
  let url;
11558
11931
  try {
11559
11932
  url = new URL(opts.figmaUrl);
@@ -11569,7 +11942,7 @@ function runCodeConnect(opts) {
11569
11942
  }
11570
11943
  let manifest;
11571
11944
  try {
11572
- const read = readBundleManifest(readFileSync24(path35.join(bundleDir, "component.json"), "utf8"));
11945
+ const read = readBundleManifest(readFileSync25(path36.join(bundleDir, "component.json"), "utf8"));
11573
11946
  if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
11574
11947
  manifest = read.manifest;
11575
11948
  } catch (err) {
@@ -11579,8 +11952,8 @@ function runCodeConnect(opts) {
11579
11952
  remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
11580
11953
  });
11581
11954
  }
11582
- const setDir = path35.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
11583
- 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"))) {
11584
11957
  fail(opts, ExitCode.InputValidation, {
11585
11958
  error: `recording set not found at ${setDir}`,
11586
11959
  code: "codeconnect-no-set",
@@ -11601,10 +11974,10 @@ function runCodeConnect(opts) {
11601
11974
  const component = api.component;
11602
11975
  const recManifest = loadManifest(setDir);
11603
11976
  const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
11604
- const meta = path35.join(setDir, r.slug, "get_metadata.json");
11605
- if (!existsSync28(meta)) return void 0;
11977
+ const meta = path36.join(setDir, r.slug, "get_metadata.json");
11978
+ if (!existsSync29(meta)) return void 0;
11606
11979
  try {
11607
- return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync24(meta, "utf8"))))?.[1];
11980
+ return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync25(meta, "utf8"))))?.[1];
11608
11981
  } catch {
11609
11982
  return void 0;
11610
11983
  }
@@ -11662,7 +12035,7 @@ function runCodeConnect(opts) {
11662
12035
  axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
11663
12036
  fragmentVars.push(varName);
11664
12037
  }
11665
- const entryRel = path35.relative(callerCwd, path35.join(bundleDir, manifest.entry));
12038
+ const entryRel = path36.relative(callerCwd, path36.join(bundleDir, manifest.entry));
11666
12039
  const trust = manifest.trustStatement.split("\n")[0] ?? "";
11667
12040
  const lines = [
11668
12041
  `// url=${opts.figmaUrl}`,
@@ -11683,7 +12056,7 @@ function runCodeConnect(opts) {
11683
12056
  `}`,
11684
12057
  ``
11685
12058
  ].join("\n");
11686
- 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`));
11687
12060
  writeFileSync13(outFile, lines);
11688
12061
  emitData(
11689
12062
  opts,
@@ -11723,17 +12096,17 @@ var init_codeconnect = __esm({
11723
12096
 
11724
12097
  // packages/mcp/src/server.ts
11725
12098
  import { createHash as createHash5 } from "node:crypto";
11726
- import { existsSync as existsSync29, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "node:fs";
11727
- import os6 from "node:os";
11728
- 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";
11729
12102
  import { fileURLToPath as fileURLToPath6 } from "node:url";
11730
12103
  import { z as z12 } from "zod";
11731
12104
  function sourceHash() {
11732
- const dir = path36.dirname(fileURLToPath6(import.meta.url));
12105
+ const dir = path37.dirname(fileURLToPath6(import.meta.url));
11733
12106
  const h = createHash5("sha256");
11734
- 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()) {
11735
12108
  h.update(f);
11736
- h.update(readFileSync25(path36.join(dir, f)));
12109
+ h.update(readFileSync26(path37.join(dir, f)));
11737
12110
  }
11738
12111
  return h.digest("hex").slice(0, 16);
11739
12112
  }
@@ -11741,10 +12114,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
11741
12114
  var init_server = __esm({
11742
12115
  "packages/mcp/src/server.ts"() {
11743
12116
  "use strict";
11744
- REPO_ROOT3 = path36.resolve(path36.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
11745
- CLI_BIN = path36.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
11746
- BUNDLED_CLI = path36.join(path36.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
11747
- 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] };
11748
12121
  str = (d) => z12.string().describe(d);
11749
12122
  optStr = (d) => z12.string().optional().describe(d);
11750
12123
  TOOLS = [
@@ -11774,7 +12147,7 @@ var init_server = __esm({
11774
12147
  const single = i["metadata"];
11775
12148
  const parts = i["metadataParts"];
11776
12149
  if (single !== void 0 || parts !== void 0) {
11777
- 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");
11778
12151
  if (single !== void 0) {
11779
12152
  writeFileSync14(tmp, single);
11780
12153
  argvOut.push("--metadata-raw-file", tmp);
@@ -11791,7 +12164,7 @@ var init_server = __esm({
11791
12164
  },
11792
12165
  {
11793
12166
  name: "tendril_permissions",
11794
- 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.",
11795
12168
  schema: z12.object({
11796
12169
  write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
11797
12170
  }),
@@ -11845,7 +12218,7 @@ var init_server = __esm({
11845
12218
  const bridge = (label, single, parts) => {
11846
12219
  if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
11847
12220
  if (single === void 0 && parts === void 0) return;
11848
- 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");
11849
12222
  if (single !== void 0) {
11850
12223
  writeFileSync14(tmp, single);
11851
12224
  argvOut.push(`--${label}-file`, tmp);
@@ -11888,7 +12261,7 @@ var init_server = __esm({
11888
12261
  const file = i["file"];
11889
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)");
11890
12263
  if (file !== void 0) return [...base, "--file", file];
11891
- 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");
11892
12265
  if (text !== void 0) {
11893
12266
  writeFileSync14(tmp, text);
11894
12267
  return [...base, "--file", tmp, "--raw"];
@@ -11951,7 +12324,7 @@ var init_server = __esm({
11951
12324
  },
11952
12325
  {
11953
12326
  name: "tendril_engine_score",
11954
- 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.",
11955
12328
  schema: z12.object({
11956
12329
  taskOrSet: str("reference task name or recording-set directory"),
11957
12330
  candidateDir: str("directory containing the proposed bundle files"),
@@ -12041,15 +12414,16 @@ __export(permissions_exports, {
12041
12414
  PERMISSIONS_DESCRIPTION: () => PERMISSIONS_DESCRIPTION,
12042
12415
  buildPermissions: () => buildPermissions,
12043
12416
  mergeAllowlist: () => mergeAllowlist,
12044
- runPermissions: () => runPermissions
12417
+ runPermissions: () => runPermissions,
12418
+ writeSelection: () => writeSelection
12045
12419
  });
12046
- import { existsSync as existsSync30, mkdirSync as mkdirSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
12047
- import os7 from "node:os";
12048
- import path37 from "node:path";
12049
- 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 = []) {
12050
12424
  let settings = {};
12051
- if (existsSync30(file) && readFileSync26(file, "utf8").trim() !== "") {
12052
- settings = JSON.parse(readFileSync26(file, "utf8"));
12425
+ if (existsSync31(file) && readFileSync27(file, "utf8").trim() !== "") {
12426
+ settings = JSON.parse(readFileSync27(file, "utf8"));
12053
12427
  if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
12054
12428
  }
12055
12429
  const permissions = settings["permissions"] ??= {};
@@ -12059,13 +12433,21 @@ function mergeAllowlist(file, entries) {
12059
12433
  const present = new Set(allow.filter((x) => typeof x === "string"));
12060
12434
  const added = entries.filter((e) => !present.has(e));
12061
12435
  const alreadyPresent = entries.filter((e) => present.has(e));
12062
- 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) {
12063
12445
  allow.push(...added);
12064
- mkdirSync9(path37.dirname(file), { recursive: true });
12446
+ mkdirSync9(path38.dirname(file), { recursive: true });
12065
12447
  writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
12066
12448
  `);
12067
12449
  }
12068
- return { added, alreadyPresent };
12450
+ return { added, alreadyPresent, denyAdded };
12069
12451
  }
12070
12452
  async function buildPermissions(options) {
12071
12453
  const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
@@ -12085,10 +12467,18 @@ async function buildPermissions(options) {
12085
12467
  ...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${TENDRIL_PLUGIN_PREFIX}__${n}`),
12086
12468
  ...figmaTools.map((name) => `${FIGMA_PLUGIN_PREFIX}__${name}`)
12087
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.",
12088
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.",
12089
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).`
12090
12477
  };
12091
12478
  }
12479
+ function writeSelection(result, user) {
12480
+ return user ? { entries: [...result.toolEntries], denyEntries: [] } : { entries: [...result.toolEntries, ...result.shellEntries, ...result.projectFileEntries], denyEntries: [...result.projectDenyEntries] };
12481
+ }
12092
12482
  async function runPermissions(flags) {
12093
12483
  if (flags.describe) {
12094
12484
  printDescription(PERMISSIONS_DESCRIPTION);
@@ -12097,21 +12487,25 @@ async function runPermissions(flags) {
12097
12487
  const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
12098
12488
  if (flags.write) {
12099
12489
  const base = process.env["INIT_CWD"] ?? process.cwd();
12100
- 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);
12101
12492
  if (flags.dryRun) {
12102
- emitData(flags, { file, wouldAdd: result.toolEntries }, () => {
12103
- process.stdout.write(`dry-run: would merge ${result.toolEntries.length} per-tool entries into ${file}
12104
- `);
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
+ );
12105
12498
  });
12106
12499
  return;
12107
12500
  }
12108
12501
  try {
12109
- const { added, alreadyPresent } = mergeAllowlist(file, result.toolEntries);
12110
- 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 } }, () => {
12111
12504
  process.stdout.write(
12112
- added.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
12113
- ` : `installed: ${added.length} allowlist entr${added.length === 1 ? "y" : "ies"} added to ${file}${alreadyPresent.length > 0 ? ` (${alreadyPresent.length} already present)` : ""}
12114
- 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
12115
12509
  `
12116
12510
  );
12117
12511
  });
@@ -12136,6 +12530,24 @@ Or paste into .claude/settings.json under permissions.allow:
12136
12530
 
12137
12531
  ${quoted(result.toolEntries)}
12138
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
+
12139
12551
  Shorter but broader \u2014 one entry per server:
12140
12552
 
12141
12553
  ${quoted(result.serverEntries)}
@@ -12147,7 +12559,7 @@ ${result.directConfigNote}
12147
12559
  );
12148
12560
  });
12149
12561
  }
12150
- 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;
12151
12563
  var init_permissions = __esm({
12152
12564
  "packages/cli/src/commands/permissions.ts"() {
12153
12565
  "use strict";
@@ -12159,14 +12571,44 @@ var init_permissions = __esm({
12159
12571
  init_output();
12160
12572
  init_doctor();
12161
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:*)"];
12162
12604
  PERMISSIONS_DESCRIPTION = {
12163
12605
  name: "permissions",
12164
12606
  summary: "Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools).",
12165
12607
  args: [],
12166
12608
  flags: [
12167
12609
  { flag: "--claude", description: "Claude Code settings format (the default and currently only format)" },
12168
- { 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)" },
12169
- { 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" },
12170
12612
  { flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
12171
12613
  { flag: "--json", description: "Machine-readable output" }
12172
12614
  ],
@@ -12175,7 +12617,7 @@ var init_permissions = __esm({
12175
12617
  serverEntries: "string[] \u2014 one entry per MCP server (allows every tool it serves)",
12176
12618
  toolEntries: "string[] \u2014 per-tool entries for selective allowlists",
12177
12619
  directConfigNote: "string \u2014 prefix rewrite for non-plugin (claude mcp add) installs",
12178
- written: "with --write: { file, added, alreadyPresent }"
12620
+ written: "with --write: { file, added, alreadyPresent, denyAdded }"
12179
12621
  },
12180
12622
  exitCodes: { 0: "printed or written", 3: "with --write: the target file exists but is not JSON this command can safely rewrite" },
12181
12623
  examples: ["tendril permissions --claude --write", "tendril permissions --claude", "tendril permissions --claude --json"]
@@ -12191,24 +12633,24 @@ __export(inspect_exports, {
12191
12633
  INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
12192
12634
  runInspect: () => runInspect
12193
12635
  });
12194
- import { existsSync as existsSync31, readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
12195
- 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";
12196
12638
  async function runInspect(opts) {
12197
12639
  if (opts.describe) {
12198
12640
  printDescription(INSPECT_DESCRIPTION);
12199
12641
  return;
12200
12642
  }
12201
- const bundleDir = path38.resolve(opts.bundleDir);
12202
- const evidenceDir = path38.join(bundleDir, "verify-evidence");
12203
- const manifestPath2 = path38.join(bundleDir, "component.json");
12204
- 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)) {
12205
12647
  fail(opts, ExitCode.InputValidation, {
12206
- 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"}`,
12207
12649
  code: "no-evidence",
12208
12650
  remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
12209
12651
  });
12210
12652
  }
12211
- const { manifest } = readBundleManifest(readFileSync27(manifestPath2, "utf8"));
12653
+ const { manifest } = readBundleManifest(readFileSync28(manifestPath2, "utf8"));
12212
12654
  if (manifest === void 0) {
12213
12655
  fail(opts, ExitCode.InputValidation, {
12214
12656
  error: "component.json did not parse as a bundle manifest",
@@ -12216,8 +12658,8 @@ async function runInspect(opts) {
12216
12658
  remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
12217
12659
  });
12218
12660
  }
12219
- const setDir = path38.resolve(opts.set ?? manifest.provenance.recordingSet.path);
12220
- 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`)));
12221
12663
  if (reps.length === 0) {
12222
12664
  fail(opts, ExitCode.InputValidation, {
12223
12665
  error: "verify-evidence holds no ref/render pairs for this bundle's configs",
@@ -12228,15 +12670,15 @@ async function runInspect(opts) {
12228
12670
  let crops = 0;
12229
12671
  const sections = [];
12230
12672
  for (const rep of reps) {
12231
- const ref = new Uint8Array(readFileSync27(path38.join(evidenceDir, `${rep}-ref.png`)));
12232
- 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`)));
12233
12675
  const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
12234
12676
  const cells = [];
12235
12677
  for (const [i, n] of nodes.entries()) {
12236
12678
  const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
12237
12679
  try {
12238
- writeFileSync16(path38.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
12239
- 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));
12240
12682
  } catch {
12241
12683
  continue;
12242
12684
  }
@@ -12249,7 +12691,7 @@ async function runInspect(opts) {
12249
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>`
12250
12692
  );
12251
12693
  }
12252
- const sheet = path38.join(evidenceDir, "inspect.html");
12694
+ const sheet = path39.join(evidenceDir, "inspect.html");
12253
12695
  writeFileSync16(
12254
12696
  sheet,
12255
12697
  `<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
@@ -12327,17 +12769,17 @@ __export(generate_recorded_exports, {
12327
12769
  runGenerateRecorded: () => runGenerateRecorded
12328
12770
  });
12329
12771
  import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
12330
- import { existsSync as existsSync32, readFileSync as readFileSync28 } from "node:fs";
12331
- import path39 from "node:path";
12772
+ import { existsSync as existsSync33, readFileSync as readFileSync29 } from "node:fs";
12773
+ import path40 from "node:path";
12332
12774
  async function runGenerateRecorded(opts) {
12333
12775
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
12334
- const outDirAbs = path39.resolve(callerCwd, opts.out);
12335
- const recordedAsPath = path39.resolve(callerCwd, opts.recorded);
12776
+ const outDirAbs = path40.resolve(callerCwd, opts.out);
12777
+ const recordedAsPath = path40.resolve(callerCwd, opts.recorded);
12336
12778
  let task;
12337
12779
  let taskName;
12338
12780
  let authoredApi;
12339
12781
  let composition;
12340
- const isSet = existsSync32(path39.join(recordedAsPath, "recording-set.json"));
12782
+ const isSet = existsSync33(path40.join(recordedAsPath, "recording-set.json"));
12341
12783
  const registry = TASKS[opts.recorded];
12342
12784
  if (registry !== void 0 && !isSet) {
12343
12785
  task = registry;
@@ -12346,7 +12788,7 @@ async function runGenerateRecorded(opts) {
12346
12788
  try {
12347
12789
  const authored = authorTaskFromSet(recordedAsPath);
12348
12790
  task = authored.task;
12349
- taskName = path39.basename(recordedAsPath);
12791
+ taskName = path40.basename(recordedAsPath);
12350
12792
  authoredApi = authored.api;
12351
12793
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
12352
12794
  if (roles.success) composition = roles.data;
@@ -12380,7 +12822,7 @@ async function runGenerateRecorded(opts) {
12380
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)`);
12381
12823
  }
12382
12824
  const missing = task.configs.filter(
12383
- (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"))
12384
12826
  );
12385
12827
  if (missing.length > 0) {
12386
12828
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -12450,8 +12892,8 @@ async function runGenerateRecorded(opts) {
12450
12892
  ` : `${line}
12451
12893
  `);
12452
12894
  if (opts.dryRun) {
12453
- emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path39.join(outDirAbs, taskName) }, () => {
12454
- 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)})
12455
12897
  `);
12456
12898
  });
12457
12899
  return;
@@ -12474,10 +12916,10 @@ async function runGenerateRecorded(opts) {
12474
12916
  });
12475
12917
  }
12476
12918
  }
12477
- const bundleDir = path39.join(outDirAbs, taskName);
12478
- if (existsSync32(path39.join(bundleDir, "component.json"))) {
12919
+ const bundleDir = path40.join(outDirAbs, taskName);
12920
+ if (existsSync33(path40.join(bundleDir, "component.json"))) {
12479
12921
  try {
12480
- const prior = readBundleManifest(readFileSync28(path39.join(bundleDir, "component.json"), "utf8")).manifest;
12922
+ const prior = readBundleManifest(readFileSync29(path40.join(bundleDir, "component.json"), "utf8")).manifest;
12481
12923
  if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
12482
12924
  fail(opts, ExitCode.InputValidation, {
12483
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`,
@@ -12643,7 +13085,7 @@ init_invocation();
12643
13085
  init_output();
12644
13086
  import { intro, isCancel, outro, password } from "@clack/prompts";
12645
13087
  import fs from "node:fs";
12646
- import path24 from "node:path";
13088
+ import path25 from "node:path";
12647
13089
  var INIT_DESCRIPTION = {
12648
13090
  name: "init",
12649
13091
  summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
@@ -12680,7 +13122,7 @@ async function runInit(flags) {
12680
13122
  printDescription(INIT_DESCRIPTION);
12681
13123
  return;
12682
13124
  }
12683
- const envPath = path24.resolve(process.cwd(), ".env");
13125
+ const envPath = path25.resolve(process.cwd(), ".env");
12684
13126
  const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
12685
13127
  let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
12686
13128
  let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
@@ -12701,7 +13143,7 @@ async function runInit(flags) {
12701
13143
  next.set(ENV_KEYS.figma, figmaToken);
12702
13144
  next.set(ENV_KEYS.openrouter, openrouterKey);
12703
13145
  const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
12704
- const gitignorePath = path24.resolve(process.cwd(), ".gitignore");
13146
+ const gitignorePath = path25.resolve(process.cwd(), ".gitignore");
12705
13147
  const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
12706
13148
  const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
12707
13149
  if (flags.dryRun) {
@@ -12757,14 +13199,14 @@ init_invocation();
12757
13199
  init_output();
12758
13200
  init_entitlement();
12759
13201
  import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
12760
- 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";
12761
13203
 
12762
13204
  // packages/cli/src/pipeline.ts
12763
13205
  init_src2();
12764
13206
  init_src4();
12765
13207
  init_src6();
12766
13208
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
12767
- import path25 from "node:path";
13209
+ import path26 from "node:path";
12768
13210
 
12769
13211
  // packages/cli/src/assets-module.ts
12770
13212
  init_src();
@@ -13100,7 +13542,7 @@ async function runGenerationPipeline(input) {
13100
13542
  });
13101
13543
  const written = [];
13102
13544
  if (!input.dryRun) {
13103
- const dir = path25.resolve(input.outDir, semantics.componentName);
13545
+ const dir = path26.resolve(input.outDir, semantics.componentName);
13104
13546
  mkdirSync5(dir, { recursive: true });
13105
13547
  const files = {
13106
13548
  // Bundle-local tokens: THE emission the component's CSS resolves
@@ -13124,13 +13566,13 @@ async function runGenerationPipeline(input) {
13124
13566
  `
13125
13567
  };
13126
13568
  for (const [name, content] of Object.entries(files)) {
13127
- const filePath = path25.join(dir, name);
13569
+ const filePath = path26.join(dir, name);
13128
13570
  writeFileSync8(filePath, content);
13129
13571
  written.push(filePath);
13130
13572
  }
13131
13573
  for (const artifact of emitTokenArtifacts(input.mapping)) {
13132
- const filePath = path25.resolve(input.outDir, artifact.path);
13133
- mkdirSync5(path25.dirname(filePath), { recursive: true });
13574
+ const filePath = path26.resolve(input.outDir, artifact.path);
13575
+ mkdirSync5(path26.dirname(filePath), { recursive: true });
13134
13576
  writeFileSync8(filePath, artifact.content);
13135
13577
  written.push(filePath);
13136
13578
  }
@@ -13189,7 +13631,7 @@ var GENERATE_DESCRIPTION = {
13189
13631
  function resolveProvidedSource(flags, contextFile) {
13190
13632
  let raw;
13191
13633
  try {
13192
- raw = readFileSync15(contextFile, "utf8");
13634
+ raw = readFileSync16(contextFile, "utf8");
13193
13635
  } catch {
13194
13636
  fail(flags, ExitCode.InputValidation, {
13195
13637
  error: `Cannot read context file "${contextFile}".`,
@@ -13309,11 +13751,11 @@ token mapping (${mapping.flat.length} variables):
13309
13751
  let initialCode;
13310
13752
  let initialSemantics;
13311
13753
  try {
13312
- if (existsSync19(flags.out)) {
13313
- for (const entry of readdirSync4(flags.out)) {
13754
+ if (existsSync20(flags.out)) {
13755
+ for (const entry of readdirSync5(flags.out)) {
13314
13756
  const cjPath = `${flags.out}/${entry}/component.json`;
13315
- if (!existsSync19(cjPath)) continue;
13316
- const cj = JSON.parse(readFileSync15(cjPath, "utf8"));
13757
+ if (!existsSync20(cjPath)) continue;
13758
+ const cj = JSON.parse(readFileSync16(cjPath, "utf8"));
13317
13759
  if (cj.name !== void 0 && Array.isArray(cj.props)) {
13318
13760
  previousApi = JSON.stringify({
13319
13761
  componentName: cj.name,
@@ -13321,14 +13763,14 @@ token mapping (${mapping.flat.length} variables):
13321
13763
  });
13322
13764
  const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
13323
13765
  const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
13324
- if (flags.refine && existsSync19(tsxPath) && existsSync19(cssPath)) {
13766
+ if (flags.refine && existsSync20(tsxPath) && existsSync20(cssPath)) {
13325
13767
  initialCode = {
13326
- tsx: readFileSync15(tsxPath, "utf8"),
13327
- css: readFileSync15(cssPath, "utf8")
13768
+ tsx: readFileSync16(tsxPath, "utf8"),
13769
+ css: readFileSync16(cssPath, "utf8")
13328
13770
  };
13329
13771
  const semPath = `${flags.out}/${entry}/semantics.json`;
13330
- if (existsSync19(semPath)) {
13331
- initialSemantics = JSON.parse(readFileSync15(semPath, "utf8"));
13772
+ if (existsSync20(semPath)) {
13773
+ initialSemantics = JSON.parse(readFileSync16(semPath, "utf8"));
13332
13774
  }
13333
13775
  }
13334
13776
  break;
@@ -13605,6 +14047,23 @@ function buildProgram() {
13605
14047
  ...local["face"] !== void 0 ? { face: Number(local["face"]) } : {}
13606
14048
  });
13607
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
+ });
13608
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) => {
13609
14068
  const flags = globalFlags(cmd.parent.parent);
13610
14069
  const local = cmd.opts();