@tendrilapp/cli 0.1.21 → 0.1.24

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
@@ -939,14 +939,25 @@ function manifestMains(manifest) {
939
939
  function requiredToolsFor(manifest, slug) {
940
940
  return manifestMains(manifest).includes(slug) ? [...REQUIRED_TOOLS, INTERIOR_TOOL] : REQUIRED_TOOLS;
941
941
  }
942
- function loadManifest(setDir) {
942
+ function readManifestFile(setDir) {
943
943
  const p = manifestPath(setDir);
944
944
  if (!existsSync(p)) throw new Error(`no recording-set.json in ${setDir} \u2014 run \`tendril record plan\` first`);
945
- return SessionManifestSchema.parse(JSON.parse(readFileSync(p, "utf8")));
945
+ const raw = JSON.parse(readFileSync(p, "utf8"));
946
+ return { manifest: SessionManifestSchema.parse(raw), raw };
947
+ }
948
+ function loadManifest(setDir) {
949
+ return readManifestFile(setDir).manifest;
950
+ }
951
+ function writeManifest(setDir, raw) {
952
+ const manifest = SessionManifestSchema.parse(raw);
953
+ mkdirSync(setDir, { recursive: true });
954
+ writeFileSync(manifestPath(setDir), `${JSON.stringify(raw, null, 1)}
955
+ `);
956
+ return manifest;
946
957
  }
947
958
  function planSet(setDir, component, symbols, opts = {}) {
948
959
  if (existsSync(manifestPath(setDir))) {
949
- const existing = loadManifest(setDir);
960
+ const { manifest: existing, raw } = readManifestFile(setDir);
950
961
  const wantsNewDefaults = opts.defaults !== void 0 && JSON.stringify(opts.defaults) !== JSON.stringify(existing.defaults ?? {});
951
962
  const anythingRecorded = existing.reps.some((r) => RECORD_TOOLS.some((t) => existsSync(path.join(setDir, r.slug, `${t}.json`))));
952
963
  if (!wantsNewDefaults || anythingRecorded) {
@@ -963,12 +974,11 @@ function planSet(setDir, component, symbols, opts = {}) {
963
974
  usedSlugs.add(slug);
964
975
  return { slug, nodeId: r.nodeId, ...r.sourceFrame !== void 0 ? { sourceFrame: r.sourceFrame } : {} };
965
976
  });
966
- existing.reps.push(...appended);
967
- existing.planMode = "full";
968
- delete existing.notRecorded;
969
- writeFileSync(manifestPath(setDir), `${JSON.stringify(existing, null, 1)}
970
- `);
971
- return { manifest: existing, plan: { reps: [], notRecorded: [] }, resumed: true, toppedUp: appended.map((a) => ({ slug: a.slug, nodeId: a.nodeId })) };
977
+ raw["reps"].push(...appended);
978
+ raw["planMode"] = "full";
979
+ delete raw["notRecorded"];
980
+ const manifest2 = writeManifest(setDir, raw);
981
+ return { manifest: manifest2, plan: { reps: [], notRecorded: [] }, resumed: true, toppedUp: appended.map((a) => ({ slug: a.slug, nodeId: a.nodeId })) };
972
982
  }
973
983
  }
974
984
  return { manifest: existing, plan: { reps: [], notRecorded: [] }, resumed: true };
@@ -989,9 +999,7 @@ function planSet(setDir, component, symbols, opts = {}) {
989
999
  planMode: opts.sample === true ? "sample" : "full",
990
1000
  ...plan.notRecorded.length > 0 ? { notRecorded: plan.notRecorded.map((n) => `${n.name} (${n.nodeId}): ${n.reason}`).join("; ") } : {}
991
1001
  };
992
- mkdirSync(setDir, { recursive: true });
993
- writeFileSync(manifestPath(setDir), `${JSON.stringify(manifest, null, 1)}
994
- `);
1002
+ writeManifest(setDir, manifest);
995
1003
  return { manifest, plan, resumed: false };
996
1004
  }
997
1005
  function sessionStatus(setDir) {
@@ -1216,8 +1224,8 @@ var init_src = __esm({
1216
1224
  function variableNameToPath(name) {
1217
1225
  return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
1218
1226
  }
1219
- function tokenPathToCssVar(path38) {
1220
- return `--${path38.join("-")}`;
1227
+ function tokenPathToCssVar(path40) {
1228
+ return `--${path40.join("-")}`;
1221
1229
  }
1222
1230
  function toDtcgToken(variable, defaultMode) {
1223
1231
  const modes = Object.keys(variable.valuesByMode);
@@ -1261,11 +1269,11 @@ function toDtcgToken(variable, defaultMode) {
1261
1269
  }
1262
1270
  function mapVariablesToDtcg(variables, defaultMode = "light") {
1263
1271
  const entries = variables.map((variable) => {
1264
- const path38 = variableNameToPath(variable.name);
1265
- if (path38.length === 0) {
1272
+ const path40 = variableNameToPath(variable.name);
1273
+ if (path40.length === 0) {
1266
1274
  throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
1267
1275
  }
1268
- return { variable, path: path38 };
1276
+ return { variable, path: path40 };
1269
1277
  });
1270
1278
  const groupPrefixes = /* @__PURE__ */ new Set();
1271
1279
  for (const e of entries) {
@@ -1286,21 +1294,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
1286
1294
  }
1287
1295
  const tokens = {};
1288
1296
  const flat = [];
1289
- for (const { variable, path: path38 } of entries) {
1297
+ for (const { variable, path: path40 } of entries) {
1290
1298
  const token = toDtcgToken(variable, defaultMode);
1291
1299
  let group = tokens;
1292
- for (const segment of path38.slice(0, -1)) {
1300
+ for (const segment of path40.slice(0, -1)) {
1293
1301
  const existing = group[segment];
1294
1302
  group = existing ?? (group[segment] = {});
1295
1303
  }
1296
- const leaf = path38[path38.length - 1];
1304
+ const leaf = path40[path40.length - 1];
1297
1305
  if (group[leaf] !== void 0) {
1298
- throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path38.join(".")}" (variable ${variable.id})`);
1306
+ throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path40.join(".")}" (variable ${variable.id})`);
1299
1307
  }
1300
1308
  group[leaf] = token;
1301
1309
  flat.push({
1302
- path: path38.join("."),
1303
- cssVar: tokenPathToCssVar(path38),
1310
+ path: path40.join("."),
1311
+ cssVar: tokenPathToCssVar(path40),
1304
1312
  type: token.$type,
1305
1313
  value: token.$value
1306
1314
  });
@@ -1489,9 +1497,9 @@ function boundId(value) {
1489
1497
  return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
1490
1498
  }
1491
1499
  function resolveBinding(ctx, id) {
1492
- const path38 = ctx.pathById.get(id);
1493
- if (path38 === void 0) ctx.unresolved.add(id);
1494
- return path38;
1500
+ const path40 = ctx.pathById.get(id);
1501
+ if (path40 === void 0) ctx.unresolved.add(id);
1502
+ return path40;
1495
1503
  }
1496
1504
  function parseVariantProps(name) {
1497
1505
  if (!name.includes("=")) return void 0;
@@ -1526,8 +1534,8 @@ function walk(ctx, raw) {
1526
1534
  if (!isObject(paint) || paint["visible"] === false) continue;
1527
1535
  const id = boundId(paint);
1528
1536
  if (id !== void 0) {
1529
- const path38 = resolveBinding(ctx, id);
1530
- if (path38 !== void 0) tokens.add(path38);
1537
+ const path40 = resolveBinding(ctx, id);
1538
+ if (path40 !== void 0) tokens.add(path40);
1531
1539
  } else if (typeof paint["color"] === "string") {
1532
1540
  ctx.hardcoded.push({ node: name, property, value: paint["color"] });
1533
1541
  }
@@ -1535,8 +1543,8 @@ function walk(ctx, raw) {
1535
1543
  }
1536
1544
  const radiusId = boundId(raw["cornerRadius"]);
1537
1545
  if (radiusId !== void 0) {
1538
- const path38 = resolveBinding(ctx, radiusId);
1539
- if (path38 !== void 0) tokens.add(path38);
1546
+ const path40 = resolveBinding(ctx, radiusId);
1547
+ if (path40 !== void 0) tokens.add(path40);
1540
1548
  } else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
1541
1549
  ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
1542
1550
  }
@@ -1546,10 +1554,10 @@ function walk(ctx, raw) {
1546
1554
  layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
1547
1555
  const gapId = boundId(raw["itemSpacing"]);
1548
1556
  if (gapId !== void 0) {
1549
- const path38 = resolveBinding(ctx, gapId);
1550
- if (path38 !== void 0) {
1551
- layout.gap = path38;
1552
- tokens.add(path38);
1557
+ const path40 = resolveBinding(ctx, gapId);
1558
+ if (path40 !== void 0) {
1559
+ layout.gap = path40;
1560
+ tokens.add(path40);
1553
1561
  }
1554
1562
  } else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
1555
1563
  ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
@@ -1558,10 +1566,10 @@ function walk(ctx, raw) {
1558
1566
  for (const field of PADDING_FIELDS) {
1559
1567
  const id = boundId(raw[field]);
1560
1568
  if (id !== void 0) {
1561
- const path38 = resolveBinding(ctx, id);
1562
- if (path38 !== void 0) {
1563
- paddingPaths.push(path38);
1564
- tokens.add(path38);
1569
+ const path40 = resolveBinding(ctx, id);
1570
+ if (path40 !== void 0) {
1571
+ paddingPaths.push(path40);
1572
+ tokens.add(path40);
1565
1573
  }
1566
1574
  } else if (typeof raw[field] === "number" && raw[field] !== 0) {
1567
1575
  ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
@@ -1768,20 +1776,72 @@ var init_src3 = __esm({
1768
1776
  }
1769
1777
  });
1770
1778
 
1779
+ // packages/cli/src/invocation.ts
1780
+ import { existsSync as existsSync3, realpathSync } from "node:fs";
1781
+ import path3 from "node:path";
1782
+ import { fileURLToPath } from "node:url";
1783
+ function findPathTendril(pathEnv, platform) {
1784
+ const dirs = pathEnv.split(path3.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
1785
+ const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
1786
+ for (const dir of dirs) {
1787
+ for (const name of names) {
1788
+ const candidate = path3.join(dir, name);
1789
+ if (existsSync3(candidate)) return candidate;
1790
+ }
1791
+ }
1792
+ return null;
1793
+ }
1794
+ function packageRootOf(file) {
1795
+ let dir = path3.dirname(file);
1796
+ for (; ; ) {
1797
+ if (existsSync3(path3.join(dir, "package.json"))) return dir;
1798
+ const parent = path3.dirname(dir);
1799
+ if (parent === dir) return null;
1800
+ dir = parent;
1801
+ }
1802
+ }
1803
+ function resolveInvocation(pathEnv, platform, selfFile = fileURLToPath(import.meta.url)) {
1804
+ const binary = findPathTendril(pathEnv, platform);
1805
+ if (binary === null) return NPX_INVOCATION;
1806
+ try {
1807
+ const onPath = packageRootOf(realpathSync(binary));
1808
+ const self = packageRootOf(realpathSync(selfFile));
1809
+ if (onPath === null || self === null) return NPX_INVOCATION;
1810
+ return realpathSync(onPath) === realpathSync(self) ? "tendril" : NPX_INVOCATION;
1811
+ } catch {
1812
+ return NPX_INVOCATION;
1813
+ }
1814
+ }
1815
+ function tendrilInvocation() {
1816
+ cached ??= resolveInvocation(process.env["PATH"] ?? "", process.platform);
1817
+ return cached;
1818
+ }
1819
+ function tendrilCommand(args) {
1820
+ return `${tendrilInvocation()} ${args}`;
1821
+ }
1822
+ var NPX_INVOCATION, cached;
1823
+ var init_invocation = __esm({
1824
+ "packages/cli/src/invocation.ts"() {
1825
+ "use strict";
1826
+ NPX_INVOCATION = "npx -y -p @tendrilapp/cli@latest tendril";
1827
+ cached = null;
1828
+ }
1829
+ });
1830
+
1771
1831
  // packages/verify/src/browser.ts
1772
1832
  import { execFileSync } from "node:child_process";
1773
- import { existsSync as existsSync3, readdirSync } from "node:fs";
1774
- import path3 from "node:path";
1833
+ import { existsSync as existsSync4, readdirSync } from "node:fs";
1834
+ import path4 from "node:path";
1775
1835
  function resolveChrome() {
1776
1836
  const fromEnv = process.env["TENDRIL_CHROME"] ?? process.env["CHROME_PATH"];
1777
1837
  if (fromEnv !== void 0 && fromEnv !== "") {
1778
- if (!existsSync3(fromEnv)) {
1838
+ if (!existsSync4(fromEnv)) {
1779
1839
  throw new Error(`CHROME_PATH points at ${fromEnv}, which does not exist \u2014 fix the variable or unset it to use discovery`);
1780
1840
  }
1781
1841
  return fromEnv;
1782
1842
  }
1783
1843
  const candidates = process.platform === "darwin" ? MAC_CANDIDATES : process.platform === "win32" ? WIN_CANDIDATES : [];
1784
- for (const c of candidates) if (existsSync3(c)) return c;
1844
+ for (const c of candidates) if (existsSync4(c)) return c;
1785
1845
  if (process.platform !== "win32") {
1786
1846
  for (const name of PATH_NAMES) {
1787
1847
  try {
@@ -1795,7 +1855,7 @@ function resolveChrome() {
1795
1855
  }
1796
1856
  function versionFromInstallDir(exePath) {
1797
1857
  try {
1798
- const builds = readdirSync(path3.dirname(exePath), { withFileTypes: true }).filter((e) => e.isDirectory() && /^\d+(\.\d+){3}$/.test(e.name)).map((e) => e.name.split(".").map(Number));
1858
+ const builds = readdirSync(path4.dirname(exePath), { withFileTypes: true }).filter((e) => e.isDirectory() && /^\d+(\.\d+){3}$/.test(e.name)).map((e) => e.name.split(".").map(Number));
1799
1859
  builds.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3]);
1800
1860
  return builds.length === 0 ? null : builds[builds.length - 1].join(".");
1801
1861
  } catch {
@@ -1807,7 +1867,7 @@ function windowsBrowserName(exePath) {
1807
1867
  if (p.includes("\\google\\chrome\\")) return "Google Chrome";
1808
1868
  if (p.includes("\\microsoft\\edge\\")) return "Microsoft Edge";
1809
1869
  if (p.includes("chromium")) return "Chromium";
1810
- return path3.win32.basename(exePath, ".exe");
1870
+ return path4.win32.basename(exePath, ".exe");
1811
1871
  }
1812
1872
  function chromeVersion() {
1813
1873
  if (_version !== void 0) return _version;
@@ -1842,32 +1902,32 @@ var init_browser = __esm({
1842
1902
  });
1843
1903
 
1844
1904
  // packages/verify/src/runtime.ts
1845
- import { existsSync as existsSync4, mkdtempSync, symlinkSync } from "node:fs";
1905
+ import { existsSync as existsSync5, mkdtempSync, symlinkSync } from "node:fs";
1846
1906
  import { createRequire } from "node:module";
1847
1907
  import os from "node:os";
1848
- import path4 from "node:path";
1849
- import { fileURLToPath } from "node:url";
1908
+ import path5 from "node:path";
1909
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1850
1910
  function runtimePackageRoot() {
1851
1911
  const env = process.env["TENDRIL_PACKAGE_ROOT"];
1852
- if (env !== void 0 && env !== "") return path4.resolve(env);
1853
- return path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "..");
1912
+ if (env !== void 0 && env !== "") return path5.resolve(env);
1913
+ return path5.resolve(path5.dirname(fileURLToPath2(import.meta.url)), "..");
1854
1914
  }
1855
1915
  function runtimeNodeModules() {
1856
1916
  const root = runtimePackageRoot();
1857
- for (let dir = root; ; dir = path4.dirname(dir)) {
1858
- const candidate = path4.basename(dir) === "node_modules" ? dir : path4.join(dir, "node_modules");
1859
- if (existsSync4(path4.join(candidate, "react"))) return candidate;
1860
- if (path4.dirname(dir) === dir) break;
1917
+ for (let dir = root; ; dir = path5.dirname(dir)) {
1918
+ const candidate = path5.basename(dir) === "node_modules" ? dir : path5.join(dir, "node_modules");
1919
+ if (existsSync5(path5.join(candidate, "react"))) return candidate;
1920
+ if (path5.dirname(dir) === dir) break;
1861
1921
  }
1862
1922
  throw new Error(
1863
1923
  `Tendril's installed dependencies are missing: no node_modules containing react found at or above ${root}. This is an INSTALLATION problem, not a component error \u2014 reinstall with \`npm install -g @tendrilapp/cli\` (or \`tendrilapp\`) and retry.`
1864
1924
  );
1865
1925
  }
1866
1926
  function newScratchDir(prefix) {
1867
- const dir = mkdtempSync(path4.join(os.tmpdir(), `tendril-${prefix}-`));
1927
+ const dir = mkdtempSync(path5.join(os.tmpdir(), `tendril-${prefix}-`));
1868
1928
  const nodeModules = runtimeNodeModules();
1869
1929
  try {
1870
- symlinkSync(nodeModules, path4.join(dir, "node_modules"), "junction");
1930
+ symlinkSync(nodeModules, path5.join(dir, "node_modules"), "junction");
1871
1931
  } catch (err) {
1872
1932
  throw new Error(
1873
1933
  `Tendril could not link its dependencies into the scratch dir (${nodeModules} -> ${dir}): ${err instanceof Error ? err.message : String(err)}. This is an environment problem, not a component error.`
@@ -1876,7 +1936,7 @@ function newScratchDir(prefix) {
1876
1936
  return dir;
1877
1937
  }
1878
1938
  function reactPinPlugin() {
1879
- const req = createRequire(path4.join(runtimePackageRoot(), "package.json"));
1939
+ const req = createRequire(path5.join(runtimePackageRoot(), "package.json"));
1880
1940
  return {
1881
1941
  name: "tendril-react-pin",
1882
1942
  setup(b) {
@@ -1929,9 +1989,9 @@ var init_gates = __esm({
1929
1989
 
1930
1990
  // packages/verify/src/tsc-check.ts
1931
1991
  import ts from "typescript";
1932
- import path5 from "node:path";
1992
+ import path6 from "node:path";
1933
1993
  function runTscStrict(files) {
1934
- const program = ts.createProgram(files.map((f) => path5.resolve(f)), STRICT_OPTIONS);
1994
+ const program = ts.createProgram(files.map((f) => path6.resolve(f)), STRICT_OPTIONS);
1935
1995
  const diagnostics = ts.getPreEmitDiagnostics(program);
1936
1996
  const mapped = diagnostics.map((d) => {
1937
1997
  const file = d.file?.fileName;
@@ -2090,7 +2150,7 @@ var init_token_lint = __esm({
2090
2150
 
2091
2151
  // packages/verify/src/loop.ts
2092
2152
  import { rmSync, writeFileSync as writeFileSync2 } from "node:fs";
2093
- import path6 from "node:path";
2153
+ import path7 from "node:path";
2094
2154
  async function runChecks(componentName, files, extraFiles, definedVars2) {
2095
2155
  const findings = [];
2096
2156
  for (const violation of scanForbiddenPatterns(files.tsx)) {
@@ -2105,10 +2165,10 @@ async function runChecks(componentName, files, extraFiles, definedVars2) {
2105
2165
  }
2106
2166
  const workDir = newScratchDir("loop");
2107
2167
  try {
2108
- const tsxPath = path6.join(workDir, `${componentName}.tsx`);
2168
+ const tsxPath = path7.join(workDir, `${componentName}.tsx`);
2109
2169
  writeFileSync2(tsxPath, files.tsx);
2110
2170
  for (const [name, content] of Object.entries(extraFiles ?? {})) {
2111
- writeFileSync2(path6.join(workDir, name), content);
2171
+ writeFileSync2(path7.join(workDir, name), content);
2112
2172
  }
2113
2173
  const tsc = runTscStrict([tsxPath]);
2114
2174
  for (const d of tsc.diagnostics) {
@@ -2275,11 +2335,11 @@ var init_report = __esm({
2275
2335
  });
2276
2336
 
2277
2337
  // packages/verify/src/mount-limits.ts
2278
- function lcdTextDisabled() {
2279
- return process.env["TENDRIL_DISABLE_LCD_TEXT"] === "1";
2338
+ function lcdTextEnabled() {
2339
+ return process.env["TENDRIL_ENABLE_LCD_TEXT"] === "1";
2280
2340
  }
2281
2341
  function mountArgs() {
2282
- return [MEMORY_CAP_ARG, ...lcdTextDisabled() ? ["--disable-lcd-text"] : []];
2342
+ return [MEMORY_CAP_ARG, ...lcdTextEnabled() ? [] : ["--disable-lcd-text"]];
2283
2343
  }
2284
2344
  function raceMountDeadline(work, deadlineMs, onDeadline) {
2285
2345
  return Promise.race([
@@ -2750,8 +2810,8 @@ var init_image_diff = __esm({
2750
2810
  });
2751
2811
 
2752
2812
  // packages/verify/src/visual-facts.ts
2753
- import path7 from "node:path";
2754
- import { fileURLToPath as fileURLToPath2 } from "node:url";
2813
+ import path8 from "node:path";
2814
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2755
2815
  import { build } from "esbuild";
2756
2816
  import { chromium } from "playwright-core";
2757
2817
  function resolveAxisValue(declared, props, axis, value) {
@@ -3295,7 +3355,7 @@ var init_visual_facts = __esm({
3295
3355
  init_browser();
3296
3356
  init_mount_limits();
3297
3357
  init_image_diff();
3298
- RESOLVE_DIR = path7.resolve(path7.dirname(fileURLToPath2(import.meta.url)), "..");
3358
+ RESOLVE_DIR = path8.resolve(path8.dirname(fileURLToPath3(import.meta.url)), "..");
3299
3359
  TOLERANCE_PX = 2;
3300
3360
  WIDTH_SLACK = 0.25;
3301
3361
  IMAGE_SIMILARITY_FLOOR = 0.8;
@@ -3304,26 +3364,41 @@ var init_visual_facts = __esm({
3304
3364
  });
3305
3365
 
3306
3366
  // packages/verify/src/paths.ts
3307
- import path8 from "node:path";
3308
- import { fileURLToPath as fileURLToPath3 } from "node:url";
3367
+ import path9 from "node:path";
3368
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
3309
3369
  var VERIFY_PKG_DIR, REPO_ROOT;
3310
3370
  var init_paths = __esm({
3311
3371
  "packages/verify/src/paths.ts"() {
3312
3372
  "use strict";
3313
- VERIFY_PKG_DIR = path8.resolve(path8.dirname(fileURLToPath3(import.meta.url)), "..");
3314
- REPO_ROOT = path8.resolve(VERIFY_PKG_DIR, "..", "..");
3373
+ VERIFY_PKG_DIR = path9.resolve(path9.dirname(fileURLToPath4(import.meta.url)), "..");
3374
+ REPO_ROOT = path9.resolve(VERIFY_PKG_DIR, "..", "..");
3315
3375
  }
3316
3376
  });
3317
3377
 
3318
3378
  // packages/verify/src/font-resolve.ts
3319
3379
  import { createHash } from "node:crypto";
3320
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
3380
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
3321
3381
  import os2 from "node:os";
3322
- import path9 from "node:path";
3382
+ import path10 from "node:path";
3323
3383
  function fontCacheDir() {
3324
3384
  const env = process.env["TENDRIL_FONT_CACHE"];
3325
- if (env !== void 0 && env !== "") return path9.resolve(env);
3326
- return path9.join(os2.homedir(), ".tendril", "fonts");
3385
+ if (env !== void 0 && env !== "") return path10.resolve(env);
3386
+ return path10.join(os2.homedir(), ".tendril", "fonts");
3387
+ }
3388
+ function normalizeFontLicense(value) {
3389
+ return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
3390
+ }
3391
+ async function googleFontMetadata(queryName) {
3392
+ try {
3393
+ const res = await fetch(`https://fonts.google.com/metadata/fonts/${encodeURIComponent(queryName)}`, { headers: { "User-Agent": UA } });
3394
+ if (!res.ok) return { license: "unknown", family: null };
3395
+ const parsed = JSON.parse((await res.text()).replace(/^\)\]\}'\s*/, ""));
3396
+ const family = typeof parsed.family === "string" && parsed.family !== "" ? parsed.family : null;
3397
+ if (parsed.isOpenSource !== true || typeof parsed.license !== "string") return { license: "unknown", family };
3398
+ return { license: GOOGLE_LICENSE_IDS.get(parsed.license.toLowerCase()) ?? "unknown", family };
3399
+ } catch {
3400
+ return { license: "unknown", family: null };
3401
+ }
3327
3402
  }
3328
3403
  function googleQueryCandidates(family) {
3329
3404
  const stripped = family.replace(/\s+(Variable|VF)$/i, "");
@@ -3335,12 +3410,14 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
3335
3410
  const failures = [];
3336
3411
  let css = null;
3337
3412
  let lastStatus = 0;
3413
+ let servedName = family;
3338
3414
  try {
3339
3415
  for (const queryName of googleQueryCandidates(family)) {
3340
3416
  const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(queryName).replace(/%20/g, "+")}:wght@${weights.join(";")}&display=swap`;
3341
3417
  const res = await fetch(cssUrl, { headers: { "User-Agent": UA } });
3342
3418
  if (res.ok) {
3343
3419
  css = await res.text();
3420
+ servedName = queryName;
3344
3421
  break;
3345
3422
  }
3346
3423
  lastStatus = res.status;
@@ -3354,6 +3431,7 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
3354
3431
  } catch (err) {
3355
3432
  return { resolved, failures: weights.map((weight) => ({ family, weight, reason: `offline or fetch failed: ${err instanceof Error ? err.message : String(err)}` })) };
3356
3433
  }
3434
+ const meta = await googleFontMetadata(servedName);
3357
3435
  for (const weight of weights) {
3358
3436
  const blocks = [...css.matchAll(/@font-face\s*{[^}]*}/g)].map((m) => m[0]);
3359
3437
  const block = blocks.find((b) => b.includes(`font-weight: ${weight}`) && /unicode-range:[^;]*U\+0000/.test(b));
@@ -3370,25 +3448,25 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
3370
3448
  }
3371
3449
  const bytes = new Uint8Array(await fileRes.arrayBuffer());
3372
3450
  const sha256 = createHash("sha256").update(bytes).digest("hex");
3373
- const file = path9.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
3451
+ const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
3374
3452
  writeFileSync3(file, bytes);
3375
- resolved.push({ family, weight, source: url, sha256, file });
3453
+ resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
3376
3454
  } catch (err) {
3377
3455
  failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
3378
3456
  }
3379
3457
  }
3380
- const mPath = path9.join(cacheDir, "manifest.json");
3381
- const prior = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3382
- const portable2 = resolved.map((m) => ({ ...m, file: path9.basename(m.file) }));
3458
+ const mPath = path10.join(cacheDir, "manifest.json");
3459
+ const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3460
+ const portable2 = resolved.map((m) => ({ ...m, file: path10.basename(m.file) }));
3383
3461
  const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
3384
3462
  if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
3385
3463
  `);
3386
3464
  return { resolved, failures };
3387
3465
  }
3388
3466
  function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
3389
- const src = path9.resolve(filePath);
3390
- if (!existsSync5(src)) throw new Error(`font file not found: ${src}`);
3391
- const ext = path9.extname(src).toLowerCase();
3467
+ const src = path10.resolve(filePath);
3468
+ if (!existsSync6(src)) throw new Error(`font file not found: ${src}`);
3469
+ const ext = path10.extname(src).toLowerCase();
3392
3470
  if (![".woff2", ".woff", ".ttf", ".otf"].includes(ext)) {
3393
3471
  throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf or .otf`);
3394
3472
  }
@@ -3396,20 +3474,20 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
3396
3474
  if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
3397
3475
  mkdirSync2(cacheDir, { recursive: true });
3398
3476
  const sha256 = createHash("sha256").update(bytes).digest("hex");
3399
- const file = path9.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${ext}`);
3477
+ const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${ext}`);
3400
3478
  writeFileSync3(file, bytes);
3401
- const face = { family, weight, source: `local:${path9.basename(src)}`, sha256, file };
3402
- const mPath = path9.join(cacheDir, "manifest.json");
3403
- const prior = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3404
- const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path9.basename(file) }];
3479
+ const face = { family, weight, source: `local:${path10.basename(src)}`, sha256, file, license: "unknown" };
3480
+ const mPath = path10.join(cacheDir, "manifest.json");
3481
+ const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3482
+ const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path10.basename(file) }];
3405
3483
  writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
3406
3484
  `);
3407
3485
  return face;
3408
3486
  }
3409
3487
  function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
3410
3488
  const lock = JSON.parse(readFileSync3(lockPath, "utf8"));
3411
- const mPath = path9.join(cacheDir, "manifest.json");
3412
- const manifest = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3489
+ const mPath = path10.join(cacheDir, "manifest.json");
3490
+ const manifest = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3413
3491
  return lock.map((l) => {
3414
3492
  const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
3415
3493
  if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
@@ -3417,8 +3495,8 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
3417
3495
  });
3418
3496
  }
3419
3497
  function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3420
- const mPath = path9.join(cacheDir, "manifest.json");
3421
- if (!existsSync5(mPath)) return [];
3498
+ const mPath = path10.join(cacheDir, "manifest.json");
3499
+ if (!existsSync6(mPath)) return [];
3422
3500
  let entries;
3423
3501
  try {
3424
3502
  entries = JSON.parse(readFileSync3(mPath, "utf8"));
@@ -3435,14 +3513,35 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3435
3513
  return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
3436
3514
  }
3437
3515
  function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
3438
- const mPath = path9.join(cacheDir, "manifest.json");
3439
- const manifest = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3516
+ const mPath = path10.join(cacheDir, "manifest.json");
3517
+ const manifest = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
3440
3518
  const wanted = new Set(families.map((f) => f.toLowerCase()));
3441
- return manifest.filter((f) => wanted.has(f.family.toLowerCase()));
3519
+ return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
3520
+ family: f.family,
3521
+ weight: f.weight,
3522
+ source: f.source,
3523
+ sha256: f.sha256,
3524
+ file: f.file,
3525
+ license: normalizeFontLicense(f.license),
3526
+ ...typeof f.servedFamily === "string" && f.servedFamily !== "" ? { servedFamily: f.servedFamily } : {}
3527
+ }));
3528
+ }
3529
+ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
3530
+ const mPath = path10.join(cacheDir, "manifest.json");
3531
+ if (!existsSync6(mPath)) return [];
3532
+ let entries;
3533
+ try {
3534
+ entries = JSON.parse(readFileSync3(mPath, "utf8"));
3535
+ } catch {
3536
+ return [];
3537
+ }
3538
+ return entries.filter(
3539
+ (e) => typeof e.family === "string" && typeof e.weight === "number" && typeof e.source === "string" && e.source.startsWith(GOOGLE_FONT_FILE_PREFIX) && !(typeof e.license === "string" && FONT_LICENSES.includes(e.license))
3540
+ ).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
3442
3541
  }
3443
3542
  function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3444
- const mPath = path9.join(cacheDir, "manifest.json");
3445
- if (!existsSync5(mPath)) return [];
3543
+ const mPath = path10.join(cacheDir, "manifest.json");
3544
+ if (!existsSync6(mPath)) return [];
3446
3545
  let entries;
3447
3546
  try {
3448
3547
  entries = JSON.parse(readFileSync3(mPath, "utf8"));
@@ -3452,8 +3551,8 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3452
3551
  const byFamily = /* @__PURE__ */ new Map();
3453
3552
  for (const e of entries) {
3454
3553
  if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
3455
- const file = path9.isAbsolute(e.file) && existsSync5(e.file) ? e.file : path9.resolve(cacheDir, path9.basename(e.file));
3456
- if (!existsSync5(file)) continue;
3554
+ const file = path10.isAbsolute(e.file) && existsSync6(e.file) ? e.file : path10.resolve(cacheDir, path10.basename(e.file));
3555
+ if (!existsSync6(file)) continue;
3457
3556
  if (createHash("sha256").update(readFileSync3(file)).digest("hex") !== e.sha256) continue;
3458
3557
  const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
3459
3558
  set.add(e.weight);
@@ -3461,67 +3560,86 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
3461
3560
  }
3462
3561
  return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
3463
3562
  }
3464
- var DEFAULT_FONT_CACHE, UA;
3563
+ var DEFAULT_FONT_CACHE, UA, FONT_LICENSES, GOOGLE_LICENSE_IDS, GOOGLE_FONT_FILE_PREFIX;
3465
3564
  var init_font_resolve = __esm({
3466
3565
  "packages/verify/src/font-resolve.ts"() {
3467
3566
  "use strict";
3468
3567
  DEFAULT_FONT_CACHE = fontCacheDir();
3469
3568
  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";
3569
+ FONT_LICENSES = ["OFL-1.1", "Apache-2.0", "UFL-1.0", "proprietary", "unknown"];
3570
+ GOOGLE_LICENSE_IDS = /* @__PURE__ */ new Map([
3571
+ ["ofl", "OFL-1.1"],
3572
+ ["apache2", "Apache-2.0"],
3573
+ ["apache", "Apache-2.0"],
3574
+ ["ufl", "UFL-1.0"]
3575
+ ]);
3576
+ GOOGLE_FONT_FILE_PREFIX = "https://fonts.gstatic.com/";
3470
3577
  }
3471
3578
  });
3472
3579
 
3473
3580
  // packages/verify/src/font-faces.ts
3474
3581
  import { createHash as createHash2 } from "node:crypto";
3475
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
3476
- import path10 from "node:path";
3477
- function fontFaceCss(manifestPath2 = path10.join(fontCacheDir(), "manifest.json")) {
3478
- if (!existsSync6(manifestPath2)) return "";
3582
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
3583
+ import path11 from "node:path";
3584
+ function injectedGroups(manifestPath2) {
3585
+ if (!existsSync7(manifestPath2)) return { groups: [], shared: false };
3479
3586
  const claimed = JSON.parse(readFileSync4(manifestPath2, "utf8"));
3480
- const resolveEntry = (f) => {
3481
- if (path10.isAbsolute(f) && existsSync6(f)) return f;
3482
- return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
3483
- };
3484
- const manifest = claimed.filter((f) => {
3485
- const file = resolveEntry(f.file);
3486
- if (!existsSync6(file)) return false;
3487
- return createHash2("sha256").update(readFileSync4(file)).digest("hex") === f.sha256;
3488
- });
3489
- const resolveFile = (f) => {
3490
- if (path10.isAbsolute(f) && existsSync6(f)) return f;
3491
- return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
3492
- };
3587
+ const resolveFile = (f) => path11.isAbsolute(f) && existsSync7(f) ? f : path11.resolve(path11.dirname(manifestPath2), path11.basename(f));
3493
3588
  const byFile = /* @__PURE__ */ new Map();
3494
- for (const f of manifest) {
3589
+ for (const f of claimed) {
3590
+ const file = resolveFile(f.file);
3591
+ if (!existsSync7(file)) continue;
3592
+ if (createHash2("sha256").update(readFileSync4(file)).digest("hex") !== f.sha256) continue;
3495
3593
  const k = `${f.family}:${f.file}`;
3496
- const e = byFile.get(k) ?? { family: f.family, weights: [], file: resolveFile(f.file) };
3594
+ const e = byFile.get(k) ?? { family: f.family, weights: [], file };
3497
3595
  e.weights.push(f.weight);
3498
3596
  byFile.set(k, e);
3499
3597
  }
3500
- const files = new Set([...byFile.values()].map((e) => e.file));
3501
- const shared = files.size < byFile.size;
3502
- return [...byFile.values()].map((e) => {
3503
- const weight = shared || e.weights.length > 1 ? "100 700" : String(e.weights[0]);
3598
+ const groups = [...byFile.values()];
3599
+ return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
3600
+ }
3601
+ function fontFaceCss(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
3602
+ const { groups, shared } = injectedGroups(manifestPath2);
3603
+ return groups.map((e) => {
3604
+ const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
3504
3605
  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'); }`;
3505
3606
  }).join("\n");
3506
3607
  }
3608
+ function injectedFamilyWeights(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
3609
+ const { groups, shared } = injectedGroups(manifestPath2);
3610
+ const out = /* @__PURE__ */ new Map();
3611
+ for (const g of groups) {
3612
+ const key = g.family.toLowerCase();
3613
+ const e = out.get(key) ?? { weights: /* @__PURE__ */ new Set(), span: false };
3614
+ for (const w of g.weights) e.weights.add(w);
3615
+ if (shared || g.weights.length > 1) e.span = true;
3616
+ out.set(key, e);
3617
+ }
3618
+ return out;
3619
+ }
3620
+ function servesWeight(coverage, weight) {
3621
+ return coverage.weights.has(weight) || coverage.span && weight >= SPAN[0] && weight <= SPAN[1];
3622
+ }
3623
+ var SPAN;
3507
3624
  var init_font_faces = __esm({
3508
3625
  "packages/verify/src/font-faces.ts"() {
3509
3626
  "use strict";
3510
3627
  init_font_resolve();
3628
+ SPAN = [100, 700];
3511
3629
  }
3512
3630
  });
3513
3631
 
3514
3632
  // packages/verify/src/admission.ts
3515
- import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync7, writeFileSync as writeFileSync4 } from "node:fs";
3516
- import path11 from "node:path";
3633
+ import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync8, writeFileSync as writeFileSync4 } from "node:fs";
3634
+ import path12 from "node:path";
3517
3635
  import { build as build2 } from "esbuild";
3518
3636
  import postcss from "postcss";
3519
3637
  import tailwindcss from "tailwindcss";
3520
3638
  import { chromium as chromium2 } from "playwright-core";
3521
3639
  function fontWeightsByFamily() {
3522
- const mPath = path11.join(fontCacheDir(), "manifest.json");
3640
+ const mPath = path12.join(fontCacheDir(), "manifest.json");
3523
3641
  const out = /* @__PURE__ */ new Map();
3524
- if (!existsSync7(mPath)) return out;
3642
+ if (!existsSync8(mPath)) return out;
3525
3643
  for (const f of JSON.parse(readFileSync5(mPath, "utf8")))
3526
3644
  out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
3527
3645
  return out;
@@ -3542,7 +3660,7 @@ var init_admission = __esm({
3542
3660
  });
3543
3661
 
3544
3662
  // packages/verify/src/tasks.ts
3545
- import path12 from "node:path";
3663
+ import path13 from "node:path";
3546
3664
  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;
3547
3665
  var init_tasks = __esm({
3548
3666
  "packages/verify/src/tasks.ts"() {
@@ -3702,7 +3820,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
3702
3820
  ];
3703
3821
  TASKS = {
3704
3822
  calendar: {
3705
- set: path12.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
3823
+ set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
3706
3824
  entry: "Calendar.tsx",
3707
3825
  configs: CALENDAR_CONFIGS,
3708
3826
  systemApi: CALENDAR_API,
@@ -3710,7 +3828,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
3710
3828
  prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
3711
3829
  },
3712
3830
  "shadcn-button": {
3713
- set: path12.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
3831
+ set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
3714
3832
  entry: "Button.tsx",
3715
3833
  configs: BUTTON_CONFIGS,
3716
3834
  systemApi: BUTTON_API,
@@ -3718,7 +3836,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
3718
3836
  prelude: { controls: ["> *"], textInputs: [] }
3719
3837
  },
3720
3838
  combobox: {
3721
- set: path12.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
3839
+ set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
3722
3840
  entry: "ComboBox.tsx",
3723
3841
  configs: COMBO_CONFIGS,
3724
3842
  systemApi: COMBO_API,
@@ -3726,7 +3844,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
3726
3844
  prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
3727
3845
  },
3728
3846
  modal: {
3729
- set: path12.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
3847
+ set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
3730
3848
  entry: "Modal.tsx",
3731
3849
  configs: MODAL_CONFIGS,
3732
3850
  systemApi: MODAL_API,
@@ -3744,8 +3862,8 @@ __export(behavior_exports, {
3744
3862
  compileMount: () => compileMount,
3745
3863
  recordingIsDark: () => recordingIsDark
3746
3864
  });
3747
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
3748
- import path13 from "node:path";
3865
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
3866
+ import path14 from "node:path";
3749
3867
  import { build as build3 } from "esbuild";
3750
3868
  import { chromium as chromium3 } from "playwright-core";
3751
3869
  import { PNG as PNG2 } from "pngjs";
@@ -3754,12 +3872,12 @@ function getFontFaces() {
3754
3872
  return _fontFaces;
3755
3873
  }
3756
3874
  async function compileMount(task, bundleDir) {
3757
- const entryTsx = path13.join(bundleDir, task.entry);
3758
- if (!existsSync8(entryTsx)) return { error: `${task.entry} missing` };
3875
+ const entryTsx = path14.join(bundleDir, task.entry);
3876
+ if (!existsSync9(entryTsx)) return { error: `${task.entry} missing` };
3759
3877
  const mountSrc = `
3760
3878
  import { createElement } from "react";
3761
3879
  import { createRoot } from "react-dom/client";
3762
- import * as B from ${JSON.stringify(path13.resolve(entryTsx))};
3880
+ import * as B from ${JSON.stringify(path14.resolve(entryTsx))};
3763
3881
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
3764
3882
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
3765
3883
  // Callbacks cannot ride the JSON config: specs NAME spy props and the
@@ -4011,8 +4129,8 @@ async function runSteps(page, spec, renderPose) {
4011
4129
  function recordingIsDark(task) {
4012
4130
  const rep = task.configs[0]?.rep;
4013
4131
  if (rep === void 0) return false;
4014
- const f = path13.join(task.set, rep, "get_screenshot.json");
4015
- if (!existsSync8(f)) return false;
4132
+ const f = path14.join(task.set, rep, "get_screenshot.json");
4133
+ if (!existsSync9(f)) return false;
4016
4134
  try {
4017
4135
  const env = JSON.parse(readFileSync6(f, "utf8")).content.find((c) => c.type === "image");
4018
4136
  if (env?.data === void 0) return false;
@@ -4086,7 +4204,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
4086
4204
  const deadlineMs = timeoutMs + 1e4;
4087
4205
  const js = await compileMount(task, bundleDir);
4088
4206
  if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
4089
- const css = ["tokens.css", "styles.css"].map((f) => path13.join(bundleDir, f)).filter((f) => existsSync8(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
4207
+ const css = ["tokens.css", "styles.css"].map((f) => path14.join(bundleDir, f)).filter((f) => existsSync9(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
4090
4208
  const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
4091
4209
  const browser = await chromium3.connect(server.wsEndpoint());
4092
4210
  const results = [];
@@ -4229,8 +4347,8 @@ var init_behavior = __esm({
4229
4347
  });
4230
4348
 
4231
4349
  // packages/verify/src/bundle-quality.ts
4232
- import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "node:fs";
4233
- import path14 from "node:path";
4350
+ import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "node:fs";
4351
+ import path15 from "node:path";
4234
4352
  function definedVars(tokensCss) {
4235
4353
  if (tokensCss === void 0) return void 0;
4236
4354
  const names = /* @__PURE__ */ new Set();
@@ -4239,7 +4357,7 @@ function definedVars(tokensCss) {
4239
4357
  }
4240
4358
  function recordedTokenMapEmpty(setDir, reps) {
4241
4359
  const readMap = (file) => {
4242
- if (!existsSync9(file)) return void 0;
4360
+ if (!existsSync10(file)) return void 0;
4243
4361
  try {
4244
4362
  const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync7(file, "utf8"))) || "{}");
4245
4363
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -4247,28 +4365,93 @@ function recordedTokenMapEmpty(setDir, reps) {
4247
4365
  return {};
4248
4366
  }
4249
4367
  };
4250
- const setLevel = readMap(path14.join(setDir, "get_variable_defs.json"));
4368
+ const setLevel = readMap(path15.join(setDir, "get_variable_defs.json"));
4251
4369
  if (setLevel !== void 0) return Object.keys(setLevel).length === 0;
4252
4370
  let recorded = false;
4253
4371
  for (const rep of reps) {
4254
- const m = readMap(path14.join(setDir, rep, "get_variable_defs.json"));
4372
+ const m = readMap(path15.join(setDir, rep, "get_variable_defs.json"));
4255
4373
  if (m === void 0) continue;
4256
4374
  recorded = true;
4257
4375
  if (Object.keys(m).length > 0) return false;
4258
4376
  }
4259
4377
  return recorded;
4260
4378
  }
4261
- async function checkBundleQuality(bundleDir, entry, set) {
4379
+ function scannable(css) {
4380
+ const blank = (m) => m.replace(/[^\n]/g, " ");
4381
+ return css.replace(/\/\*[^]*?\*\//g, blank).replace(/@font-face\s*{[^}]*}/g, blank);
4382
+ }
4383
+ function usedWeights(css) {
4384
+ const out = /* @__PURE__ */ new Set();
4385
+ for (const m of css.matchAll(/font-weight\s*:\s*([^;}]+)/g)) {
4386
+ const v = declValue(m[1]).toLowerCase();
4387
+ if (v === "bold") out.add(700);
4388
+ else if (v === "normal") out.add(400);
4389
+ else if (/^\d{1,4}$/.test(v)) out.add(Number(v));
4390
+ }
4391
+ return [...out].sort((a, b) => a - b);
4392
+ }
4393
+ function fontStacks(css) {
4394
+ const out = [];
4395
+ for (const m of css.matchAll(/font-family\s*:\s*([^;}]+)/g)) {
4396
+ const text = declValue(m[1]);
4397
+ const families = text.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, "")).filter((f) => f !== "");
4398
+ if (families.length > 0) out.push({ families, line: css.slice(0, m.index).split("\n").length, text });
4399
+ }
4400
+ return out;
4401
+ }
4402
+ function fontStackFindings(sheets, coverage) {
4403
+ if (coverage.size === 0) return [];
4404
+ const scanned = sheets.map((s) => ({ file: s.file, css: scannable(s.css) }));
4405
+ const weights = [...new Set(scanned.flatMap((s) => usedWeights(s.css)))].sort((a, b) => a - b);
4406
+ if (weights.length === 0) return [];
4407
+ const findings = [];
4408
+ const seen = /* @__PURE__ */ new Set();
4409
+ for (const sheet of scanned) {
4410
+ for (const stack of fontStacks(sheet.css)) {
4411
+ if (seen.has(stack.text)) continue;
4412
+ seen.add(stack.text);
4413
+ const family = stack.families[0];
4414
+ const bound = coverage.get(family.toLowerCase());
4415
+ if (bound === void 0) continue;
4416
+ const unserved = weights.filter((w) => !servesWeight(bound, w));
4417
+ if (unserved.length === 0) continue;
4418
+ const rescue = stack.families.slice(1).find((f) => {
4419
+ const c = coverage.get(f.toLowerCase());
4420
+ return c !== void 0 && unserved.every((w) => servesWeight(c, w));
4421
+ });
4422
+ const has = bound.span ? "100\u2013700" : [...bound.weights].sort((a, b) => a - b).join(", ");
4423
+ const list = unserved.join(", ");
4424
+ const renders = !bound.span && bound.weights.size === 1 ? `renders in the ${[...bound.weights][0]} face` : "renders in whichever of those faces is nearest";
4425
+ findings.push({
4426
+ kind: "font-stack",
4427
+ file: sheet.file,
4428
+ line: stack.line,
4429
+ message: `font-family ${stack.text} binds to '${family}', which the font kit provides at ${has} \u2014 CSS matches the FAMILY first and picks a weight only inside it (a later family is never consulted for a missing weight, and the prelude's font-synthesis: none rules out faux-bold), so text at weight ${list} ${renders}, silently. ` + (rescue !== void 0 ? `'${rescue}' in this same stack provides ${list}: list it first.` : `No family in this stack provides ${list} \u2014 if the recording shows ${unserved.length === 1 ? "that weight" : "those weights"}, run \`tendril fonts resolve "${family}" --weights ${unserved.join(" ")}\` and re-score.`) + " ADVISORY: quality feedback only \u2014 it never moves a score, a tier, or the verdict."
4430
+ });
4431
+ }
4432
+ }
4433
+ return findings;
4434
+ }
4435
+ async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
4262
4436
  const findings = [];
4263
- const entryPath = path14.join(bundleDir, entry);
4264
- const cssPath = path14.join(bundleDir, "styles.css");
4265
- const tokensPath = path14.join(bundleDir, "tokens.css");
4266
- const css = existsSync9(cssPath) ? readFileSync7(cssPath, "utf8") : "";
4267
- const tokensCss = existsSync9(tokensPath) ? readFileSync7(tokensPath, "utf8") : void 0;
4268
- if (existsSync9(entryPath)) {
4437
+ const entryPath = path15.join(bundleDir, entry);
4438
+ const cssPath = path15.join(bundleDir, "styles.css");
4439
+ const tokensPath = path15.join(bundleDir, "tokens.css");
4440
+ const css = existsSync10(cssPath) ? readFileSync7(cssPath, "utf8") : "";
4441
+ const tokensCss = existsSync10(tokensPath) ? readFileSync7(tokensPath, "utf8") : void 0;
4442
+ findings.push(
4443
+ ...fontStackFindings(
4444
+ [
4445
+ { file: "styles.css", css },
4446
+ ...tokensCss === void 0 ? [] : [{ file: "tokens.css", css: tokensCss }]
4447
+ ],
4448
+ injectedFamilyWeights(fontManifest)
4449
+ )
4450
+ );
4451
+ if (existsSync10(entryPath)) {
4269
4452
  const workDir = newScratchDir("quality");
4270
4453
  try {
4271
- const tsxPath = path14.join(workDir, entry);
4454
+ const tsxPath = path15.join(workDir, entry);
4272
4455
  writeFileSync5(tsxPath, readFileSync7(entryPath, "utf8"));
4273
4456
  for (const d of runTscStrict([tsxPath]).diagnostics) {
4274
4457
  findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
@@ -4285,13 +4468,16 @@ async function checkBundleQuality(bundleDir, entry, set) {
4285
4468
  }
4286
4469
  return { findings, tokensAbsent: tokensCss === void 0 && !/var\(\s*--/.test(css) };
4287
4470
  }
4471
+ var declValue;
4288
4472
  var init_bundle_quality = __esm({
4289
4473
  "packages/verify/src/bundle-quality.ts"() {
4290
4474
  "use strict";
4291
4475
  init_src();
4476
+ init_font_faces();
4292
4477
  init_runtime();
4293
4478
  init_tsc_check();
4294
4479
  init_token_lint();
4480
+ declValue = (raw) => raw.trim().replace(/\s*!important$/i, "").trim();
4295
4481
  }
4296
4482
  });
4297
4483
 
@@ -4334,8 +4520,8 @@ var init_effect_geometry = __esm({
4334
4520
  });
4335
4521
 
4336
4522
  // packages/verify/src/bundle-score.ts
4337
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
4338
- import path15 from "node:path";
4523
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
4524
+ import path16 from "node:path";
4339
4525
  import { build as build4 } from "esbuild";
4340
4526
  import { chromium as chromium4 } from "playwright-core";
4341
4527
  function getFontFaces2() {
@@ -4389,7 +4575,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
4389
4575
  }
4390
4576
  function metadataRoot(set, rep) {
4391
4577
  try {
4392
- const text = JSON.parse(readFileSync8(path15.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
4578
+ const text = JSON.parse(readFileSync8(path16.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
4393
4579
  return parseMetadataStructure(text);
4394
4580
  } catch {
4395
4581
  return void 0;
@@ -4444,17 +4630,17 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
4444
4630
  });
4445
4631
  }
4446
4632
  function repMeta(set, rep) {
4447
- const text = JSON.parse(readFileSync8(path15.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
4633
+ const text = JSON.parse(readFileSync8(path16.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
4448
4634
  const root = parseMetadataStructure(text);
4449
4635
  return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
4450
4636
  }
4451
4637
  function repRef(set, rep) {
4452
- const env = JSON.parse(readFileSync8(path15.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
4638
+ const env = JSON.parse(readFileSync8(path16.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
4453
4639
  return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
4454
4640
  }
4455
4641
  function repEffectExtents(set, rep) {
4456
- const file = path15.join(set, rep, "get_design_context.json");
4457
- if (!existsSync10(file)) return void 0;
4642
+ const file = path16.join(set, rep, "get_design_context.json");
4643
+ if (!existsSync11(file)) return void 0;
4458
4644
  try {
4459
4645
  const text = JSON.parse(readFileSync8(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
4460
4646
  const extents = shadowExtents(text);
@@ -4467,13 +4653,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
4467
4653
  if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
4468
4654
  const CONFIGS2 = task.configs;
4469
4655
  const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
4470
- const entryTsx = path15.join(bundleDir, task.entry);
4471
- if (!existsSync10(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
4472
- const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) => existsSync10(f)).map((f) => readFileSync8(f, "utf8")).join("\n");
4656
+ const entryTsx = path16.join(bundleDir, task.entry);
4657
+ 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` }));
4658
+ const css = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f)).filter((f) => existsSync11(f)).map((f) => readFileSync8(f, "utf8")).join("\n");
4473
4659
  const mountSrc = `
4474
4660
  import { createElement } from "react";
4475
4661
  import { createRoot } from "react-dom/client";
4476
- import * as B from ${JSON.stringify(path15.resolve(entryTsx))};
4662
+ import * as B from ${JSON.stringify(path16.resolve(entryTsx))};
4477
4663
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
4478
4664
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
4479
4665
  const root = document.getElementById("root");
@@ -4568,12 +4754,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
4568
4754
  return name === void 0 ? c : { ...c, name };
4569
4755
  });
4570
4756
  if (opts.evidenceDir !== void 0) {
4571
- writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
4572
- writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
4573
- writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
4757
+ writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
4758
+ writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
4759
+ writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
4574
4760
  for (const [i, c] of absent.slice(0, 3).entries()) {
4575
- writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
4576
- writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
4761
+ writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
4762
+ writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
4577
4763
  }
4578
4764
  }
4579
4765
  return {
@@ -4672,7 +4858,7 @@ var init_prelude = __esm({
4672
4858
  "use strict";
4673
4859
  PRELUDE_CONTRACT = `BUNDLE PRELUDE (machine-verified on computed styles \u2014 the harness page provides NONE of this; your CSS must):
4674
4860
  - Containment reset on the component subtree: margin 0, box-sizing border-box, line-height/letter-spacing/font-family inherited from the root (host globals must not leak in).
4675
- - Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (a missing font weight must fall back visibly, never fake-bold); color-scheme MATCHING YOUR RECORDING (light for a light capture, dark for a dark one) and direction: ltr \u2014 pin them, so the render cannot follow the viewer OS preference and drift from the capture it is graded against; isolation: isolate (own stacking context; overlay z-indexes never fight the host).
4861
+ - Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (no faux bold or italic \u2014 text renders in the face your stack actually binds, so a weight the kit cannot serve reaches the score instead of being faked; it does NOT fall through to the next family in the stack); color-scheme MATCHING YOUR RECORDING (light for a light capture, dark for a dark one) and direction: ltr \u2014 pin them, so the render cannot follow the viewer OS preference and drift from the capture it is graded against; isolation: isolate (own stacking context; overlay z-indexes never fight the host).
4676
4862
  - Interactive controls (buttons, options): touch-action: manipulation and user-select: none. Text inputs stay selectable (never user-select: none on them).
4677
4863
  - Scrollable popovers/menus: overscroll-behavior: contain.
4678
4864
  - Focus indicators bind to :focus-visible, never bare :focus. If the recording contains a focus pose, style the indicator from that recorded truth. If NO focus pose is recorded, do NOT invent ring colors/widths/offsets \u2014 an invented ring is unrecorded pixels; keep the browser's default indicator for keyboard focus and state the gap in your report. Text inputs match :focus-visible even on mouse click BY SPEC; the ONE permitted refinement is suppressing the indicator on a POSITIVELY OBSERVED pointer press, fail-safe toward showing it (programmatic focus, restored focus, and assistive tech all count as keyboard and keep the ring \u2014 WCAG 2.4.7 binds keyboard operation). Never suppress more broadly than an observed press, and never restyle what you kept.
@@ -4681,8 +4867,8 @@ var init_prelude = __esm({
4681
4867
  });
4682
4868
 
4683
4869
  // packages/verify/src/parity.ts
4684
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
4685
- import path16 from "node:path";
4870
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
4871
+ import path17 from "node:path";
4686
4872
  import { chromium as chromium6 } from "playwright-core";
4687
4873
  function getFontFaces3() {
4688
4874
  _fontFaces3 ??= fontFaceCss();
@@ -4711,7 +4897,7 @@ async function checkHoverParity(task, bundleDir, opts = {}) {
4711
4897
  const deadlineMs = timeoutMs + 1e4;
4712
4898
  const js = await compileMount(task, bundleDir);
4713
4899
  if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
4714
- const css = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f)).filter((f) => existsSync11(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
4900
+ const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
4715
4901
  const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
4716
4902
  const browser = await chromium6.connect(server.wsEndpoint());
4717
4903
  const results = [];
@@ -4791,8 +4977,8 @@ var init_parity = __esm({
4791
4977
 
4792
4978
  // packages/verify/src/composition.ts
4793
4979
  import { createRequire as createRequire2 } from "node:module";
4794
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
4795
- import path17 from "node:path";
4980
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
4981
+ import path18 from "node:path";
4796
4982
  import { build as build6 } from "esbuild";
4797
4983
  import { chromium as chromium7 } from "playwright-core";
4798
4984
  function getFontFaces4() {
@@ -4800,9 +4986,9 @@ function getFontFaces4() {
4800
4986
  return _fontFaces4;
4801
4987
  }
4802
4988
  async function compileInstrumentedMount(task, bundleDir) {
4803
- const entryTsx = path17.join(bundleDir, task.entry);
4804
- if (!existsSync12(entryTsx)) return { error: `${task.entry} missing` };
4805
- const requireFromVerify = createRequire2(path17.join(VERIFY_PKG_DIR, "package.json"));
4989
+ const entryTsx = path18.join(bundleDir, task.entry);
4990
+ if (!existsSync13(entryTsx)) return { error: `${task.entry} missing` };
4991
+ const requireFromVerify = createRequire2(path18.join(VERIFY_PKG_DIR, "package.json"));
4806
4992
  let realJsxPath;
4807
4993
  try {
4808
4994
  realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
@@ -4813,7 +4999,7 @@ async function compileInstrumentedMount(task, bundleDir) {
4813
4999
  import { createElement } from "react";
4814
5000
  import { createRoot } from "react-dom/client";
4815
5001
  import { __registerParts } from "react/jsx-runtime";
4816
- import * as B from ${JSON.stringify(path17.resolve(entryTsx))};
5002
+ import * as B from ${JSON.stringify(path18.resolve(entryTsx))};
4817
5003
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
4818
5004
  const pairs: Array<[unknown, string]> = [];
4819
5005
  for (const name of cfg.partComponents) {
@@ -4868,7 +5054,7 @@ function expectedParts(task, roles, mainSlug) {
4868
5054
  }
4869
5055
  function interiorRegions(setDir, roles) {
4870
5056
  const mains = roles.main;
4871
- const withInterior = mains.filter((m) => existsSync12(path17.join(setDir, m, "get_metadata_interior.json")));
5057
+ const withInterior = mains.filter((m) => existsSync13(path18.join(setDir, m, "get_metadata_interior.json")));
4872
5058
  if (withInterior.length === 0) {
4873
5059
  return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
4874
5060
  }
@@ -4885,7 +5071,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
4885
5071
  if (typeof js !== "string") {
4886
5072
  return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
4887
5073
  }
4888
- const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
5074
+ const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
4889
5075
  const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
4890
5076
  const browser = await chromium7.connect(server.wsEndpoint());
4891
5077
  const PAD = 4;
@@ -4990,7 +5176,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
4990
5176
  const deadlineMs = timeoutMs + 1e4;
4991
5177
  const js = await compileInstrumentedMount(task, bundleDir);
4992
5178
  if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
4993
- const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
5179
+ const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
4994
5180
  const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
4995
5181
  const browser = await chromium7.connect(server.wsEndpoint());
4996
5182
  try {
@@ -5080,17 +5266,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
5080
5266
  });
5081
5267
 
5082
5268
  // packages/verify/src/occlusion.ts
5083
- import { existsSync as existsSync13 } from "node:fs";
5084
- import path18 from "node:path";
5269
+ import { existsSync as existsSync14 } from "node:fs";
5270
+ import path19 from "node:path";
5085
5271
  import { build as build7 } from "esbuild";
5086
5272
  import { chromium as chromium8 } from "playwright-core";
5087
5273
  async function compileTwoUp(task, bundleDir) {
5088
- const entryTsx = path18.join(bundleDir, task.entry);
5089
- if (!existsSync13(entryTsx)) return { error: `${task.entry} missing` };
5274
+ const entryTsx = path19.join(bundleDir, task.entry);
5275
+ if (!existsSync14(entryTsx)) return { error: `${task.entry} missing` };
5090
5276
  const src = `
5091
5277
  import { createElement } from "react";
5092
5278
  import { createRoot } from "react-dom/client";
5093
- import * as B from ${JSON.stringify(path18.resolve(entryTsx))};
5279
+ import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
5094
5280
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
5095
5281
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
5096
5282
  for (const id of ["first", "second"]) {
@@ -5268,21 +5454,21 @@ var init_src4 = __esm({
5268
5454
  });
5269
5455
 
5270
5456
  // packages/cli/src/environment.ts
5271
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
5272
- import path19 from "node:path";
5457
+ import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
5458
+ import path20 from "node:path";
5273
5459
  import { createHash as createHash3 } from "node:crypto";
5274
- import { fileURLToPath as fileURLToPath4 } from "node:url";
5460
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
5275
5461
  function cliVersion() {
5276
5462
  try {
5277
- return JSON.parse(readFileSync11(path19.join(path19.dirname(fileURLToPath4(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
5463
+ return JSON.parse(readFileSync11(path20.join(path20.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
5278
5464
  } catch {
5279
5465
  return "dev";
5280
5466
  }
5281
5467
  }
5282
5468
  function environmentStamp(taskFamilies) {
5283
- const manifestPath2 = path19.join(fontCacheDir(), "manifest.json");
5469
+ const manifestPath2 = path20.join(fontCacheDir(), "manifest.json");
5284
5470
  let fontsHash = null;
5285
- if (existsSync14(manifestPath2)) {
5471
+ if (existsSync15(manifestPath2)) {
5286
5472
  try {
5287
5473
  const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
5288
5474
  const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
@@ -5298,8 +5484,8 @@ function environmentStamp(taskFamilies) {
5298
5484
  } catch {
5299
5485
  }
5300
5486
  const ver = chrome === "unavailable" ? null : chromeVersion();
5301
- const lcdOff = lcdTextDisabled() ? " (lcd-text-disabled)" : "";
5302
- return { chrome, chromeVersion: ver === null ? null : ver + lcdOff, fontsManifestSha256: fontsHash };
5487
+ const lcdSuffix = lcdTextEnabled() ? " (lcd-text-enabled)" : "";
5488
+ return { chrome, chromeVersion: ver === null ? null : ver + lcdSuffix, fontsManifestSha256: fontsHash };
5303
5489
  }
5304
5490
  var init_environment = __esm({
5305
5491
  "packages/cli/src/environment.ts"() {
@@ -5328,8 +5514,8 @@ var init_describe = __esm({
5328
5514
  });
5329
5515
 
5330
5516
  // packages/cli/src/env.ts
5331
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
5332
- import path20 from "node:path";
5517
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
5518
+ import path21 from "node:path";
5333
5519
  function parseEnv(content) {
5334
5520
  const entries = /* @__PURE__ */ new Map();
5335
5521
  for (const line of content.split("\n")) {
@@ -5341,8 +5527,8 @@ function parseEnv(content) {
5341
5527
  function resolveCredential(name) {
5342
5528
  const fromProcess = process.env[name];
5343
5529
  if (fromProcess) return fromProcess;
5344
- const envPath = path20.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
5345
- if (!existsSync15(envPath)) return void 0;
5530
+ const envPath = path21.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
5531
+ if (!existsSync16(envPath)) return void 0;
5346
5532
  return parseEnv(readFileSync12(envPath, "utf8")).get(name);
5347
5533
  }
5348
5534
  var init_env = __esm({
@@ -5403,15 +5589,15 @@ var init_output = __esm({
5403
5589
  });
5404
5590
 
5405
5591
  // packages/cli/src/entitlement.ts
5406
- import { chmodSync, existsSync as existsSync16, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
5592
+ import { chmodSync, existsSync as existsSync17, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
5407
5593
  import crypto from "node:crypto";
5408
5594
  import os3 from "node:os";
5409
- import path21 from "node:path";
5595
+ import path22 from "node:path";
5410
5596
  function entitlementPath() {
5411
- return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path21.join(os3.homedir(), ".tendril", "entitlement.json");
5597
+ return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path22.join(os3.homedir(), ".tendril", "entitlement.json");
5412
5598
  }
5413
5599
  function readStoredEntitlement(file = entitlementPath()) {
5414
- if (!existsSync16(file)) return void 0;
5600
+ if (!existsSync17(file)) return void 0;
5415
5601
  try {
5416
5602
  const parsed = JSON.parse(readFileSync13(file, "utf8"));
5417
5603
  if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
@@ -5421,7 +5607,7 @@ function readStoredEntitlement(file = entitlementPath()) {
5421
5607
  }
5422
5608
  }
5423
5609
  function writeStoredEntitlement(stored, file = entitlementPath()) {
5424
- mkdirSync4(path21.dirname(file), { recursive: true });
5610
+ mkdirSync4(path22.dirname(file), { recursive: true });
5425
5611
  writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
5426
5612
  `);
5427
5613
  chmodSync(file, 384);
@@ -5447,11 +5633,11 @@ function checkEntitlement(opts = {}) {
5447
5633
  const now = opts.now ?? Date.now();
5448
5634
  const stored = "stored" in opts ? opts.stored : readStoredEntitlement();
5449
5635
  if (stored === void 0) {
5450
- return { ok: false, code: "entitlement-required", error: "no entitlement on this machine \u2014 record and generate need an active Tendril plan (verify stays free, always)", remediation: ACTIVATE_REMEDIATION };
5636
+ return { ok: false, code: "entitlement-required", error: "no entitlement on this machine \u2014 record and generate need an active Tendril plan (verify stays free, always)", remediation: activateRemediation() };
5451
5637
  }
5452
5638
  const parsed = parseEntitlementToken(stored.token);
5453
5639
  if ("error" in parsed) {
5454
- return { ok: false, code: "entitlement-invalid", error: `stored entitlement is unreadable: ${parsed.error}`, remediation: ACTIVATE_REMEDIATION };
5640
+ return { ok: false, code: "entitlement-invalid", error: `stored entitlement is unreadable: ${parsed.error}`, remediation: activateRemediation() };
5455
5641
  }
5456
5642
  const pem = keys[parsed.claims.kid];
5457
5643
  const valid = pem !== void 0 && (() => {
@@ -5462,14 +5648,14 @@ function checkEntitlement(opts = {}) {
5462
5648
  }
5463
5649
  })();
5464
5650
  if (!valid) {
5465
- return { ok: false, code: "entitlement-invalid", error: "stored entitlement failed signature verification", remediation: ACTIVATE_REMEDIATION };
5651
+ return { ok: false, code: "entitlement-invalid", error: "stored entitlement failed signature verification", remediation: activateRemediation() };
5466
5652
  }
5467
5653
  if (now > parsed.claims.exp + TOLERANCE_MS) {
5468
5654
  return {
5469
5655
  ok: false,
5470
5656
  code: "entitlement-expired",
5471
5657
  error: "entitlement expired (and the offline tolerance window has passed)",
5472
- remediation: `Reconnect and ${ACTIVATE_REMEDIATION}`
5658
+ remediation: `Reconnect and ${activateRemediation()}`
5473
5659
  };
5474
5660
  }
5475
5661
  if (now < stored.lastRefreshAt - CLOCK_ROLLBACK_MS) {
@@ -5477,7 +5663,7 @@ function checkEntitlement(opts = {}) {
5477
5663
  ok: false,
5478
5664
  code: "entitlement-clock",
5479
5665
  error: "system clock sits more than a day before the last entitlement refresh",
5480
- remediation: `Fix the system clock, then ${ACTIVATE_REMEDIATION}`
5666
+ remediation: `Fix the system clock, then ${activateRemediation()}`
5481
5667
  };
5482
5668
  }
5483
5669
  return { ok: true, mode: "active", claims: parsed.claims, stale: now > parsed.claims.exp };
@@ -5488,26 +5674,27 @@ function requireEntitlement(flags) {
5488
5674
  fail(flags, ExitCode.Auth, { error: status.error, code: status.code, remediation: status.remediation });
5489
5675
  }
5490
5676
  }
5491
- var ENT_PREFIX, PUBLIC_KEYS, TOLERANCE_MS, CLOCK_ROLLBACK_MS, b64urlDecode, ACTIVATE_REMEDIATION;
5677
+ var ENT_PREFIX, PUBLIC_KEYS, TOLERANCE_MS, CLOCK_ROLLBACK_MS, b64urlDecode, activateRemediation;
5492
5678
  var init_entitlement = __esm({
5493
5679
  "packages/cli/src/entitlement.ts"() {
5494
5680
  "use strict";
5495
5681
  init_src3();
5682
+ init_invocation();
5496
5683
  init_output();
5497
5684
  ENT_PREFIX = "tendril-ent.v1.";
5498
5685
  PUBLIC_KEYS = {};
5499
5686
  TOLERANCE_MS = 24 * 60 * 60 * 1e3;
5500
5687
  CLOCK_ROLLBACK_MS = 24 * 60 * 60 * 1e3;
5501
5688
  b64urlDecode = (s) => Buffer.from(s, "base64url");
5502
- ACTIVATE_REMEDIATION = "Run `tendril activate` in your terminal (a browser approval \u2014 never paste license material into an agent chat).";
5689
+ activateRemediation = () => `Run \`${tendrilCommand("activate")}\` in your terminal (a browser approval \u2014 never paste license material into an agent chat).`;
5503
5690
  }
5504
5691
  });
5505
5692
 
5506
5693
  // packages/cli/src/commands/doctor.ts
5507
5694
  import { spawnSync } from "node:child_process";
5508
- import { existsSync as existsSync17, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
5695
+ import { existsSync as existsSync18, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
5509
5696
  import os4 from "node:os";
5510
- import path22 from "node:path";
5697
+ import path23 from "node:path";
5511
5698
  async function runDoctorChecks(options) {
5512
5699
  const checks = [];
5513
5700
  const mcpUrl = options.mcpUrl ?? DEFAULT_MCP_URL;
@@ -5537,12 +5724,12 @@ async function runDoctorChecks(options) {
5537
5724
  name: "openrouter-key",
5538
5725
  ok: false,
5539
5726
  detail: "OPENROUTER_API_KEY not set \u2014 optional; only the curated API-model engine needs it",
5540
- remediation: "Run `tendril init` to store your OpenRouter key (BYOK) if you plan to use the curated engine."
5727
+ remediation: `Run \`${tendrilCommand("init")}\` to store your OpenRouter key (BYOK) if you plan to use the curated engine.`
5541
5728
  }
5542
5729
  );
5543
5730
  try {
5544
5731
  const chrome = resolveChrome();
5545
- checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"}${lcdTextDisabled() ? " \u2014 EXPERIMENT override active (lcd-text-disabled): scores not comparable to default runs" : ""} at ${chrome}` });
5732
+ checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"}${lcdTextEnabled() ? " \u2014 EXPERIMENT override active (lcd-text-enabled): scores not comparable to canonical greyscale runs" : ""} at ${chrome}` });
5546
5733
  } catch (err) {
5547
5734
  checks.push({
5548
5735
  name: "browser",
@@ -5551,17 +5738,17 @@ async function runDoctorChecks(options) {
5551
5738
  remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
5552
5739
  });
5553
5740
  }
5554
- const fontManifest = path22.join(fontCacheDir(), "manifest.json");
5741
+ const fontManifest = path23.join(fontCacheDir(), "manifest.json");
5555
5742
  checks.push(
5556
- existsSync17(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
5743
+ existsSync18(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
5557
5744
  name: "font-cache",
5558
5745
  ok: true,
5559
5746
  detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
5560
- remediation: "Nothing to do now: `tendril fonts resolve --set <recording-dir>` fetches exactly what a recording declares, and generate/verify name that command when they need it."
5747
+ 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.`
5561
5748
  }
5562
5749
  );
5563
- const pluginRoot = path22.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
5564
- if (existsSync17(pluginRoot)) {
5750
+ const pluginRoot = path23.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
5751
+ if (existsSync18(pluginRoot)) {
5565
5752
  try {
5566
5753
  const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
5567
5754
  const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
@@ -5605,17 +5792,6 @@ async function runDoctorChecks(options) {
5605
5792
  });
5606
5793
  return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key" && c.name !== "path-skew").every((c) => c.ok), checks };
5607
5794
  }
5608
- function findPathTendril(pathEnv, platform) {
5609
- const dirs = pathEnv.split(path22.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
5610
- const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
5611
- for (const dir of dirs) {
5612
- for (const name of names) {
5613
- const candidate = path22.join(dir, name);
5614
- if (existsSync17(candidate)) return candidate;
5615
- }
5616
- }
5617
- return null;
5618
- }
5619
5795
  function probeVersion(binary) {
5620
5796
  const windowsShim = /\.(cmd|bat)$/i.test(binary);
5621
5797
  const res = windowsShim ? spawnSync(`"${binary}" --version`, { shell: true, timeout: 5e3, encoding: "utf8" }) : spawnSync(binary, ["--version"], { timeout: 5e3, encoding: "utf8" });
@@ -5687,6 +5863,7 @@ var init_doctor = __esm({
5687
5863
  init_describe();
5688
5864
  init_env();
5689
5865
  init_environment();
5866
+ init_invocation();
5690
5867
  init_output();
5691
5868
  init_entitlement();
5692
5869
  DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
@@ -6676,7 +6853,64 @@ var init_extract = __esm({
6676
6853
 
6677
6854
  // packages/metadata/src/recording-set.ts
6678
6855
  import { z as z9 } from "zod";
6679
- var RECORDING_SET_VERSION, EnvelopeSchema, RepEntrySchema, RolesSchema, RecordingSetManifestSchema;
6856
+ function roleLossToken(loss) {
6857
+ return loss.kind === "main" ? `main:${loss.main}` : `part:${loss.part}${EDGE_ARROW}${loss.main}`;
6858
+ }
6859
+ function parseRoleLossToken(token) {
6860
+ if (token.startsWith("main:")) {
6861
+ const main2 = token.slice("main:".length);
6862
+ return main2 === "" ? void 0 : { kind: "main", main: main2 };
6863
+ }
6864
+ if (!token.startsWith("part:")) return void 0;
6865
+ const [part, main, ...rest] = token.slice("part:".length).split(EDGE_ARROW);
6866
+ if (rest.length > 0 || part === void 0 || part === "" || main === void 0 || main === "") return void 0;
6867
+ return { kind: "edge", part, main };
6868
+ }
6869
+ function validateRecordingSet(manifest, fileExists) {
6870
+ const issues = [];
6871
+ const parsed = RecordingSetManifestSchema.safeParse(manifest);
6872
+ if (!parsed.success) {
6873
+ return {
6874
+ issues: [
6875
+ {
6876
+ severity: "error",
6877
+ message: `manifest does not match recording-set layout v${RECORDING_SET_VERSION}: ${parsed.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
6878
+ }
6879
+ ]
6880
+ };
6881
+ }
6882
+ const m = parsed.data;
6883
+ const slugs = /* @__PURE__ */ new Set();
6884
+ for (const rep of m.reps) {
6885
+ if (slugs.has(rep.slug)) issues.push({ severity: "error", message: `duplicate rep slug "${rep.slug}"` });
6886
+ slugs.add(rep.slug);
6887
+ for (const f of REQUIRED_REP_FILES) {
6888
+ if (!fileExists(`${rep.slug}/${f}`)) issues.push({ severity: "error", message: `${rep.slug}: missing ${f}` });
6889
+ }
6890
+ if (rep.sourceFrame !== void 0 && m.sourceFrames?.[rep.sourceFrame] === void 0)
6891
+ issues.push({ severity: "warning", message: `${rep.slug}: sourceFrame ${rep.sourceFrame} not described in sourceFrames` });
6892
+ }
6893
+ if (m.roles !== void 0) {
6894
+ for (const slug of m.roles.main)
6895
+ if (!slugs.has(slug)) issues.push({ severity: "error", message: `roles.main references unknown slug "${slug}"` });
6896
+ for (const [slug, part] of Object.entries(m.roles.parts)) {
6897
+ if (!slugs.has(slug)) issues.push({ severity: "error", message: `roles.parts references unknown slug "${slug}"` });
6898
+ for (const of of part.partOf)
6899
+ if (!m.roles.main.includes(of)) issues.push({ severity: "error", message: `roles.parts["${slug}"].partOf references non-main "${of}"` });
6900
+ }
6901
+ for (const token of m.roles.narrowingAccepted ?? []) {
6902
+ const loss = parseRoleLossToken(token);
6903
+ if (loss === void 0) {
6904
+ issues.push({ severity: "error", message: `roles.narrowingAccepted entry "${token}" is not "main:<slug>" or "part:<part-slug>-><main-slug>"` });
6905
+ continue;
6906
+ }
6907
+ for (const slug of loss.kind === "main" ? [loss.main] : [loss.part, loss.main])
6908
+ if (!slugs.has(slug)) issues.push({ severity: "error", message: `roles.narrowingAccepted "${token}" references unknown slug "${slug}"` });
6909
+ }
6910
+ }
6911
+ return { manifest: m, issues };
6912
+ }
6913
+ var RECORDING_SET_VERSION, EnvelopeSchema, RepEntrySchema, RolesSchema, EDGE_ARROW, RecordingSetManifestSchema, REQUIRED_REP_FILES;
6680
6914
  var init_recording_set = __esm({
6681
6915
  "packages/metadata/src/recording-set.ts"() {
6682
6916
  "use strict";
@@ -6709,10 +6943,21 @@ var init_recording_set = __esm({
6709
6943
  * derivation — disclosed, never silently dropped (Calendar's Icon
6710
6944
  * Buttons; unrecorded base variants). */
6711
6945
  external: z9.array(z9.string()).optional(),
6712
- /** True when a human override changed derived roles (surfaced in
6713
- * provenance downstream). */
6714
- humanOverride: z9.boolean().optional()
6946
+ /** Where this graph came from. A graph with empty `parts` makes the
6947
+ * structural composition check return an affirmative PASS, so a
6948
+ * human-authored override and a derived graph must never be
6949
+ * indistinguishable in a report. The CLI stamps this itself — a
6950
+ * supplied roles file cannot claim "derived" for itself. */
6951
+ rolesSource: z9.enum(["derived", "human-override"]).optional(),
6952
+ /** Losses in the DERIVED relation a human override deliberately
6953
+ * accepts, one token per loss (`roleLossToken`). Named one by one on
6954
+ * purpose: emptying out what composition measures — by dropping a
6955
+ * main, or by dropping/re-pointing a part→main edge — is exactly what
6956
+ * turns composition into a vacuous pass, so it is an acceptance
6957
+ * someone signs rather than an omission nobody notices. */
6958
+ narrowingAccepted: z9.array(z9.string()).optional()
6715
6959
  });
6960
+ EDGE_ARROW = "->";
6716
6961
  RecordingSetManifestSchema = z9.object({
6717
6962
  version: z9.literal(RECORDING_SET_VERSION),
6718
6963
  /** Human-readable component/system name. */
@@ -6724,6 +6969,7 @@ var init_recording_set = __esm({
6724
6969
  notRecorded: z9.string().optional(),
6725
6970
  roles: RolesSchema.optional()
6726
6971
  });
6972
+ REQUIRED_REP_FILES = ["get_design_context.json", "get_metadata.json", "get_screenshot.json"];
6727
6973
  }
6728
6974
  });
6729
6975
 
@@ -6930,7 +7176,7 @@ async function runActivate(flags) {
6930
7176
  fail(flags, ExitCode.Auth, {
6931
7177
  error: `could not start activation: ${err instanceof Error ? err.message : String(err)}`,
6932
7178
  code: "entitlement-service-unreachable",
6933
- remediation: "Check your connection and retry `tendril activate`."
7179
+ remediation: `Check your connection and retry \`${tendrilCommand("activate")}\`.`
6934
7180
  });
6935
7181
  }
6936
7182
  process.stderr.write(`To activate Tendril, visit:
@@ -6957,7 +7203,7 @@ Waiting for approval\u2026
6957
7203
  fail(flags, ExitCode.Auth, {
6958
7204
  error: `activation was not approved (HTTP ${res.status})`,
6959
7205
  code: "entitlement-denied",
6960
- remediation: "Retry `tendril activate`; if it persists, check the account's plan in the portal."
7206
+ remediation: `Retry \`${tendrilCommand("activate")}\`; if it persists, check the account's plan in the portal.`
6961
7207
  });
6962
7208
  }
6963
7209
  token = (await res.json()).token;
@@ -6967,7 +7213,7 @@ Waiting for approval\u2026
6967
7213
  fail(flags, ExitCode.Auth, {
6968
7214
  error: "activation timed out before the browser approval arrived",
6969
7215
  code: "entitlement-timeout",
6970
- remediation: "Run `tendril activate` again and complete the browser step within the shown window."
7216
+ remediation: `Run \`${tendrilCommand("activate")}\` again and complete the browser step within the shown window.`
6971
7217
  });
6972
7218
  }
6973
7219
  const parsed = parseEntitlementToken(token);
@@ -6975,7 +7221,7 @@ Waiting for approval\u2026
6975
7221
  fail(flags, ExitCode.Auth, {
6976
7222
  error: `service returned an unusable token: ${parsed.error}`,
6977
7223
  code: "entitlement-invalid",
6978
- remediation: "Retry `tendril activate`; report this if it persists \u2014 it is a service-side fault."
7224
+ remediation: `Retry \`${tendrilCommand("activate")}\`; report this if it persists \u2014 it is a service-side fault.`
6979
7225
  });
6980
7226
  }
6981
7227
  if (Object.keys(PUBLIC_KEYS).length > 0) {
@@ -6994,6 +7240,7 @@ var init_activate = __esm({
6994
7240
  "use strict";
6995
7241
  init_src3();
6996
7242
  init_entitlement();
7243
+ init_invocation();
6997
7244
  init_output();
6998
7245
  ENTITLEMENT_SERVICE_URL = void 0;
6999
7246
  }
@@ -7005,6 +7252,7 @@ __export(record_exports, {
7005
7252
  instanceLeads: () => instanceLeads,
7006
7253
  isFigmaAssetUrl: () => isFigmaAssetUrl,
7007
7254
  isLocalAssetUrl: () => isLocalAssetUrl,
7255
+ narrowedRoles: () => narrowedRoles,
7008
7256
  nextPayload: () => nextPayload,
7009
7257
  runRecordAsset: () => runRecordAsset,
7010
7258
  runRecordFetch: () => runRecordFetch,
@@ -7015,9 +7263,9 @@ __export(record_exports, {
7015
7263
  runRecordPlan: () => runRecordPlan,
7016
7264
  runRecordStatus: () => runRecordStatus
7017
7265
  });
7018
- import { existsSync as existsSync19, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
7266
+ import { existsSync as existsSync20, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
7019
7267
  import os5 from "node:os";
7020
- import path25 from "node:path";
7268
+ import path26 from "node:path";
7021
7269
  import { writeFileSync as writeFileSync9 } from "node:fs";
7022
7270
  function symbolsFromMetadataEnvelope(file, sourceFrame) {
7023
7271
  const env = JSON.parse(readFileSync16(file, "utf8"));
@@ -7078,7 +7326,7 @@ function runRecordPlan(opts) {
7078
7326
  if (rawFile !== void 0) {
7079
7327
  try {
7080
7328
  const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
7081
- const tmp = path25.join(mkdtempSync2(path25.join(os5.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
7329
+ const tmp = path26.join(mkdtempSync2(path26.join(os5.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
7082
7330
  writeFileSync9(tmp, JSON.stringify(envelope));
7083
7331
  metadataEntries.push({ file: tmp });
7084
7332
  } catch (err) {
@@ -7100,7 +7348,7 @@ function runRecordPlan(opts) {
7100
7348
  let metadataTruncated = false;
7101
7349
  for (const { file, frame } of metadataEntries) {
7102
7350
  try {
7103
- const parsed = symbolsFromMetadataEnvelope(path25.resolve(file), frame);
7351
+ const parsed = symbolsFromMetadataEnvelope(path26.resolve(file), frame);
7104
7352
  symbols.push(...parsed.symbols);
7105
7353
  if (parsed.truncated) metadataTruncated = true;
7106
7354
  } catch (err) {
@@ -7133,7 +7381,7 @@ function runRecordPlan(opts) {
7133
7381
  if (symbols.length === 0) {
7134
7382
  const leads = metadataEntries.flatMap(({ file }) => {
7135
7383
  try {
7136
- const env = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
7384
+ const env = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
7137
7385
  return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
7138
7386
  } catch {
7139
7387
  return [];
@@ -7238,7 +7486,7 @@ function nextPayload(setDir) {
7238
7486
  const instruction = nextInstruction(setDir);
7239
7487
  const status = sessionStatus(setDir);
7240
7488
  const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
7241
- if (instruction === null && !existsSync19(path25.join(setDir, "get_variable_defs.json"))) {
7489
+ if (instruction === null && !existsSync20(path26.join(setDir, "get_variable_defs.json"))) {
7242
7490
  const manifest = loadManifest(setDir);
7243
7491
  const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
7244
7492
  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 };
@@ -7255,7 +7503,7 @@ function runRecordNext(opts) {
7255
7503
  const progress = payload["progress"];
7256
7504
  process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
7257
7505
  \u2192 ${payload["note"]}
7258
- \u2192 then: tendril record ingest --set <dir> --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>
7506
+ \u2192 then: ${tendrilCommand(`record ingest --set ${path26.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
7259
7507
  `);
7260
7508
  });
7261
7509
  }
@@ -7329,7 +7577,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
7329
7577
  const skipped = [];
7330
7578
  const failed = [];
7331
7579
  for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
7332
- if (existsSync19(path25.join(setDir, rep, name))) {
7580
+ if (existsSync20(path26.join(setDir, rep, name))) {
7333
7581
  skipped.push(name);
7334
7582
  continue;
7335
7583
  }
@@ -7351,16 +7599,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
7351
7599
  }
7352
7600
  function rawEnvelopeFromFile(file, parts) {
7353
7601
  if (parts) {
7354
- const blocks = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
7602
+ const blocks = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
7355
7603
  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");
7356
7604
  return { content: blocks.map((text) => ({ type: "text", text })) };
7357
7605
  }
7358
- return { content: [{ type: "text", text: readFileSync16(path25.resolve(file), "utf8") }] };
7606
+ return { content: [{ type: "text", text: readFileSync16(path26.resolve(file), "utf8") }] };
7359
7607
  }
7360
7608
  async function runRecordIngest(opts) {
7361
7609
  let payload;
7362
7610
  try {
7363
- payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(path25.resolve(opts.file), "utf8"));
7611
+ payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(path26.resolve(opts.file), "utf8"));
7364
7612
  } catch (err) {
7365
7613
  fail(opts, ExitCode.InputValidation, {
7366
7614
  error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
@@ -7372,7 +7620,7 @@ async function runRecordIngest(opts) {
7372
7620
  fail(opts, ExitCode.InputValidation, {
7373
7621
  error: "--raw is for text tool responses; screenshots are binary",
7374
7622
  code: "envelope-invalid",
7375
- remediation: "Use `tendril record fetch` with the image_url instead."
7623
+ remediation: `Use \`${tendrilCommand(`record fetch --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
7376
7624
  });
7377
7625
  }
7378
7626
  if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
@@ -7384,7 +7632,7 @@ async function runRecordIngest(opts) {
7384
7632
  remediation: "Save the get_variable_defs response verbatim as a text envelope."
7385
7633
  });
7386
7634
  }
7387
- writeFileSync9(path25.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
7635
+ writeFileSync9(path26.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
7388
7636
  `);
7389
7637
  emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
7390
7638
  process.stdout.write("set-level get_variable_defs ingested\n");
@@ -7400,7 +7648,7 @@ async function runRecordIngest(opts) {
7400
7648
  if (assets !== void 0) {
7401
7649
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
7402
7650
  `);
7403
- for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it and run tendril record asset
7651
+ 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>`)}
7404
7652
  `);
7405
7653
  }
7406
7654
  });
@@ -7473,14 +7721,14 @@ async function runRecordIngestRep(opts) {
7473
7721
  if (assets !== void 0) {
7474
7722
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
7475
7723
  `);
7476
- for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it and run tendril record asset
7724
+ 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>`)}
7477
7725
  `);
7478
7726
  }
7479
7727
  });
7480
7728
  }
7481
7729
  function runRecordAsset(opts) {
7482
7730
  if (opts.dir !== void 0) {
7483
- const dir = path25.resolve(opts.dir);
7731
+ const dir = path26.resolve(opts.dir);
7484
7732
  const names = readdirSync5(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
7485
7733
  if (names.length === 0) {
7486
7734
  fail(opts, ExitCode.InputValidation, {
@@ -7492,7 +7740,7 @@ function runRecordAsset(opts) {
7492
7740
  const ingested = [];
7493
7741
  try {
7494
7742
  for (const name of names) {
7495
- ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path25.join(dir, name)));
7743
+ ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path26.join(dir, name)));
7496
7744
  ingested.push(name);
7497
7745
  }
7498
7746
  } catch (err) {
@@ -7512,11 +7760,11 @@ function runRecordAsset(opts) {
7512
7760
  fail(opts, ExitCode.InputValidation, {
7513
7761
  error: "pass --name and --file for a single asset, or --dir for a batch",
7514
7762
  code: "asset-rejected",
7515
- remediation: "tendril record asset --set <dir> --rep <slug> --dir <downloads-dir>"
7763
+ remediation: tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
7516
7764
  });
7517
7765
  }
7518
7766
  try {
7519
- ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(path25.resolve(opts.file)));
7767
+ ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(path26.resolve(opts.file)));
7520
7768
  emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
7521
7769
  process.stdout.write(`ingested ${opts.rep}/${opts.name}
7522
7770
  `);
@@ -7541,11 +7789,66 @@ ${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
7541
7789
  `);
7542
7790
  });
7543
7791
  }
7792
+ function narrowedRoles(derived, override) {
7793
+ const losses = [];
7794
+ const mains = new Set(override.main);
7795
+ for (const main of derived.main) if (!mains.has(main)) losses.push({ kind: "main", main });
7796
+ for (const [part, entry] of Object.entries(derived.parts))
7797
+ for (const main of entry.partOf) if (override.parts[part]?.partOf.includes(main) !== true) losses.push({ kind: "edge", part, main });
7798
+ return losses.sort((a, b) => roleLossToken(a) < roleLossToken(b) ? -1 : 1);
7799
+ }
7800
+ function rolesFromFile(opts, file, derived) {
7801
+ let json;
7802
+ try {
7803
+ json = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
7804
+ } catch (err) {
7805
+ fail(opts, ExitCode.InputValidation, {
7806
+ error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
7807
+ code: "roles-file-unreadable",
7808
+ remediation: 'The roles file is JSON: {"main":["<slug>"],"parts":{"<slug>":{"partOf":["<main-slug>"]}}}.'
7809
+ });
7810
+ }
7811
+ const parsed = RolesSchema.safeParse(json);
7812
+ if (!parsed.success) {
7813
+ fail(opts, ExitCode.InputValidation, {
7814
+ error: `roles file rejected: ${parsed.error.issues.slice(0, 3).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`,
7815
+ code: "roles-file-invalid",
7816
+ remediation: 'The roles file is JSON: {"main":["<slug>"],"parts":{"<slug>":{"partOf":["<main-slug>"]}},"external":["\u2026"]}.'
7817
+ });
7818
+ }
7819
+ const accepted = parsed.data.narrowingAccepted ?? [];
7820
+ const unaccepted = narrowedRoles(derived, parsed.data).filter((loss) => !accepted.includes(roleLossToken(loss)));
7821
+ if (unaccepted.length > 0) {
7822
+ fail(opts, ExitCode.InputValidation, {
7823
+ error: `roles file narrows what this set's derivation found: it ${unaccepted.map(describeRoleLoss).join("; ")} \u2014 a main the graph never names is not composition-checked at all, and a main whose graph names no parts passes the check vacuously`,
7824
+ code: "roles-file-narrows",
7825
+ remediation: `Keep the derived mains and part edges in the graph, or accept each loss by name: add "narrowingAccepted": ${JSON.stringify(
7826
+ [.../* @__PURE__ */ new Set([...accepted, ...unaccepted.map(roleLossToken)])].sort()
7827
+ )} to the roles file.`
7828
+ });
7829
+ }
7830
+ return {
7831
+ main: parsed.data.main,
7832
+ parts: parsed.data.parts,
7833
+ ...parsed.data.external !== void 0 ? { external: parsed.data.external } : {},
7834
+ ...accepted.length > 0 ? { narrowingAccepted: accepted } : {},
7835
+ // Stamped by the CLI, never read from the file: a supplied graph
7836
+ // does not get to describe itself as derived.
7837
+ rolesSource: "human-override"
7838
+ };
7839
+ }
7544
7840
  function runRecordFinish(opts) {
7545
- const manifest = loadManifest(opts.setDir);
7841
+ if (!existsSync20(path26.join(opts.setDir, "recording-set.json"))) {
7842
+ fail(opts, ExitCode.InputValidation, {
7843
+ error: `no recording-set.json in ${opts.setDir}`,
7844
+ code: "no-recording-set",
7845
+ 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.`
7846
+ });
7847
+ }
7848
+ const { manifest, raw } = readManifestFile(opts.setDir);
7546
7849
  const derived = deriveRoles(opts.setDir, manifest);
7547
- const roles = opts.rolesFile !== void 0 ? { ...JSON.parse(readFileSync16(path25.resolve(opts.rolesFile), "utf8")), humanOverride: true } : { main: derived.main, parts: derived.parts, external: derived.external, humanOverride: false };
7548
- emitData(opts, { derived, confirmed: opts.confirmRoles }, () => {
7850
+ const roles = opts.rolesFile !== void 0 ? rolesFromFile(opts, opts.rolesFile, derived) : { main: derived.main, parts: derived.parts, external: derived.external, rolesSource: "derived" };
7851
+ emitData(opts, { derived, confirmed: opts.confirmRoles, rolesSource: roles.rolesSource, narrowingAccepted: roles.narrowingAccepted ?? [] }, () => {
7549
7852
  process.stdout.write(`derived mains: ${derived.main.join(", ") || "(none)"}
7550
7853
  `);
7551
7854
  for (const [slug, part] of Object.entries(derived.parts)) process.stdout.write(` part ${slug} \u2192 ${part.partOf.join(", ")}
@@ -7567,24 +7870,40 @@ function runRecordFinish(opts) {
7567
7870
  fail(opts, ExitCode.ConfirmationRequired, {
7568
7871
  error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
7569
7872
  code: "roles-confirmation-not-interactive",
7570
- remediation: "A human runs `tendril record finish --set <dir> --confirm-roles` in their own terminal. Agents: show the proposal to your operator instead of confirming it."
7873
+ 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.`
7874
+ });
7875
+ }
7876
+ const merged = { ...raw, roles };
7877
+ const { issues } = validateRecordingSet(merged, (rel) => existsSync20(path26.join(opts.setDir, rel)));
7878
+ const errors = issues.filter((i) => i.severity === "error");
7879
+ if (errors.length > 0) {
7880
+ fail(opts, ExitCode.InputValidation, {
7881
+ error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
7882
+ code: "recording-set-invalid",
7883
+ remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path26.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
7571
7884
  });
7572
7885
  }
7573
- const updated = { ...manifest, roles };
7574
- writeFileSync9(path25.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
7886
+ for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
7887
+ writeManifest(opts.setDir, merged);
7888
+ if (!opts.json) {
7889
+ const accepted = roles.narrowingAccepted ?? [];
7890
+ process.stdout.write(`roles written to recording-set.json (source: ${roles.rolesSource})${accepted.length > 0 ? ` \u2014 narrowing accepted by name: ${accepted.join(", ")}` : ""}
7575
7891
  `);
7576
- if (!opts.json) process.stdout.write("roles written to recording-set.json\n");
7892
+ }
7577
7893
  }
7578
- var ENVELOPE_HELP, isAutoFetchAssetUrl;
7894
+ var ENVELOPE_HELP, isAutoFetchAssetUrl, describeRoleLoss;
7579
7895
  var init_record = __esm({
7580
7896
  "packages/cli/src/commands/record.ts"() {
7581
7897
  "use strict";
7582
7898
  init_src3();
7583
7899
  init_src();
7900
+ init_src6();
7584
7901
  init_output();
7585
7902
  init_entitlement();
7586
- ENVELOPE_HELP = 'Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; get_screenshot: do NOT download the image yourself \u2014 pass its image_url to `tendril record fetch` (MCP: tendril_record_fetch), which pulls the bytes to disk directly. That keeps the pixel ground truth out of your context and costs one approval instead of a shell command per asset. Assets are asset-<first-8-hex-of-figma-uuid>.<ext>; their URLs appear as const declarations inside the design-context text and also expire.';
7903
+ init_invocation();
7904
+ ENVELOPE_HELP = `Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; get_screenshot: do NOT download the image yourself \u2014 pass its image_url to \`${tendrilCommand("record fetch")}\` (MCP: tendril_record_fetch), which pulls the bytes to disk directly. That keeps the pixel ground truth out of your context and costs one approval instead of a shell command per asset. Assets are asset-<first-8-hex-of-figma-uuid>.<ext>; their URLs appear as const declarations inside the design-context text and also expire.`;
7587
7905
  isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
7906
+ describeRoleLoss = (loss) => loss.kind === "main" ? `drops main "${loss.main}"` : `stops "${loss.part}" being a part of main "${loss.main}"`;
7588
7907
  }
7589
7908
  });
7590
7909
 
@@ -7775,8 +8094,8 @@ var init_engine_curated = __esm({
7775
8094
  });
7776
8095
 
7777
8096
  // packages/generate/src/loop.ts
7778
- import { existsSync as existsSync20, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
7779
- import path26 from "node:path";
8097
+ import { existsSync as existsSync21, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
8098
+ import path27 from "node:path";
7780
8099
  import { z as z11 } from "zod";
7781
8100
  function objective(scores, behaviors) {
7782
8101
  const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
@@ -7810,12 +8129,12 @@ ${preludeLines.join("\n")}` : ""}${absentLines.length > 0 ? `
7810
8129
  MISSING FEATURES (absent-ink clusters \u2014 recorded marks your render leaves out or paints invisibly; GATING at the cert bar. Fix the named node's ink \u2014 a config carrying one cannot certify):
7811
8130
  ${absentLines.join("\n")}` : ""}
7812
8131
 
7813
- 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=1 with LOW sim means nothing is missing \u2014 shapes or positions are wrong, do not chase missing ink; 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."}`;
8132
+ 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=1 with LOW sim means nothing is MISSING (do not chase missing ink) \u2014 the same pixels are being painted wrong, and there are TWO causes. 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."}`;
7814
8133
  }
7815
8134
  function archivePriorRun(outDir) {
7816
- if (!existsSync20(path26.join(outDir, "run-log.json")) && !existsSync20(path26.join(outDir, "loop-state.json"))) return void 0;
8135
+ if (!existsSync21(path27.join(outDir, "run-log.json")) && !existsSync21(path27.join(outDir, "loop-state.json"))) return void 0;
7817
8136
  let n = 1;
7818
- while (existsSync20(`${outDir}-prev-${n}`)) n += 1;
8137
+ while (existsSync21(`${outDir}-prev-${n}`)) n += 1;
7819
8138
  renameSync(outDir, `${outDir}-prev-${n}`);
7820
8139
  return `${outDir}-prev-${n}`;
7821
8140
  }
@@ -7824,14 +8143,14 @@ async function runEngineLoop(opts) {
7824
8143
  const plateau = opts.plateau ?? 2;
7825
8144
  const progress = opts.onProgress ?? (() => {
7826
8145
  });
7827
- const statePath = path26.join(opts.outDir, "loop-state.json");
7828
- const resuming = opts.resume === true && existsSync20(statePath);
8146
+ const statePath = path27.join(opts.outDir, "loop-state.json");
8147
+ const resuming = opts.resume === true && existsSync21(statePath);
7829
8148
  if (!resuming) {
7830
8149
  const archived = archivePriorRun(opts.outDir);
7831
8150
  if (archived !== void 0) progress(`previous run archived to ${archived}`);
7832
8151
  }
7833
8152
  mkdirSync6(opts.outDir, { recursive: true });
7834
- const scratch = path26.join(opts.outDir, ".candidate");
8153
+ const scratch = path27.join(opts.outDir, ".candidate");
7835
8154
  let attempts = [];
7836
8155
  let log = [];
7837
8156
  let best;
@@ -7859,7 +8178,7 @@ async function runEngineLoop(opts) {
7859
8178
  };
7860
8179
  const writeCandidate = (files) => {
7861
8180
  mkdirSync6(scratch, { recursive: true });
7862
- for (const [name, content] of Object.entries(files)) writeFileSync10(path26.join(scratch, name), content);
8181
+ for (const [name, content] of Object.entries(files)) writeFileSync10(path27.join(scratch, name), content);
7863
8182
  };
7864
8183
  const scoreCandidate = async (candidate, iter, usd, modelMs) => {
7865
8184
  writeCandidate(candidate.files);
@@ -7917,8 +8236,8 @@ async function runEngineLoop(opts) {
7917
8236
  const usd = candidate.usage?.usd ?? 0;
7918
8237
  spentUsd += usd;
7919
8238
  if (candidate.raw !== void 0) {
7920
- mkdirSync6(path26.join(opts.outDir, "responses"), { recursive: true });
7921
- writeFileSync10(path26.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
8239
+ mkdirSync6(path27.join(opts.outDir, "responses"), { recursive: true });
8240
+ writeFileSync10(path27.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
7922
8241
  }
7923
8242
  if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
7924
8243
  const finish = candidate.usage?.finishReason ?? "?";
@@ -7944,10 +8263,10 @@ async function runEngineLoop(opts) {
7944
8263
  }
7945
8264
  }
7946
8265
  }
7947
- if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path26.join(opts.outDir, name), content);
8266
+ if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path27.join(opts.outDir, name), content);
7948
8267
  const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
7949
8268
  writeFileSync10(
7950
- path26.join(opts.outDir, "run-log.json"),
8269
+ path27.join(opts.outDir, "run-log.json"),
7951
8270
  `${JSON.stringify(
7952
8271
  {
7953
8272
  ...opts.meta,
@@ -8014,8 +8333,8 @@ var init_loop2 = __esm({
8014
8333
  });
8015
8334
 
8016
8335
  // packages/generate/src/brief.ts
8017
- import { existsSync as existsSync21, readFileSync as readFileSync18 } from "node:fs";
8018
- import path27 from "node:path";
8336
+ import { existsSync as existsSync22, readFileSync as readFileSync18 } from "node:fs";
8337
+ import path28 from "node:path";
8019
8338
  function singleAxes2(name) {
8020
8339
  const parsed = parseVariantAxes(name);
8021
8340
  if (parsed === void 0) return void 0;
@@ -8254,8 +8573,8 @@ function envelopeText(file) {
8254
8573
  }
8255
8574
  function dismissEvidence(setDir, repSlugs) {
8256
8575
  for (const slug of repSlugs) {
8257
- const f = path27.join(setDir, slug, "get_design_context.json");
8258
- if (!existsSync21(f)) continue;
8576
+ const f = path28.join(setDir, slug, "get_design_context.json");
8577
+ if (!existsSync22(f)) continue;
8259
8578
  const text = envelopeText(f);
8260
8579
  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);
8261
8580
  if (propHit !== null) return `emission prop "${propHit[1]}"`;
@@ -8299,13 +8618,13 @@ function recordedFontNeeds(setDir, opts = {}) {
8299
8618
  }
8300
8619
  };
8301
8620
  const manifest = loadManifest(setDir);
8302
- const setDefs = path27.join(setDir, "get_variable_defs.json");
8303
- if (existsSync21(setDefs)) fromDefs(envelopeText(setDefs));
8621
+ const setDefs = path28.join(setDir, "get_variable_defs.json");
8622
+ if (existsSync22(setDefs)) fromDefs(envelopeText(setDefs));
8304
8623
  for (const rep of manifest.reps) {
8305
- const ctx = path27.join(setDir, rep.slug, "get_design_context.json");
8306
- if (existsSync21(ctx)) fromEmission(envelopeText(ctx));
8307
- const defs = path27.join(setDir, rep.slug, "get_variable_defs.json");
8308
- if (existsSync21(defs)) fromDefs(envelopeText(defs));
8624
+ const ctx = path28.join(setDir, rep.slug, "get_design_context.json");
8625
+ if (existsSync22(ctx)) fromEmission(envelopeText(ctx));
8626
+ const defs = path28.join(setDir, rep.slug, "get_variable_defs.json");
8627
+ if (existsSync22(defs)) fromDefs(envelopeText(defs));
8309
8628
  }
8310
8629
  return [...byFamily.entries()].map(([family, paired]) => {
8311
8630
  const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
@@ -8316,8 +8635,8 @@ function symbolFontGlyphCount(setDir, reps) {
8316
8635
  const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
8317
8636
  const glyphs = /* @__PURE__ */ new Set();
8318
8637
  for (const rep of reps) {
8319
- const file = path27.join(setDir, rep, "get_metadata.json");
8320
- if (!existsSync21(file)) continue;
8638
+ const file = path28.join(setDir, rep, "get_metadata.json");
8639
+ if (!existsSync22(file)) continue;
8321
8640
  try {
8322
8641
  const text = JSON.parse(readFileSync18(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
8323
8642
  for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
@@ -8345,8 +8664,8 @@ function recordedTextSlots(setDir, repSlugs) {
8345
8664
  const propRep = [];
8346
8665
  const perRep = [];
8347
8666
  for (const slug of repSlugs) {
8348
- const f = path27.join(setDir, slug, "get_design_context.json");
8349
- if (!existsSync21(f)) continue;
8667
+ const f = path28.join(setDir, slug, "get_design_context.json");
8668
+ if (!existsSync22(f)) continue;
8350
8669
  const code = envelopeText(f);
8351
8670
  const props = /* @__PURE__ */ new Map();
8352
8671
  for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
@@ -8371,8 +8690,8 @@ function recordedTextSlots(setDir, repSlugs) {
8371
8690
  }
8372
8691
  const axisValuesBySlug = /* @__PURE__ */ new Map();
8373
8692
  for (const slug of repSlugs) {
8374
- const metaFile = path27.join(setDir, slug, "get_metadata.json");
8375
- if (!existsSync21(metaFile)) continue;
8693
+ const metaFile = path28.join(setDir, slug, "get_metadata.json");
8694
+ if (!existsSync22(metaFile)) continue;
8376
8695
  const name = symbolName(envelopeText(metaFile));
8377
8696
  if (name === void 0) continue;
8378
8697
  const values = /* @__PURE__ */ new Set();
@@ -8471,8 +8790,8 @@ function authorTaskFromSet(setDir, opts = {}) {
8471
8790
  const poses = [];
8472
8791
  const missing = [];
8473
8792
  for (const rep of manifest.reps) {
8474
- const metaFile = path27.join(setDir, rep.slug, "get_metadata.json");
8475
- if (!existsSync21(metaFile)) {
8793
+ const metaFile = path28.join(setDir, rep.slug, "get_metadata.json");
8794
+ if (!existsSync22(metaFile)) {
8476
8795
  missing.push(rep.slug);
8477
8796
  continue;
8478
8797
  }
@@ -8486,8 +8805,8 @@ function authorTaskFromSet(setDir, opts = {}) {
8486
8805
  if (missing.length > 0) {
8487
8806
  throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
8488
8807
  }
8489
- const setMeta = path27.join(setDir, "get_metadata.json");
8490
- const latticeNames = manifest.latticeNames ?? (existsSync21(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
8808
+ const setMeta = path28.join(setDir, "get_metadata.json");
8809
+ const latticeNames = manifest.latticeNames ?? (existsSync22(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
8491
8810
  const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
8492
8811
  const recordedFonts = recordedFontFamilies(setDir);
8493
8812
  const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
@@ -8661,10 +8980,10 @@ var init_brief = __esm({
8661
8980
  });
8662
8981
 
8663
8982
  // packages/generate/src/segments.ts
8664
- import { existsSync as existsSync22, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
8665
- import path28 from "node:path";
8983
+ import { existsSync as existsSync23, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
8984
+ import path29 from "node:path";
8666
8985
  function repText(set, rep, tool) {
8667
- const env = JSON.parse(readFileSync19(path28.join(set, rep, `${tool}.json`), "utf8"));
8986
+ const env = JSON.parse(readFileSync19(path29.join(set, rep, `${tool}.json`), "utf8"));
8668
8987
  return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
8669
8988
  }
8670
8989
  function stripFigmaInstructions(emission) {
@@ -8725,16 +9044,16 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
8725
9044
  function buildSegments(task, mode = "fenced") {
8726
9045
  const SET = task.set;
8727
9046
  let rawDefs = {};
8728
- if (existsSync22(path28.join(SET, "get_variable_defs.json"))) {
8729
- const text = envelopeFirstTextPart(JSON.parse(readFileSync19(path28.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
9047
+ if (existsSync23(path29.join(SET, "get_variable_defs.json"))) {
9048
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync19(path29.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
8730
9049
  try {
8731
9050
  rawDefs = JSON.parse(text);
8732
9051
  } catch {
8733
9052
  }
8734
9053
  } else {
8735
9054
  for (const cfg of task.configs) {
8736
- const f = path28.join(SET, cfg.rep, "get_variable_defs.json");
8737
- if (!existsSync22(f)) continue;
9055
+ const f = path29.join(SET, cfg.rep, "get_variable_defs.json");
9056
+ if (!existsSync23(f)) continue;
8738
9057
  const text = envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) || "{}";
8739
9058
  try {
8740
9059
  for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
@@ -8743,8 +9062,8 @@ function buildSegments(task, mode = "fenced") {
8743
9062
  }
8744
9063
  }
8745
9064
  const emissionTexts = task.configs.map((cfg) => {
8746
- const f = path28.join(SET, cfg.rep, "get_design_context.json");
8747
- return existsSync22(f) ? envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) : "";
9065
+ const f = path29.join(SET, cfg.rep, "get_design_context.json");
9066
+ return existsSync23(f) ? envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) : "";
8748
9067
  });
8749
9068
  const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
8750
9069
  const defs = JSON.stringify(map, null, 1);
@@ -8759,9 +9078,9 @@ ${defs}
8759
9078
  for (const cfg of task.configs) {
8760
9079
  const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
8761
9080
  const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
8762
- const assets = readdirSync6(path28.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
9081
+ const assets = readdirSync6(path29.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
8763
9082
  \`\`\`svg
8764
- ${readFileSync19(path28.join(SET, cfg.rep, f), "utf8")}
9083
+ ${readFileSync19(path29.join(SET, cfg.rep, f), "utf8")}
8765
9084
  \`\`\``).join("\n");
8766
9085
  parts.push(`
8767
9086
  ## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
@@ -8786,7 +9105,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
8786
9105
  } else {
8787
9106
  parts.push(`
8788
9107
  ## Output format
8789
- Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into your candidate directory. Do not paste file contents into chat \u2014 the scorer reads the directory.`);
9108
+ 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.`);
8790
9109
  }
8791
9110
  return parts.join("\n");
8792
9111
  }
@@ -8854,8 +9173,8 @@ var init_adapter = __esm({
8854
9173
 
8855
9174
  // packages/generate/src/bundle-emit.ts
8856
9175
  import { createHash as createHash4 } from "node:crypto";
8857
- import { copyFileSync, existsSync as existsSync23, mkdirSync as mkdirSync7, readFileSync as readFileSync20, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
8858
- import path29 from "node:path";
9176
+ import { copyFileSync, existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync20, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
9177
+ import path30 from "node:path";
8859
9178
  function pinFromConfigs(configs) {
8860
9179
  const domains = /* @__PURE__ */ new Map();
8861
9180
  const kinds = /* @__PURE__ */ new Map();
@@ -8903,6 +9222,11 @@ function cssFontFamilies(css) {
8903
9222
  }
8904
9223
  return [...out];
8905
9224
  }
9225
+ function upstreamAttribution(face) {
9226
+ if (!face.source.startsWith(GOOGLE_FONT_FILE_PREFIX)) return face.source;
9227
+ if (face.servedFamily === void 0) return `the Google Fonts specimen for "${face.family.replace(/[\r\n]+/g, " ")}"`;
9228
+ return `https://fonts.google.com/specimen/${encodeURIComponent(face.servedFamily).replace(/%20/g, "+")}/license`;
9229
+ }
8906
9230
  function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitutedFamilies = []) {
8907
9231
  const header = [
8908
9232
  "/* tendril fonts.css \u2014 the exact faces this bundle was scored with (sha-pinned in",
@@ -8916,32 +9240,78 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
8916
9240
  ] : []
8917
9241
  ];
8918
9242
  const lines = [];
9243
+ const notices = [];
9244
+ const licenseTexts = /* @__PURE__ */ new Map();
8919
9245
  for (const face of faces) {
8920
- const src = path29.join(cacheDir, path29.basename(face.file));
8921
- const target = `./fonts/${path29.basename(face.file)}`;
8922
- const format = FONT_FORMATS[path29.extname(face.file).toLowerCase()] ?? "truetype";
9246
+ const src = path30.join(cacheDir, path30.basename(face.file));
9247
+ const target = `./fonts/${path30.basename(face.file)}`;
9248
+ const format = FONT_FORMATS[path30.extname(face.file).toLowerCase()] ?? "truetype";
8923
9249
  const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
8924
9250
  const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
8925
- if (face.source.startsWith("local:")) {
8926
- lines.push(
8927
- `/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
8928
- ` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
8929
- `/* ${decl} */`
9251
+ const license = normalizeFontLicense(face.license);
9252
+ const terms = REDISTRIBUTABLE.get(license);
9253
+ if (terms === void 0) {
9254
+ if (face.source.startsWith("local:")) {
9255
+ lines.push(
9256
+ `/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
9257
+ ` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
9258
+ `/* ${decl} */`
9259
+ );
9260
+ } else if (license === "unknown") {
9261
+ lines.push(
9262
+ `/* '${family}' ${face.weight}: no licence on record (sha256 ${face.sha256.slice(0, 16)}\u2026). That is a gap`,
9263
+ " in the font cache, not a statement about your rights \u2014 Tendril copies font bytes into a",
9264
+ " bundle only under a licence it can name, so this face is declared here instead. Establish",
9265
+ ` the licence with \`tendril fonts resolve "${family}" --weights ${face.weight}\``,
9266
+ " (or `tendril fonts resolve --set <recording-dir>`) and re-emit. If it is a face you licence",
9267
+ ` rather than one free to redistribute, place your copy at ${target} and uncomment: */`,
9268
+ `/* ${decl} */`
9269
+ );
9270
+ } else {
9271
+ lines.push(
9272
+ `/* '${family}' ${face.weight} is recorded as ${license} (sha256 ${face.sha256.slice(0, 16)}\u2026), which does`,
9273
+ " not permit redistribution \u2014 Tendril never copies such bytes. Place your licensed copy",
9274
+ ` at ${target} and uncomment: */`,
9275
+ `/* ${decl} */`
9276
+ );
9277
+ }
9278
+ } else if (existsSync24(src) && createHash4("sha256").update(readFileSync20(src)).digest("hex") === face.sha256) {
9279
+ mkdirSync7(path30.join(bundleDir, "fonts"), { recursive: true });
9280
+ copyFileSync(src, path30.join(bundleDir, "fonts", path30.basename(face.file)));
9281
+ licenseTexts.set(terms.file, terms.text);
9282
+ const upstream = upstreamAttribution(face);
9283
+ notices.push(
9284
+ "",
9285
+ `${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path30.basename(face.file)}`,
9286
+ ` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
9287
+ ` source: ${face.source}`,
9288
+ ` sha256: ${face.sha256}`,
9289
+ " copyright: this face's upstream copyright statement is published with its",
9290
+ ` original release \u2014 ${upstream}`
8930
9291
  );
8931
- } else if (existsSync23(src) && createHash4("sha256").update(readFileSync20(src)).digest("hex") === face.sha256) {
8932
- mkdirSync7(path29.join(bundleDir, "fonts"), { recursive: true });
8933
- copyFileSync(src, path29.join(bundleDir, "fonts", path29.basename(face.file)));
8934
9292
  lines.push(decl);
8935
9293
  } else {
8936
9294
  lines.push(`/* '${family}' ${face.weight} (sha256 ${face.sha256.slice(0, 16)}\u2026) could not be shipped: cache bytes missing or hash mismatch \u2014 re-run \`tendril fonts resolve\` and re-emit. */`);
8937
9295
  }
8938
9296
  }
8939
- return lines.length === 0 ? null : `${[...header, ...lines].join("\n")}
9297
+ if (lines.length === 0) return null;
9298
+ if (notices.length > 0) {
9299
+ const fontsDir = path30.join(bundleDir, "fonts");
9300
+ for (const [file, text] of licenseTexts) writeFileSync11(path30.join(fontsDir, file), text);
9301
+ writeFileSync11(path30.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
9302
+ `);
9303
+ header.push(
9304
+ "/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
9305
+ ` recorded in ./fonts/NOTICE.txt; full texts in ${[...licenseTexts.keys()].map((f) => `./fonts/${f}`).join(", ")}.`,
9306
+ " Keep NOTICE.txt and the licence texts with the font files wherever they travel. */"
9307
+ );
9308
+ }
9309
+ return `${[...header, ...lines].join("\n")}
8940
9310
  `;
8941
9311
  }
8942
9312
  function countLatticeSymbols(setDir) {
8943
- const manifestFile = path29.join(setDir, "recording-set.json");
8944
- if (existsSync23(manifestFile)) {
9313
+ const manifestFile = path30.join(setDir, "recording-set.json");
9314
+ if (existsSync24(manifestFile)) {
8945
9315
  try {
8946
9316
  const lattice = JSON.parse(readFileSync20(manifestFile, "utf8")).latticeNames;
8947
9317
  if (lattice !== void 0 && lattice.length > 0) return lattice.length;
@@ -8949,9 +9319,9 @@ function countLatticeSymbols(setDir) {
8949
9319
  }
8950
9320
  }
8951
9321
  const files = [
8952
- path29.join(setDir, "get_metadata.json"),
8953
- ...existsSync23(setDir) ? readdirSync7(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path29.join(setDir, f)) : []
8954
- ].filter((f) => existsSync23(f));
9322
+ path30.join(setDir, "get_metadata.json"),
9323
+ ...existsSync24(setDir) ? readdirSync7(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path30.join(setDir, f)) : []
9324
+ ].filter((f) => existsSync24(f));
8955
9325
  if (files.length === 0) return null;
8956
9326
  let count = 0;
8957
9327
  for (const f of files) {
@@ -8963,21 +9333,21 @@ function countLatticeSymbols(setDir) {
8963
9333
  function recordingSetHash(setDir, configs) {
8964
9334
  const relPaths = [];
8965
9335
  for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
8966
- if (existsSync23(path29.join(setDir, name))) relPaths.push(name);
9336
+ if (existsSync24(path30.join(setDir, name))) relPaths.push(name);
8967
9337
  }
8968
9338
  for (const cfg of configs) {
8969
9339
  for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
8970
- if (existsSync23(path29.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
9340
+ if (existsSync24(path30.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
8971
9341
  }
8972
- if (existsSync23(path29.join(setDir, cfg.rep))) {
8973
- for (const asset of readdirSync7(path29.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
9342
+ if (existsSync24(path30.join(setDir, cfg.rep))) {
9343
+ for (const asset of readdirSync7(path30.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
8974
9344
  relPaths.push(`${cfg.rep}/${asset}`);
8975
9345
  }
8976
9346
  }
8977
9347
  }
8978
9348
  return hashRecordingSet(
8979
9349
  relPaths,
8980
- (p) => new Uint8Array(readFileSync20(path29.join(setDir, p))),
9350
+ (p) => new Uint8Array(readFileSync20(path30.join(setDir, p))),
8981
9351
  (chunks) => {
8982
9352
  const h = createHash4("sha256");
8983
9353
  for (const c of chunks) h.update(c);
@@ -9000,7 +9370,7 @@ function emitBundleV1(opts) {
9000
9370
  const lattice = countLatticeSymbols(opts.task.set);
9001
9371
  const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
9002
9372
  const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
9003
- const cssFiles = ["styles.css", "tokens.css"].map((f) => path29.join(opts.bundleDir, f)).filter((f) => existsSync23(f));
9373
+ const cssFiles = ["styles.css", "tokens.css"].map((f) => path30.join(opts.bundleDir, f)).filter((f) => existsSync24(f));
9004
9374
  const families = cssFontFamilies(cssFiles.map((f) => readFileSync20(f, "utf8")).join("\n"));
9005
9375
  const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
9006
9376
  family: f.family,
@@ -9030,7 +9400,7 @@ function emitBundleV1(opts) {
9030
9400
  // resolvable via verify's --set override).
9031
9401
  path: (() => {
9032
9402
  const base = process.env["INIT_CWD"] ?? process.cwd();
9033
- const rel = path29.relative(base, opts.task.set);
9403
+ const rel = path30.relative(base, opts.task.set);
9034
9404
  return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
9035
9405
  })(),
9036
9406
  component: opts.componentName,
@@ -9054,12 +9424,12 @@ function emitBundleV1(opts) {
9054
9424
  })
9055
9425
  };
9056
9426
  const written = [];
9057
- const manifestPath2 = path29.join(opts.bundleDir, "component.json");
9427
+ const manifestPath2 = path30.join(opts.bundleDir, "component.json");
9058
9428
  writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
9059
9429
  `);
9060
9430
  written.push(manifestPath2);
9061
- const stylesPath = path29.join(opts.bundleDir, "styles.css");
9062
- if (existsSync23(stylesPath)) {
9431
+ const stylesPath = path30.join(opts.bundleDir, "styles.css");
9432
+ if (existsSync24(stylesPath)) {
9063
9433
  const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
9064
9434
  const current = readFileSync20(stylesPath, "utf8");
9065
9435
  const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
@@ -9067,8 +9437,8 @@ function emitBundleV1(opts) {
9067
9437
  ${stripped}`);
9068
9438
  written.push(stylesPath);
9069
9439
  }
9070
- const fontsCssPath = path29.join(opts.bundleDir, "fonts.css");
9071
- rmSync3(path29.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
9440
+ const fontsCssPath = path30.join(opts.bundleDir, "fonts.css");
9441
+ rmSync3(path30.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
9072
9442
  rmSync3(fontsCssPath, { force: true });
9073
9443
  const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
9074
9444
  if (fontsCss !== null) {
@@ -9079,7 +9449,7 @@ ${stripped}`);
9079
9449
  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\``;
9080
9450
  return { manifest, statusLine, written };
9081
9451
  }
9082
- var FONT_FORMATS, BARS;
9452
+ var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
9083
9453
  var init_bundle_emit = __esm({
9084
9454
  "packages/generate/src/bundle-emit.ts"() {
9085
9455
  "use strict";
@@ -9091,6 +9461,408 @@ var init_bundle_emit = __esm({
9091
9461
  pass: { sim: 0.95, ink: 0.95 },
9092
9462
  cert: { sim: 0.97, ink: 0.95 }
9093
9463
  };
9464
+ NOTICE_PREAMBLE = [
9465
+ "Font redistribution notice \u2014 tendril bundle",
9466
+ "===========================================",
9467
+ "",
9468
+ "The font files in this directory were copied into the bundle because the",
9469
+ "licence they are served under permits redistribution. Each entry below names",
9470
+ "that licence, the exact file the bytes came from, and the sha256 the bundle",
9471
+ "was scored against. Full licence texts sit beside this file \u2014 keep them, and",
9472
+ "this notice, with the font files wherever they travel."
9473
+ ].join("\n");
9474
+ OFL_1_1_TEXT = `-----------------------------------------------------------
9475
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
9476
+ -----------------------------------------------------------
9477
+
9478
+ PREAMBLE
9479
+ The goals of the Open Font License (OFL) are to stimulate worldwide
9480
+ development of collaborative font projects, to support the font creation
9481
+ efforts of academic and linguistic communities, and to provide a free and
9482
+ open framework in which fonts may be shared and improved in partnership
9483
+ with others.
9484
+
9485
+ The OFL allows the licensed fonts to be used, studied, modified and
9486
+ redistributed freely as long as they are not sold by themselves. The
9487
+ fonts, including any derivative works, can be bundled, embedded,
9488
+ redistributed and/or sold with any software provided that any reserved
9489
+ names are not used by derivative works. The fonts and derivatives,
9490
+ however, cannot be released under any other type of license. The
9491
+ requirement for fonts to remain under this license does not apply
9492
+ to any document created using the fonts or their derivatives.
9493
+
9494
+ DEFINITIONS
9495
+ "Font Software" refers to the set of files released by the Copyright
9496
+ Holder(s) under this license and clearly marked as such. This may
9497
+ include source files, build scripts and documentation.
9498
+
9499
+ "Reserved Font Name" refers to any names specified as such after the
9500
+ copyright statement(s).
9501
+
9502
+ "Original Version" refers to the collection of Font Software components as
9503
+ distributed by the Copyright Holder(s).
9504
+
9505
+ "Modified Version" refers to any derivative made by adding to, deleting,
9506
+ or substituting -- in part or in whole -- any of the components of the
9507
+ Original Version, by changing formats or by porting the Font Software to a
9508
+ new environment.
9509
+
9510
+ "Author" refers to any designer, engineer, programmer, technical
9511
+ writer or other person who contributed to the Font Software.
9512
+
9513
+ PERMISSION & CONDITIONS
9514
+ Permission is hereby granted, free of charge, to any person obtaining
9515
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
9516
+ redistribute, and sell modified and unmodified copies of the Font
9517
+ Software, subject to the following conditions:
9518
+
9519
+ 1) Neither the Font Software nor any of its individual components,
9520
+ in Original or Modified Versions, may be sold by itself.
9521
+
9522
+ 2) Original or Modified Versions of the Font Software may be bundled,
9523
+ redistributed and/or sold with any software, provided that each copy
9524
+ contains the above copyright notice and this license. These can be
9525
+ included either as stand-alone text files, human-readable headers or
9526
+ in the appropriate machine-readable metadata fields within text or
9527
+ binary files as long as those fields can be easily viewed by the user.
9528
+
9529
+ 3) No Modified Version of the Font Software may use the Reserved Font
9530
+ Name(s) unless explicit written permission is granted by the corresponding
9531
+ Copyright Holder. This restriction only applies to the primary font name as
9532
+ presented to the users.
9533
+
9534
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
9535
+ Software shall not be used to promote, endorse or advertise any
9536
+ Modified Version, except to acknowledge the contribution(s) of the
9537
+ Copyright Holder(s) and the Author(s) or with their explicit written
9538
+ permission.
9539
+
9540
+ 5) The Font Software, modified or unmodified, in part or in whole,
9541
+ must be distributed entirely under this license, and must not be
9542
+ distributed under any other license. The requirement for fonts to
9543
+ remain under this license does not apply to any document created
9544
+ using the Font Software.
9545
+
9546
+ TERMINATION
9547
+ This license becomes null and void if any of the above conditions are
9548
+ not met.
9549
+
9550
+ DISCLAIMER
9551
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
9552
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
9553
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
9554
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
9555
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
9556
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
9557
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
9558
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
9559
+ OTHER DEALINGS IN THE FONT SOFTWARE.
9560
+ `;
9561
+ UFL_1_0_TEXT = `-------------------------------
9562
+ UBUNTU FONT LICENCE Version 1.0
9563
+ -------------------------------
9564
+
9565
+ PREAMBLE
9566
+ This licence allows the licensed fonts to be used, studied, modified and
9567
+ redistributed freely. The fonts, including any derivative works, can be
9568
+ bundled, embedded, and redistributed provided the terms of this licence
9569
+ are met. The fonts and derivatives, however, cannot be released under
9570
+ any other licence. The requirement for fonts to remain under this
9571
+ licence does not require any document created using the fonts or their
9572
+ derivatives to be published under this licence, as long as the primary
9573
+ purpose of the document is not to be a vehicle for the distribution of
9574
+ the fonts.
9575
+
9576
+ DEFINITIONS
9577
+ "Font Software" refers to the set of files released by the Copyright
9578
+ Holder(s) under this licence and clearly marked as such. This may
9579
+ include source files, build scripts and documentation.
9580
+
9581
+ "Original Version" refers to the collection of Font Software components
9582
+ as received under this licence.
9583
+
9584
+ "Modified Version" refers to any derivative made by adding to, deleting,
9585
+ or substituting -- in part or in whole -- any of the components of the
9586
+ Original Version, by changing formats or by porting the Font Software to
9587
+ a new environment.
9588
+
9589
+ "Copyright Holder(s)" refers to all individuals and companies who have a
9590
+ copyright ownership of the Font Software.
9591
+
9592
+ "Substantially Changed" refers to Modified Versions which can be easily
9593
+ identified as dissimilar to the Font Software by users of the Font
9594
+ Software comparing the Original Version with the Modified Version.
9595
+
9596
+ To "Propagate" a work means to do anything with it that, without
9597
+ permission, would make you directly or secondarily liable for
9598
+ infringement under applicable copyright law, except executing it on a
9599
+ computer or modifying a private copy. Propagation includes copying,
9600
+ distribution (with or without modification and with or without charging
9601
+ a redistribution fee), making available to the public, and in some
9602
+ countries other activities as well.
9603
+
9604
+ PERMISSION & CONDITIONS
9605
+ This licence does not grant any rights under trademark law and all such
9606
+ rights are reserved.
9607
+
9608
+ Permission is hereby granted, free of charge, to any person obtaining a
9609
+ copy of the Font Software, to propagate the Font Software, subject to
9610
+ the below conditions:
9611
+
9612
+ 1) Each copy of the Font Software must contain the above copyright
9613
+ notice and this licence. These can be included either as stand-alone
9614
+ text files, human-readable headers or in the appropriate machine-
9615
+ readable metadata fields within text or binary files as long as those
9616
+ fields can be easily viewed by the user.
9617
+
9618
+ 2) The font name complies with the following:
9619
+ (a) The Original Version must retain its name, unmodified.
9620
+ (b) Modified Versions which are Substantially Changed must be renamed to
9621
+ avoid use of the name of the Original Version or similar names entirely.
9622
+ (c) Modified Versions which are not Substantially Changed must be
9623
+ renamed to both (i) retain the name of the Original Version and (ii) add
9624
+ additional naming elements to distinguish the Modified Version from the
9625
+ Original Version. The name of such Modified Versions must be the name of
9626
+ the Original Version, with "derivative X" where X represents the name of
9627
+ the new work, appended to that name.
9628
+
9629
+ 3) The name(s) of the Copyright Holder(s) and any contributor to the
9630
+ Font Software shall not be used to promote, endorse or advertise any
9631
+ Modified Version, except (i) as required by this licence, (ii) to
9632
+ acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
9633
+ their explicit written permission.
9634
+
9635
+ 4) The Font Software, modified or unmodified, in part or in whole, must
9636
+ be distributed entirely under this licence, and must not be distributed
9637
+ under any other licence. The requirement for fonts to remain under this
9638
+ licence does not affect any document created using the Font Software,
9639
+ except any version of the Font Software extracted from a document
9640
+ created using the Font Software may only be distributed under this
9641
+ licence.
9642
+
9643
+ TERMINATION
9644
+ This licence becomes null and void if any of the above conditions are
9645
+ not met.
9646
+
9647
+ DISCLAIMER
9648
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
9649
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
9650
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
9651
+ COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
9652
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
9653
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
9654
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
9655
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
9656
+ DEALINGS IN THE FONT SOFTWARE.
9657
+ `;
9658
+ APACHE_2_0_TEXT = `
9659
+ Apache License
9660
+ Version 2.0, January 2004
9661
+ http://www.apache.org/licenses/
9662
+
9663
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
9664
+
9665
+ 1. Definitions.
9666
+
9667
+ "License" shall mean the terms and conditions for use, reproduction,
9668
+ and distribution as defined by Sections 1 through 9 of this document.
9669
+
9670
+ "Licensor" shall mean the copyright owner or entity authorized by
9671
+ the copyright owner that is granting the License.
9672
+
9673
+ "Legal Entity" shall mean the union of the acting entity and all
9674
+ other entities that control, are controlled by, or are under common
9675
+ control with that entity. For the purposes of this definition,
9676
+ "control" means (i) the power, direct or indirect, to cause the
9677
+ direction or management of such entity, whether by contract or
9678
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
9679
+ outstanding shares, or (iii) beneficial ownership of such entity.
9680
+
9681
+ "You" (or "Your") shall mean an individual or Legal Entity
9682
+ exercising permissions granted by this License.
9683
+
9684
+ "Source" form shall mean the preferred form for making modifications,
9685
+ including but not limited to software source code, documentation
9686
+ source, and configuration files.
9687
+
9688
+ "Object" form shall mean any form resulting from mechanical
9689
+ transformation or translation of a Source form, including but
9690
+ not limited to compiled object code, generated documentation,
9691
+ and conversions to other media types.
9692
+
9693
+ "Work" shall mean the work of authorship, whether in Source or
9694
+ Object form, made available under the License, as indicated by a
9695
+ copyright notice that is included in or attached to the work
9696
+ (an example is provided in the Appendix below).
9697
+
9698
+ "Derivative Works" shall mean any work, whether in Source or Object
9699
+ form, that is based on (or derived from) the Work and for which the
9700
+ editorial revisions, annotations, elaborations, or other modifications
9701
+ represent, as a whole, an original work of authorship. For the purposes
9702
+ of this License, Derivative Works shall not include works that remain
9703
+ separable from, or merely link (or bind by name) to the interfaces of,
9704
+ the Work and Derivative Works thereof.
9705
+
9706
+ "Contribution" shall mean any work of authorship, including
9707
+ the original version of the Work and any modifications or additions
9708
+ to that Work or Derivative Works thereof, that is intentionally
9709
+ submitted to Licensor for inclusion in the Work by the copyright owner
9710
+ or by an individual or Legal Entity authorized to submit on behalf of
9711
+ the copyright owner. For the purposes of this definition, "submitted"
9712
+ means any form of electronic, verbal, or written communication sent
9713
+ to the Licensor or its representatives, including but not limited to
9714
+ communication on electronic mailing lists, source code control systems,
9715
+ and issue tracking systems that are managed by, or on behalf of, the
9716
+ Licensor for the purpose of discussing and improving the Work, but
9717
+ excluding communication that is conspicuously marked or otherwise
9718
+ designated in writing by the copyright owner as "Not a Contribution."
9719
+
9720
+ "Contributor" shall mean Licensor and any individual or Legal Entity
9721
+ on behalf of whom a Contribution has been received by Licensor and
9722
+ subsequently incorporated within the Work.
9723
+
9724
+ 2. Grant of Copyright License. Subject to the terms and conditions of
9725
+ this License, each Contributor hereby grants to You a perpetual,
9726
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
9727
+ copyright license to reproduce, prepare Derivative Works of,
9728
+ publicly display, publicly perform, sublicense, and distribute the
9729
+ Work and such Derivative Works in Source or Object form.
9730
+
9731
+ 3. Grant of Patent License. Subject to the terms and conditions of
9732
+ this License, each Contributor hereby grants to You a perpetual,
9733
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
9734
+ (except as stated in this section) patent license to make, have made,
9735
+ use, offer to sell, sell, import, and otherwise transfer the Work,
9736
+ where such license applies only to those patent claims licensable
9737
+ by such Contributor that are necessarily infringed by their
9738
+ Contribution(s) alone or by combination of their Contribution(s)
9739
+ with the Work to which such Contribution(s) was submitted. If You
9740
+ institute patent litigation against any entity (including a
9741
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
9742
+ or a Contribution incorporated within the Work constitutes direct
9743
+ or contributory patent infringement, then any patent licenses
9744
+ granted to You under this License for that Work shall terminate
9745
+ as of the date such litigation is filed.
9746
+
9747
+ 4. Redistribution. You may reproduce and distribute copies of the
9748
+ Work or Derivative Works thereof in any medium, with or without
9749
+ modifications, and in Source or Object form, provided that You
9750
+ meet the following conditions:
9751
+
9752
+ (a) You must give any other recipients of the Work or
9753
+ Derivative Works a copy of this License; and
9754
+
9755
+ (b) You must cause any modified files to carry prominent notices
9756
+ stating that You changed the files; and
9757
+
9758
+ (c) You must retain, in the Source form of any Derivative Works
9759
+ that You distribute, all copyright, patent, trademark, and
9760
+ attribution notices from the Source form of the Work,
9761
+ excluding those notices that do not pertain to any part of
9762
+ the Derivative Works; and
9763
+
9764
+ (d) If the Work includes a "NOTICE" text file as part of its
9765
+ distribution, then any Derivative Works that You distribute must
9766
+ include a readable copy of the attribution notices contained
9767
+ within such NOTICE file, excluding those notices that do not
9768
+ pertain to any part of the Derivative Works, in at least one
9769
+ of the following places: within a NOTICE text file distributed
9770
+ as part of the Derivative Works; within the Source form or
9771
+ documentation, if provided along with the Derivative Works; or,
9772
+ within a display generated by the Derivative Works, if and
9773
+ wherever such third-party notices normally appear. The contents
9774
+ of the NOTICE file are for informational purposes only and
9775
+ do not modify the License. You may add Your own attribution
9776
+ notices within Derivative Works that You distribute, alongside
9777
+ or as an addendum to the NOTICE text from the Work, provided
9778
+ that such additional attribution notices cannot be construed
9779
+ as modifying the License.
9780
+
9781
+ You may add Your own copyright statement to Your modifications and
9782
+ may provide additional or different license terms and conditions
9783
+ for use, reproduction, or distribution of Your modifications, or
9784
+ for any such Derivative Works as a whole, provided Your use,
9785
+ reproduction, and distribution of the Work otherwise complies with
9786
+ the conditions stated in this License.
9787
+
9788
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
9789
+ any Contribution intentionally submitted for inclusion in the Work
9790
+ by You to the Licensor shall be under the terms and conditions of
9791
+ this License, without any additional terms or conditions.
9792
+ Notwithstanding the above, nothing herein shall supersede or modify
9793
+ the terms of any separate license agreement you may have executed
9794
+ with Licensor regarding such Contributions.
9795
+
9796
+ 6. Trademarks. This License does not grant permission to use the trade
9797
+ names, trademarks, service marks, or product names of the Licensor,
9798
+ except as required for reasonable and customary use in describing the
9799
+ origin of the Work and reproducing the content of the NOTICE file.
9800
+
9801
+ 7. Disclaimer of Warranty. Unless required by applicable law or
9802
+ agreed to in writing, Licensor provides the Work (and each
9803
+ Contributor provides its Contributions) on an "AS IS" BASIS,
9804
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
9805
+ implied, including, without limitation, any warranties or conditions
9806
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
9807
+ PARTICULAR PURPOSE. You are solely responsible for determining the
9808
+ appropriateness of using or redistributing the Work and assume any
9809
+ risks associated with Your exercise of permissions under this License.
9810
+
9811
+ 8. Limitation of Liability. In no event and under no legal theory,
9812
+ whether in tort (including negligence), contract, or otherwise,
9813
+ unless required by applicable law (such as deliberate and grossly
9814
+ negligent acts) or agreed to in writing, shall any Contributor be
9815
+ liable to You for damages, including any direct, indirect, special,
9816
+ incidental, or consequential damages of any character arising as a
9817
+ result of this License or out of the use or inability to use the
9818
+ Work (including but not limited to damages for loss of goodwill,
9819
+ work stoppage, computer failure or malfunction, or any and all
9820
+ other commercial damages or losses), even if such Contributor
9821
+ has been advised of the possibility of such damages.
9822
+
9823
+ 9. Accepting Warranty or Additional Liability. While redistributing
9824
+ the Work or Derivative Works thereof, You may choose to offer,
9825
+ and charge a fee for, acceptance of support, warranty, indemnity,
9826
+ or other liability obligations and/or rights consistent with this
9827
+ License. However, in accepting such obligations, You may act only
9828
+ on Your own behalf and on Your sole responsibility, not on behalf
9829
+ of any other Contributor, and only if You agree to indemnify,
9830
+ defend, and hold each Contributor harmless for any liability
9831
+ incurred by, or claims asserted against, such Contributor by reason
9832
+ of your accepting any such warranty or additional liability.
9833
+
9834
+ END OF TERMS AND CONDITIONS
9835
+
9836
+ APPENDIX: How to apply the Apache License to your work.
9837
+
9838
+ To apply the Apache License to your work, attach the following
9839
+ boilerplate notice, with the fields enclosed by brackets "[]"
9840
+ replaced with your own identifying information. (Don't include
9841
+ the brackets!) The text should be enclosed in the appropriate
9842
+ comment syntax for the file format. We also recommend that a
9843
+ file or class name and description of purpose be included on the
9844
+ same "printed page" as the copyright notice for easier
9845
+ identification within third-party archives.
9846
+
9847
+ Copyright [yyyy] [name of copyright owner]
9848
+
9849
+ Licensed under the Apache License, Version 2.0 (the "License");
9850
+ you may not use this file except in compliance with the License.
9851
+ You may obtain a copy of the License at
9852
+
9853
+ http://www.apache.org/licenses/LICENSE-2.0
9854
+
9855
+ Unless required by applicable law or agreed to in writing, software
9856
+ distributed under the License is distributed on an "AS IS" BASIS,
9857
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9858
+ See the License for the specific language governing permissions and
9859
+ limitations under the License.
9860
+ `;
9861
+ REDISTRIBUTABLE = /* @__PURE__ */ new Map([
9862
+ ["OFL-1.1", { title: "SIL Open Font License 1.1", file: "LICENSE-OFL-1.1.txt", text: OFL_1_1_TEXT }],
9863
+ ["Apache-2.0", { title: "Apache License 2.0", file: "LICENSE-Apache-2.0.txt", text: APACHE_2_0_TEXT }],
9864
+ ["UFL-1.0", { title: "Ubuntu Font Licence 1.0", file: "LICENSE-UFL-1.0.txt", text: UFL_1_0_TEXT }]
9865
+ ]);
9094
9866
  }
9095
9867
  });
9096
9868
 
@@ -9120,8 +9892,8 @@ __export(fonts_exports, {
9120
9892
  runFontsResolveSet: () => runFontsResolveSet,
9121
9893
  runFontsStatus: () => runFontsStatus
9122
9894
  });
9123
- import { existsSync as existsSync24, readFileSync as readFileSync21 } from "node:fs";
9124
- import path30 from "node:path";
9895
+ import { existsSync as existsSync25, readFileSync as readFileSync21 } from "node:fs";
9896
+ import path31 from "node:path";
9125
9897
  async function runFontsResolve(opts) {
9126
9898
  const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
9127
9899
  emitData(opts, result, () => {
@@ -9136,7 +9908,7 @@ async function runFontsResolve(opts) {
9136
9908
  }
9137
9909
  }
9138
9910
  async function runFontsResolveSet(opts) {
9139
- const setDir = path30.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
9911
+ const setDir = path31.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
9140
9912
  let needs = [];
9141
9913
  try {
9142
9914
  needs = recordedFontNeeds(setDir);
@@ -9144,57 +9916,81 @@ async function runFontsResolveSet(opts) {
9144
9916
  fail(opts, ExitCode.InputValidation, {
9145
9917
  error: `cannot read font needs from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
9146
9918
  code: "font-needs-unreadable",
9147
- remediation: 'Point --set at a directory recorded with `tendril record` (it carries recording-set.json), or resolve a family explicitly: tendril fonts resolve "<Family>" --weights 400 500 600.'
9919
+ remediation: `Point --set at a directory recorded with \`${tendrilCommand("record plan")}\` (it carries recording-set.json), or resolve a family explicitly: ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}.`
9148
9920
  });
9149
9921
  }
9150
9922
  if (needs.length === 0) {
9151
9923
  fail(opts, ExitCode.InputValidation, {
9152
9924
  error: `recording set ${setDir} declares no detectable font families`,
9153
9925
  code: "font-needs-empty",
9154
- remediation: 'Resolve the family the design uses explicitly: tendril fonts resolve "<Family>" --weights 400 500 600.'
9926
+ remediation: `Resolve the family the design uses explicitly: ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}.`
9155
9927
  });
9156
9928
  }
9157
9929
  const inCache = new Set(resolvedFontFamilies(opts.cacheDir).flatMap((f) => f.weights.map((w) => `${f.family.toLowerCase()}:${w}`)));
9158
- const cached = [];
9930
+ const unstamped = new Map(facesMissingLicense(opts.cacheDir).map((f) => [`${f.family.toLowerCase()}:${f.weight}`, f.sha256]));
9931
+ const cached2 = [];
9159
9932
  const resolved = [];
9160
9933
  const failures = [];
9934
+ const relicensed = [];
9935
+ const unlicensed = [];
9936
+ const byteDrift = [];
9161
9937
  for (const need of needs) {
9162
9938
  const missing = [];
9939
+ const toStamp = [];
9163
9940
  for (const w of need.weights) {
9164
- if (inCache.has(`${need.family.toLowerCase()}:${w}`)) cached.push({ family: need.family, weight: w });
9165
- else missing.push(w);
9941
+ const key = `${need.family.toLowerCase()}:${w}`;
9942
+ if (!inCache.has(key)) missing.push(w);
9943
+ else if (unstamped.has(key)) toStamp.push(w);
9944
+ else cached2.push({ family: need.family, weight: w });
9945
+ }
9946
+ if (missing.length > 0) {
9947
+ const result = await resolveFonts(need.family, missing, opts.cacheDir);
9948
+ resolved.push(...result.resolved);
9949
+ failures.push(...result.failures);
9950
+ }
9951
+ if (toStamp.length > 0) {
9952
+ const result = await resolveFonts(need.family, toStamp, opts.cacheDir);
9953
+ for (const face of result.resolved) {
9954
+ if (face.license === "unknown") unlicensed.push({ family: face.family, weight: face.weight, reason: "the foundry's metadata named no licence for this family" });
9955
+ else relicensed.push({ family: face.family, weight: face.weight, license: face.license });
9956
+ if (unstamped.get(`${face.family.toLowerCase()}:${face.weight}`) !== face.sha256) byteDrift.push({ family: face.family, weight: face.weight });
9957
+ }
9958
+ unlicensed.push(...result.failures);
9166
9959
  }
9167
- if (missing.length === 0) continue;
9168
- const result = await resolveFonts(need.family, missing, opts.cacheDir);
9169
- resolved.push(...result.resolved);
9170
- failures.push(...result.failures);
9171
9960
  }
9172
- emitData(opts, { set: setDir, needs, cached, resolved, failures }, () => {
9961
+ emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures }, () => {
9173
9962
  process.stdout.write(`set declares: ${needs.map((n) => `${n.family} (${n.weights.join(", ")})`).join(" \xB7 ")}
9174
9963
  `);
9175
- for (const f of cached) process.stdout.write(`cached ${f.family} ${f.weight}
9964
+ for (const f of cached2) process.stdout.write(`cached ${f.family} ${f.weight}
9176
9965
  `);
9177
9966
  for (const f of resolved) process.stdout.write(`resolved ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
9967
+ `);
9968
+ for (const f of relicensed) process.stdout.write(`licence ${f.family} ${f.weight}: ${f.license} \u2014 recorded now; bundles may ship these bytes with the licence text
9969
+ `);
9970
+ for (const f of unlicensed) process.stdout.write(`licence ${f.family} ${f.weight}: still not on record (${f.reason}) \u2014 the cached face renders as always; bundles declare it instead of copying it
9178
9971
  `);
9179
9972
  for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
9180
9973
  `);
9181
9974
  });
9975
+ if (byteDrift.length > 0) {
9976
+ 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`);
9977
+ }
9182
9978
  if (failures.length > 0) {
9183
9979
  warn(opts, `${failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
9184
9980
  process.exitCode = ExitCode.FontsUnproven;
9185
9981
  }
9186
9982
  }
9187
9983
  function runFontsStatus(opts) {
9188
- const manifestPath2 = path30.join(opts.cacheDir, "manifest.json");
9189
- if (!existsSync24(manifestPath2)) {
9984
+ const manifestPath2 = path31.join(opts.cacheDir, "manifest.json");
9985
+ if (!existsSync25(manifestPath2)) {
9190
9986
  fail(opts, ExitCode.FontsUnproven, {
9191
9987
  error: `no font cache at ${opts.cacheDir}`,
9192
9988
  code: "fonts-unresolved",
9193
- remediation: 'Run `tendril fonts resolve --set <recording-dir>` (or `tendril fonts resolve "<Family>" --weights 400 500 600`) first.'
9989
+ remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
9194
9990
  });
9195
9991
  }
9196
9992
  const faces = JSON.parse(readFileSync21(manifestPath2, "utf8"));
9197
- const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path30.resolve(opts.lock), opts.cacheDir) : null;
9993
+ const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path31.resolve(opts.lock), opts.cacheDir) : null;
9198
9994
  emitData(opts, { faces, lockVerdicts }, () => {
9199
9995
  for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
9200
9996
  `);
@@ -9225,7 +10021,10 @@ function runFontsAdd(opts) {
9225
10021
  fail(opts, ExitCode.InputValidation, {
9226
10022
  error: err instanceof Error ? err.message : String(err),
9227
10023
  code: "font-add-failed",
9228
- remediation: 'Pass a font file you are licensed to use: tendril fonts add "S\xF6hne" 400 ./fonts/soehne-400.woff2'
10024
+ // The face file is the user's own licensed asset: this command
10025
+ // can name every argument but that one, so it stays a marked
10026
+ // blank rather than a fabricated path that looks real.
10027
+ remediation: `Pass a font file you are licensed to use, by absolute path: ${tendrilCommand(`fonts add "${opts.family}" ${opts.weight} <absolute-path-to-face.woff2>`)}`
9229
10028
  });
9230
10029
  }
9231
10030
  emitData(opts, face, () => {
@@ -9240,22 +10039,26 @@ var init_fonts = __esm({
9240
10039
  init_src4();
9241
10040
  init_src7();
9242
10041
  init_output();
10042
+ init_invocation();
9243
10043
  }
9244
10044
  });
9245
10045
 
9246
10046
  // packages/cli/src/font-guidance.ts
10047
+ import path32 from "node:path";
9247
10048
  function fontsUnprovenRemediation(setDir) {
9248
- if (setDir !== void 0) {
10049
+ const set = setDir === void 0 ? void 0 : path32.resolve(setDir);
10050
+ if (set !== void 0) {
9249
10051
  try {
9250
- const needs = recordedFontNeeds(setDir);
10052
+ const needs = recordedFontNeeds(set);
9251
10053
  if (needs.length > 0) {
9252
10054
  const families = needs.map((n) => `"${n.family}"`).join(", ");
9253
- return `Run \`tendril fonts resolve --set ${setDir}\` \u2014 the recording declares ${families}; faces land in ${DEFAULT_FONT_CACHE}.`;
10055
+ return `Run \`${tendrilCommand(`fonts resolve --set ${set}`)}\` \u2014 the recording declares ${families}; faces land in ${DEFAULT_FONT_CACHE}.`;
9254
10056
  }
9255
10057
  } catch {
9256
10058
  }
9257
10059
  }
9258
- return `Run \`tendril fonts resolve --set <recording-dir>\` (or \`tendril fonts resolve "<Family>" --weights 400 500 600\`) to populate the font cache \u2014 the cache read is ${DEFAULT_FONT_CACHE}.`;
10060
+ const target = set ?? "<recording-dir>";
10061
+ return `Run \`${tendrilCommand(`fonts resolve --set ${target}`)}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) to populate the font cache \u2014 the cache read is ${DEFAULT_FONT_CACHE}.`;
9259
10062
  }
9260
10063
  function taskFontFamilies(setDir) {
9261
10064
  try {
@@ -9301,18 +10104,27 @@ var init_font_guidance = __esm({
9301
10104
  "use strict";
9302
10105
  init_src4();
9303
10106
  init_src7();
10107
+ init_invocation();
9304
10108
  }
9305
10109
  });
9306
10110
 
9307
10111
  // packages/cli/src/commands/verify.ts
9308
10112
  var verify_exports = {};
9309
10113
  __export(verify_exports, {
10114
+ NO_ROLE_MANIFEST: () => NO_ROLE_MANIFEST,
10115
+ ROLES_NOT_RESOLVED: () => ROLES_NOT_RESOLVED,
10116
+ checkSummarySegments: () => checkSummarySegments,
10117
+ compositionReport: () => compositionReport,
10118
+ eyeCheck: () => eyeCheck,
10119
+ failureTally: () => failureTally,
9310
10120
  foldConfigStatus: () => foldConfigStatus,
9311
10121
  interactionCoverage: () => interactionCoverage,
10122
+ occlusionReport: () => occlusionReport,
10123
+ resolveComposition: () => resolveComposition,
9312
10124
  runVerify: () => runVerify
9313
10125
  });
9314
- import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
9315
- import path31 from "node:path";
10126
+ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
10127
+ import path33 from "node:path";
9316
10128
  function interactionCoverage(behaviors) {
9317
10129
  const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
9318
10130
  return {
@@ -9342,12 +10154,68 @@ function foldConfigStatus(s, failDemotions, substitutedFamilies) {
9342
10154
  absentInkDemoted
9343
10155
  };
9344
10156
  }
10157
+ function resolveComposition(raw) {
10158
+ if (raw === void 0) return NO_ROLE_MANIFEST;
10159
+ const parsed = RolesSchema.safeParse(raw);
10160
+ if (parsed.success) return { roles: parsed.data };
10161
+ const why = parsed.error.issues.slice(0, 3).map((i) => `${i.path.join(".") === "" ? "roles" : i.path.join(".")}: ${i.message}`).join("; ");
10162
+ return {
10163
+ short: "roles block REJECTED",
10164
+ unavailable: `the recording set's roles block was REJECTED by the schema (${why}) \u2014 composition (structural + crop) was NEVER CHECKED`,
10165
+ warn: `recording set roles block REJECTED (${why}) \u2014 the composition checks (structural + crop) did NOT run, while the set still reads as role-confirmed. This is an instrument failure, not a clean bill. Repair the set's roles block and re-verify.`
10166
+ };
10167
+ }
10168
+ function rolesSourceLabel(roles) {
10169
+ return roles.rolesSource === "human-override" ? "HUMAN-OVERRIDE" : roles.rolesSource ?? UNSTAMPED_ROLES;
10170
+ }
10171
+ function checkSummarySegments(input) {
10172
+ const tally = (rows) => `${rows.filter((r) => r.pass).length}/${rows.length}`;
10173
+ const { availability } = input;
10174
+ const composition = availability.unavailable !== void 0 ? `composition NOT CHECKED (${availability.short})` : `composition ${tally(input.structural)} structural, ${input.crops !== void 0 ? `${tally(input.crops)} crops` : "crops unavailable"} (roles: ${rolesSourceLabel(availability.roles)}${(availability.roles.narrowingAccepted ?? []).length > 0 ? `, ${(availability.roles.narrowingAccepted ?? []).length} narrowing(s) accepted: ${(availability.roles.narrowingAccepted ?? []).join(", ")}` : ""})`;
10175
+ const occ = occlusionReport(input.occlusion);
10176
+ const occlusion = "unavailable" in occ ? `occlusion not applicable (${NO_OVERLAY_DECLARED})` : `occlusion ${tally(input.occlusion)}`;
10177
+ return ` \xB7 ${composition} \xB7 ${occlusion}`;
10178
+ }
10179
+ function occlusionReport(occlusion) {
10180
+ if (occlusion.length === 0) {
10181
+ return {
10182
+ unavailable: "the bundle declares no overlay (no open-state config and no prelude popover) \u2014 the sibling-occlusion check DOES NOT APPLY and nothing about overlay stacking was measured; an empty check list is this state, not a clean bill"
10183
+ };
10184
+ }
10185
+ return { checks: occlusion.length, passed: occlusion.filter((o) => o.pass).length };
10186
+ }
10187
+ function compositionReport(input) {
10188
+ const { availability, regions } = input;
10189
+ if (availability.unavailable !== void 0) return { unavailable: availability.unavailable };
10190
+ const cropsUnavailable = regions !== void 0 && "unavailable" in regions ? regions.unavailable : "interior geometry was never computed for this run";
10191
+ return {
10192
+ rolesSource: availability.roles.rolesSource ?? UNSTAMPED_ROLES,
10193
+ // Each token is a derived main, or a part→main edge, that a human
10194
+ // signed away at confirm time. Every one of them REMOVES something
10195
+ // the structural check would otherwise have had to satisfy, so the
10196
+ // list belongs beside the pass it enabled — not only in the manifest
10197
+ // that recorded it.
10198
+ narrowingAccepted: availability.roles.narrowingAccepted ?? [],
10199
+ structural: input.structural,
10200
+ crops: input.crops ?? { unavailable: cropsUnavailable }
10201
+ };
10202
+ }
10203
+ function eyeCheck(bundleDir) {
10204
+ return {
10205
+ command: tendrilCommand(`inspect "${bundleDir}"`),
10206
+ sheetPath: path33.join(bundleDir, "verify-evidence", "inspect.html"),
10207
+ 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."
10208
+ };
10209
+ }
10210
+ function failureTally(t) {
10211
+ return `${t.configs.length} config(s), ${t.behaviors.length} behavior(s), ${t.structural.length + t.crops.length} composition check(s), ${t.occlusion.length} occlusion check(s) below the ${t.bar} bar${t.evidenceUnverified ? "; PLUS the interaction-evidence instrument gate failed (operability could not be verified \u2014 see FAIL interaction-evidence above)" : ""} \u2014 bundle written, verdict honest (Q4)`;
10212
+ }
9345
10213
  function taskFromManifest(opts, manifest, setDir) {
9346
10214
  const recordedSlugs = loadManifest(setDir).reps.map((r) => r.slug);
9347
10215
  const adapterSlugs = Object.keys(manifest.propAdapter);
9348
10216
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
9349
10217
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
9350
- const registry = Object.values(TASKS).find((t) => path31.resolve(t.set) === path31.resolve(setDir));
10218
+ const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
9351
10219
  const authored = (() => {
9352
10220
  if (registry !== void 0) return void 0;
9353
10221
  try {
@@ -9356,7 +10224,7 @@ function taskFromManifest(opts, manifest, setDir) {
9356
10224
  fail(opts, ExitCode.InputValidation, {
9357
10225
  error: `cannot author the verification task from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
9358
10226
  code: "AUTHORING_FAILED",
9359
- remediation: "Fix the recording set (duplicate/colliding poses fail authoring by design \u2014 check `tendril record status` and the set's variant names)."
10227
+ remediation: `Fix the recording set (duplicate/colliding poses fail authoring by design \u2014 check \`${tendrilCommand(`record status --set ${setDir}`)}\` and the set's variant names).`
9360
10228
  });
9361
10229
  }
9362
10230
  })();
@@ -9379,18 +10247,18 @@ function taskFromManifest(opts, manifest, setDir) {
9379
10247
  }
9380
10248
  async function runVerify(opts) {
9381
10249
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
9382
- const setOverride = opts.set !== void 0 ? path31.resolve(callerCwd, opts.set) : void 0;
9383
- opts = { ...opts, bundleDir: path31.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
9384
- if (!existsSync25(opts.bundleDir)) {
10250
+ const setOverride = opts.set !== void 0 ? path33.resolve(callerCwd, opts.set) : void 0;
10251
+ opts = { ...opts, bundleDir: path33.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
10252
+ if (!existsSync26(opts.bundleDir)) {
9385
10253
  fail(opts, ExitCode.InputValidation, {
9386
10254
  error: `bundle directory not found: ${opts.bundleDir}`,
9387
10255
  code: "bundle-missing",
9388
10256
  remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
9389
10257
  });
9390
10258
  }
9391
- const manifestPath2 = path31.join(opts.bundleDir, "component.json");
10259
+ const manifestPath2 = path33.join(opts.bundleDir, "component.json");
9392
10260
  let manifest;
9393
- if (existsSync25(manifestPath2)) {
10261
+ if (existsSync26(manifestPath2)) {
9394
10262
  const { manifest: parsed, issues } = readBundleManifest(readFileSync22(manifestPath2, "utf8"));
9395
10263
  if (issues.length > 0) {
9396
10264
  fail(opts, ExitCode.InputValidation, {
@@ -9405,7 +10273,7 @@ async function runVerify(opts) {
9405
10273
  let unmapped = [];
9406
10274
  let interactionEvidence = [];
9407
10275
  let unmappedInteractionEvidence = [];
9408
- let roles;
10276
+ let availability = ROLES_NOT_RESOLVED;
9409
10277
  if (opts.task !== void 0) {
9410
10278
  const registry = TASKS[opts.task];
9411
10279
  if (registry === void 0) {
@@ -9418,21 +10286,21 @@ async function runVerify(opts) {
9418
10286
  task = registry;
9419
10287
  } else if (manifest !== void 0) {
9420
10288
  const resolveSetDir = (p) => {
9421
- if (path31.isAbsolute(p)) return p;
9422
- const fromRepo = path31.resolve(REPO_ROOT, p);
9423
- if (existsSync25(fromRepo)) return fromRepo;
9424
- return path31.resolve(callerCwd, p);
10289
+ if (path33.isAbsolute(p)) return p;
10290
+ const fromRepo = path33.resolve(REPO_ROOT, p);
10291
+ if (existsSync26(fromRepo)) return fromRepo;
10292
+ return path33.resolve(callerCwd, p);
9425
10293
  };
9426
10294
  const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
9427
- if (!existsSync25(path31.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path31.resolve(t.set) === path31.resolve(setDir))) {
10295
+ if (!existsSync26(path33.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path33.resolve(t.set) === path33.resolve(setDir))) {
9428
10296
  fail(opts, ExitCode.RecordingIncomplete, {
9429
10297
  error: `recording set not found or unmanifested: ${setDir}`,
9430
10298
  code: "recording-set-missing",
9431
10299
  remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
9432
10300
  });
9433
10301
  }
9434
- const registry = Object.values(TASKS).find((t) => path31.resolve(t.set) === path31.resolve(setDir));
9435
- if (registry !== void 0 && !existsSync25(path31.join(setDir, "recording-set.json"))) {
10302
+ const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
10303
+ if (registry !== void 0 && !existsSync26(path33.join(setDir, "recording-set.json"))) {
9436
10304
  const recordedSlugs = registry.configs.map((c) => c.rep);
9437
10305
  const adapterSlugs = Object.keys(manifest.propAdapter);
9438
10306
  unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
@@ -9448,16 +10316,16 @@ async function runVerify(opts) {
9448
10316
  interactionEvidence = built.interactionEvidence;
9449
10317
  unmappedInteractionEvidence = built.unmappedInteractionEvidence;
9450
10318
  for (const s of built.adapterOnly) warn(opts, `prop adapter maps "${s}" which is not in the recording set \u2014 ignored`);
9451
- const rolesParse = RolesSchema.safeParse(loadManifest(setDir).roles);
9452
- if (rolesParse.success) roles = rolesParse.data;
10319
+ availability = resolveComposition(loadManifest(setDir).roles);
10320
+ if (availability.warn !== void 0) warn(opts, availability.warn);
9453
10321
  }
9454
10322
  const hash = recordingSetHash(setDir, task.configs);
9455
10323
  if (hash !== manifest.provenance.recordingSet.hash) {
9456
10324
  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`);
9457
10325
  }
9458
10326
  for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
9459
- const p = path31.join(opts.bundleDir, name);
9460
- if (!existsSync25(p)) continue;
10327
+ const p = path33.join(opts.bundleDir, name);
10328
+ if (!existsSync26(p)) continue;
9461
10329
  const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync22(p)));
9462
10330
  if (issues.length > 0) {
9463
10331
  fail(opts, ExitCode.InputValidation, {
@@ -9474,6 +10342,7 @@ async function runVerify(opts) {
9474
10342
  remediation: `Regenerate the bundle (bundle v1 carries its prop manifest), or pass --task <${Object.keys(TASKS).join("|")}> for legacy bundles.`
9475
10343
  });
9476
10344
  }
10345
+ const roles = availability.roles;
9477
10346
  if (!fontsResolved()) {
9478
10347
  fail(opts, ExitCode.FontsUnproven, {
9479
10348
  error: "resolved font cache absent \u2014 verification under system font substitution is refused",
@@ -9481,8 +10350,8 @@ async function runVerify(opts) {
9481
10350
  remediation: fontsUnprovenRemediation(task.set)
9482
10351
  });
9483
10352
  }
9484
- if (lcdTextDisabled()) {
9485
- warn(opts, "EXPERIMENT override active (TENDRIL_DISABLE_LCD_TEXT=1): text renders greyscale \u2014 scores are NOT comparable to default runs; the environment stamp and report are marked");
10353
+ if (lcdTextEnabled()) {
10354
+ warn(opts, "EXPERIMENT override active (TENDRIL_ENABLE_LCD_TEXT=1): text renders with platform LCD antialiasing instead of canonical greyscale \u2014 scores are NOT comparable to default runs; the environment stamp and report are marked");
9486
10355
  }
9487
10356
  const substitutedFamilies = unprovisionedFamilies(task.set);
9488
10357
  if (substitutedFamilies.length > 0) {
@@ -9495,7 +10364,7 @@ async function runVerify(opts) {
9495
10364
  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)`);
9496
10365
  }
9497
10366
  const missing = task.configs.filter(
9498
- (c) => !existsSync25(path31.join(task.set, c.rep, "get_screenshot.json")) || !existsSync25(path31.join(task.set, c.rep, "get_metadata.json"))
10367
+ (c) => !existsSync26(path33.join(task.set, c.rep, "get_screenshot.json")) || !existsSync26(path33.join(task.set, c.rep, "get_metadata.json"))
9499
10368
  );
9500
10369
  if (missing.length > 0) {
9501
10370
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -9505,10 +10374,10 @@ async function runVerify(opts) {
9505
10374
  });
9506
10375
  }
9507
10376
  const bar = BARS2[opts.bar];
9508
- const evidenceDir = path31.join(opts.bundleDir, "verify-evidence");
10377
+ const evidenceDir = path33.join(opts.bundleDir, "verify-evidence");
9509
10378
  const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
9510
10379
  const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
9511
- const bundleCss = ["tokens.css", "styles.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f)).map((f) => readFileSync22(f, "utf8")).join("\n");
10380
+ const bundleCss = ["tokens.css", "styles.css"].map((f) => path33.join(opts.bundleDir, f)).filter((f) => existsSync26(f)).map((f) => readFileSync22(f, "utf8")).join("\n");
9512
10381
  const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
9513
10382
  const parity = await checkHoverParity(task, opts.bundleDir);
9514
10383
  const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
@@ -9548,7 +10417,10 @@ async function runVerify(opts) {
9548
10417
  ...opts.task !== void 0 ? { task: opts.task } : {},
9549
10418
  ...manifest !== void 0 ? { bundleManifest: { name: manifest.name, bundleVersion: manifest.bundleVersion, recordingSetHash: manifest.provenance.recordingSet.hash } } : {},
9550
10419
  targetBar: opts.bar,
9551
- environment: environmentStamp(taskFontFamilies(task.set)),
10420
+ // Canonical mount semantics are VERSION-scoped (0.1.22 review: the
10421
+ // greyscale flip changed Windows pixels under an identical-looking
10422
+ // stamp) — the ruler version is part of the comparability key.
10423
+ environment: { ...environmentStamp(taskFontFamilies(task.set)), ruler: cliVersion() },
9552
10424
  coverage: {
9553
10425
  scoredConfigs: statuses.length,
9554
10426
  certified,
@@ -9590,31 +10462,22 @@ async function runVerify(opts) {
9590
10462
  // silently inherited neither.
9591
10463
  quality: { findings: quality.findings, tokensAbsent: quality.tokensAbsent },
9592
10464
  occlusion,
10465
+ // The rows' STATE, which the rows alone cannot carry (see
10466
+ // occlusionReport): the agent channel had no counterpart to the
10467
+ // human line's "occlusion not applicable (no overlay declared)".
10468
+ occlusionCheck: occlusionReport(occlusion),
9593
10469
  configs: statuses,
9594
10470
  behaviors,
9595
10471
  evidence: { dir: evidenceDir, files: "per config: <rep>-render.png, <rep>-ref.png, <rep>-diff.png" },
9596
- ...roles !== void 0 ? {
9597
- composition: {
9598
- structural,
9599
- crops: crops ?? { unavailable: regionsOut !== void 0 && "unavailable" in regionsOut ? regionsOut.unavailable : "no role manifest" }
9600
- }
9601
- } : {},
10472
+ composition: compositionReport({ availability, structural, crops, regions: regionsOut }),
9602
10473
  verdict: ok ? "verified" : "verification-failed",
9603
10474
  // Machine-readable cause (adversarial review): a cert-bar failure
9604
10475
  // with zero pixel/behavior/composition failures was only
9605
10476
  // explainable from stderr prose.
9606
10477
  ...certBlockedByAbsentInk.length > 0 ? { certBlockedByAbsentInk } : {},
9607
10478
  ...weightGaps.length > 0 ? { fontWeightGaps: weightGaps } : {},
9608
- ...lcdTextDisabled() ? { environmentOverrides: ["disable-lcd-text"] } : {},
9609
- // Run 13 finding 1: the eye-check nudge lived only in the human
9610
- // footer, so MCP-driven agents — the documented path — were never
9611
- // told the check exists and shipped without ever opening a sheet.
9612
- // Structured here so every consumer sees it.
9613
- eyeCheck: {
9614
- command: `tendril inspect "${opts.bundleDir}"`,
9615
- sheetPath: path31.join(opts.bundleDir, "verify-evidence", "inspect.html"),
9616
- 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."
9617
- }
10479
+ ...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
10480
+ eyeCheck: eyeCheck(opts.bundleDir)
9618
10481
  };
9619
10482
  emitData(opts, report, () => {
9620
10483
  for (const s of statuses) {
@@ -9636,17 +10499,22 @@ async function runVerify(opts) {
9636
10499
  process.stdout.write(`${o.pass ? "PASS" : "FAIL"} ${o.id}${o.detail !== void 0 ? ` [${o.detail}]` : ""}
9637
10500
  `);
9638
10501
  }
9639
- if (crops !== void 0) {
10502
+ if (availability.unavailable !== void 0) {
10503
+ process.stdout.write(`UNAVAILABLE composition \u2014 ${availability.unavailable}
10504
+ `);
10505
+ } else if (crops !== void 0) {
9640
10506
  for (const c of crops) process.stdout.write(`${c.pass ? "PASS" : "FAIL"} ${c.id} sim=${c.similarity} ink=${c.inkRecall}${c.detail !== void 0 ? ` [${c.detail}]` : ""}
9641
10507
  `);
9642
- } else if (roles !== void 0 && regionsOut !== void 0 && "unavailable" in regionsOut) {
10508
+ } else if (regionsOut !== void 0 && "unavailable" in regionsOut) {
9643
10509
  process.stdout.write(`UNAVAILABLE composition crops \u2014 ${regionsOut.unavailable}
9644
10510
  `);
9645
10511
  }
9646
10512
  const ic = interactionCoverage(behaviors);
9647
10513
  process.stdout.write(
9648
10514
  `
9649
- ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuses.length} \u2265 pass bar \xB7 behaviors ${behaviors.length - behaviorFailures.length}/${behaviors.length}${roles !== void 0 ? ` \xB7 composition ${structural.length - structuralFailures.length}/${structural.length} structural${crops !== void 0 ? `, ${crops.length - cropFailures.length}/${crops.length} crops` : ", crops unavailable"}` : ""}
10515
+ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuses.length} \u2265 pass bar \xB7 behaviors ${behaviors.length - behaviorFailures.length}/${behaviors.length}${checkSummarySegments(
10516
+ { availability, structural, crops, occlusion }
10517
+ )}
9650
10518
  `
9651
10519
  );
9652
10520
  const QUALITY_SHOWN = 8;
@@ -9703,18 +10571,20 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
9703
10571
  }
9704
10572
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
9705
10573
  `);
9706
- const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f)).map((f) => readFileSync22(f, "utf8")).join("\n")));
10574
+ 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")));
9707
10575
  if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
9708
10576
  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)
9709
10577
  `);
9710
10578
  }
9711
10579
  for (const g of weightGaps) {
9712
- process.stdout.write(`fonts (weight gap): "${g.family}" declares ${g.declared.join(", ")} \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (advisory, never a gate). Fix: tendril fonts resolve --set ${task.set}
9713
- `);
10580
+ process.stdout.write(
10581
+ `fonts (weight gap): "${g.family}" declares ${g.declared.join(", ")} \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (advisory, never a gate). Fix: ${tendrilCommand(`fonts resolve --set ${task.set}`)}
10582
+ `
10583
+ );
9714
10584
  }
9715
10585
  process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
9716
10586
  `);
9717
- process.stdout.write(`eye check: tendril inspect "${opts.bundleDir}" \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape (re-run after every verify)
10587
+ process.stdout.write(`eye check: ${report.eyeCheck.command} \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape (re-run after every verify)
9718
10588
  `);
9719
10589
  });
9720
10590
  if (opts.bar === "cert" && substitutedFamilies.length > 0) {
@@ -9741,12 +10611,20 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
9741
10611
  if (!okExceptDemotion) {
9742
10612
  warn(
9743
10613
  opts,
9744
- `${pixelFailures.length} config(s), ${behaviorFailures.length} behavior(s), ${structuralFailures.length + cropFailures.length + occlusionFailures.length} composition check(s) below the ${opts.bar} bar${evidenceUnverified ? "; PLUS the interaction-evidence instrument gate failed (operability could not be verified \u2014 see FAIL interaction-evidence above)" : ""} \u2014 bundle written, verdict honest (Q4)`
10614
+ failureTally({
10615
+ configs: pixelFailures,
10616
+ behaviors: behaviorFailures,
10617
+ structural: structuralFailures,
10618
+ crops: cropFailures,
10619
+ occlusion: occlusionFailures,
10620
+ bar: opts.bar,
10621
+ evidenceUnverified
10622
+ })
9745
10623
  );
9746
10624
  process.exitCode = ExitCode.VerificationFailed;
9747
10625
  }
9748
10626
  }
9749
- var BARS2;
10627
+ var BARS2, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES, NO_OVERLAY_DECLARED;
9750
10628
  var init_verify = __esm({
9751
10629
  "packages/cli/src/commands/verify.ts"() {
9752
10630
  "use strict";
@@ -9757,11 +10635,22 @@ var init_verify = __esm({
9757
10635
  init_src4();
9758
10636
  init_environment();
9759
10637
  init_font_guidance();
10638
+ init_invocation();
9760
10639
  init_output();
9761
10640
  BARS2 = {
9762
10641
  pass: { sim: 0.95, ink: 0.95 },
9763
10642
  cert: { sim: 0.97, ink: 0.95 }
9764
10643
  };
10644
+ NO_ROLE_MANIFEST = Object.freeze({
10645
+ short: "no role manifest",
10646
+ unavailable: "the recording set declares no role manifest \u2014 composition (structural + crop) was NEVER CHECKED; nothing here says the main renders the SHIPPED part modules rather than a pixel-identical re-implementation"
10647
+ });
10648
+ ROLES_NOT_RESOLVED = Object.freeze({
10649
+ short: "roles never resolved on this path",
10650
+ unavailable: "this run never resolved a role manifest \u2014 the legacy --task adapter and pre-manifest reference sets skip the set's roles block entirely, so composition (structural + crop) was NEVER CHECKED and the set was never asked what it declares; nothing here says the main renders the SHIPPED part modules rather than a pixel-identical re-implementation"
10651
+ });
10652
+ UNSTAMPED_ROLES = "unstamped";
10653
+ NO_OVERLAY_DECLARED = "no overlay declared";
9765
10654
  }
9766
10655
  });
9767
10656
 
@@ -9771,36 +10660,36 @@ __export(engine_exports, {
9771
10660
  runEngineBrief: () => runEngineBrief,
9772
10661
  runEngineScore: () => runEngineScore
9773
10662
  });
9774
- import { appendFileSync, existsSync as existsSync26, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
9775
- import path32 from "node:path";
10663
+ import { appendFileSync, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
10664
+ import path34 from "node:path";
9776
10665
  function resolveEngineTask(opts, callerCwd) {
9777
- const asPath = path32.resolve(callerCwd, opts.taskOrSet);
9778
- const isSet = existsSync26(path32.join(asPath, "recording-set.json"));
10666
+ const asPath = path34.resolve(callerCwd, opts.taskOrSet);
10667
+ const isSet = existsSync27(path34.join(asPath, "recording-set.json"));
9779
10668
  const registry = TASKS[opts.taskOrSet];
9780
- if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, disclosures: [], interactionEvidence: [] };
10669
+ if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: [] };
9781
10670
  if (isSet) {
9782
10671
  try {
9783
10672
  const authored = authorTaskFromSet(asPath);
9784
10673
  for (const d of authored.disclosures) warn(opts, d);
9785
- return { task: authored.task, name: path32.basename(asPath), disclosures: authored.disclosures, interactionEvidence: authored.api.interactionEvidence, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
10674
+ 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 } };
9786
10675
  } catch (err) {
9787
10676
  fail(opts, ExitCode.InputValidation, {
9788
10677
  error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
9789
10678
  code: "AUTHORING_FAILED",
9790
- remediation: "Complete the recording set (tendril record status) or use a reference task name."
10679
+ remediation: `Complete the recording set (${tendrilCommand(`record status --set ${asPath}`)}) or use a reference task name.`
9791
10680
  });
9792
10681
  }
9793
10682
  }
9794
10683
  fail(opts, ExitCode.InputValidation, {
9795
10684
  error: `"${opts.taskOrSet}" is neither a reference task nor a recorded set`,
9796
10685
  code: "UNKNOWN_RECORDED_INPUT",
9797
- remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with \`tendril record\`.`
10686
+ remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with \`${tendrilCommand("record plan")}\`.`
9798
10687
  });
9799
10688
  }
9800
10689
  function runEngineBrief(opts) {
9801
10690
  requireEntitlement(opts);
9802
10691
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
9803
- const { task, name, disclosures } = resolveEngineTask(opts, callerCwd);
10692
+ const { task, name, ref, disclosures } = resolveEngineTask(opts, callerCwd);
9804
10693
  const bar = BARS3[opts.bar];
9805
10694
  const disclosureBlock = disclosures.length > 0 ? `
9806
10695
 
@@ -9809,8 +10698,8 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
9809
10698
  const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
9810
10699
  const segments = buildSegments(task, "files");
9811
10700
  let notRecorded;
9812
- const manifestPath2 = path32.join(task.set, "recording-set.json");
9813
- if (existsSync26(manifestPath2)) {
10701
+ const manifestPath2 = path34.join(task.set, "recording-set.json");
10702
+ if (existsSync27(manifestPath2)) {
9814
10703
  notRecorded = JSON.parse(readFileSync23(manifestPath2, "utf8")).notRecorded;
9815
10704
  }
9816
10705
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
@@ -9819,7 +10708,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
9819
10708
  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.
9820
10709
  ${notRecorded}` : "";
9821
10710
  let fontProvisioning;
9822
- if (existsSync26(manifestPath2)) {
10711
+ if (existsSync27(manifestPath2)) {
9823
10712
  const missingFams = unprovisionedFamilies(task.set);
9824
10713
  const unprovided = unprovisionedFaces(task.set);
9825
10714
  const weightOnly = missingFams.length === 0;
@@ -9827,16 +10716,17 @@ ${notRecorded}` : "";
9827
10716
  const names = unprovided.map((f) => `'${f}'`).join(", ");
9828
10717
  const PROPRIETARY = /^(sf pro|sf compact|sf mono|new york|pingfang|segoe ui|helvetica neue|proxima nova|avenir)/i;
9829
10718
  const allProprietary = unprovided.every((f) => PROPRIETARY.test(f.trim()));
10719
+ const resolveCommand = tendrilCommand(`fonts resolve --set ${task.set}`);
9830
10720
  fontProvisioning = {
9831
10721
  unprovided,
9832
10722
  question: {
9833
10723
  prompt: weightOnly ? `This design implies font ${unprovided.length === 1 ? "weight" : "weights"} the local kit lacks: ${names} (the ${missingFams.length === 0 && unprovided.length === 1 ? "family itself is" : "families themselves are"} provisioned). How should it be handled?` : `This design's text uses ${names}, which is not in the local font kit. How should it be handled?`,
9834
10724
  options: [
9835
- allProprietary && !weightOnly ? `(Recommended) ${names} ${unprovided.length === 1 ? "is a proprietary face" : "are proprietary faces"} \u2014 Google Fonts cannot serve ${unprovided.length === 1 ? "it" : "them"}, so skip \`fonts resolve\`: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold a licence for (the file never leaves this machine), then re-run this brief \u2014 exact text fidelity.` : `(Recommended) Run \`tendril fonts resolve --set ${task.set}\` \u2014 open-source families are fetched automatically. Any face that fails there is one you license privately: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold (the file never leaves this machine). Then re-run this brief \u2014 exact text fidelity.`,
10725
+ allProprietary && !weightOnly ? `(Recommended) ${names} ${unprovided.length === 1 ? "is a proprietary face" : "are proprietary faces"} \u2014 Google Fonts cannot serve ${unprovided.length === 1 ? "it" : "them"}, so skip \`fonts resolve\`: run \`${tendrilCommand('fonts add "<Family>" <weight> <file>')}\` with the .woff2/.ttf/.otf you hold a licence for (the file never leaves this machine), then re-run this brief \u2014 exact text fidelity.` : `(Recommended) Run \`${resolveCommand}\` \u2014 open-source families are fetched automatically. Any face that fails there is one you license privately: run \`${tendrilCommand('fonts add "<Family>" <weight> <file>')}\` with the .woff2/.ttf/.otf you hold (the file never leaves this machine). Then re-run this brief \u2014 exact text fidelity.`,
9836
10726
  weightOnly ? "Continue with nearest-weight rendering: the family is provisioned, certification remains possible, and the gap rides the report as an advisory (fontWeightGaps); resolving removes it." : 'Continue with a substitute face: generation proceeds in a provided family, no config can score "certified" under it (cert-bar runs exit fonts-unproven after their report), the substitution is disclosed, and small text differences are expected and not fixable from CSS.'
9837
10727
  ]
9838
10728
  },
9839
- nonInteractive: weightOnly ? `Run \`tendril fonts resolve --set ${task.set}\` if quick, or continue \u2014 the weight gap is an advisory, never a gate.` : opts.bar === "cert" ? `Resolve the families first (\`tendril fonts resolve --set ${task.set}\`) \u2014 this loop targets the cert bar, and scoring exits fonts-unproven under a substitute, so continuing without resolving is a dead end.` : "Continue with the substitute and state the substitution in your report."
10729
+ nonInteractive: weightOnly ? `Run \`${resolveCommand}\` if quick, or continue \u2014 the weight gap is an advisory, never a gate.` : opts.bar === "cert" ? `Resolve the families first (\`${resolveCommand}\`) \u2014 this loop targets the cert bar, and scoring exits fonts-unproven under a substitute, so continuing without resolving is a dead end.` : "Continue with the substitute and state the substitution in your report."
9840
10730
  };
9841
10731
  }
9842
10732
  }
@@ -9844,8 +10734,8 @@ ${notRecorded}` : "";
9844
10734
 
9845
10735
  === TASK PAYLOAD (recorded truth, verbatim) ===
9846
10736
  ${segments}`;
9847
- const payloadFile = path32.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
9848
- mkdirSync8(path32.dirname(payloadFile), { recursive: true });
10737
+ const payloadFile = path34.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
10738
+ mkdirSync8(path34.dirname(payloadFile), { recursive: true });
9849
10739
  writeFileSync12(payloadFile, payload);
9850
10740
  emitData(
9851
10741
  opts,
@@ -9874,7 +10764,7 @@ ${segments}`;
9874
10764
  "Settle the proposer model (see modelSelection \u2014 ask the user when one is present), then:",
9875
10765
  `Read ${payloadFile} completely \u2014 it is the system brief plus every recorded config's emission, box, and assets.`,
9876
10766
  `Write the complete bundle files (${task.entry}, styles.css, optional tokens.css) into a candidate directory.`,
9877
- `Run \`tendril engine score ${name} <candidateDir> --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"} --json\` \u2014 the CLI is the sole judge, and it refuses to score an undeclared model.`,
10767
+ `Run \`${tendrilCommand(`engine score ${ref} <candidateDir> --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"} --json`)}\` \u2014 the CLI is the sole judge, and it refuses to score an undeclared model.`,
9878
10768
  "Apply the returned feedback and re-score. Stop when all checks pass or two consecutive scores fail to improve.",
9879
10769
  "Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
9880
10770
  ]
@@ -9887,18 +10777,23 @@ ${segments}`;
9887
10777
  }
9888
10778
  );
9889
10779
  }
10780
+ function appendScoreHistory(candidateDir, entry) {
10781
+ appendFileSync(path34.join(candidateDir, "score-history.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
10782
+ `);
10783
+ }
9890
10784
  async function runEngineScore(opts) {
9891
10785
  requireEntitlement(opts);
9892
10786
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
9893
- const candidateDir = path32.resolve(callerCwd, opts.candidateDir);
10787
+ const candidateDir = path34.resolve(callerCwd, opts.candidateDir);
9894
10788
  const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
9895
- if (!existsSync26(candidateDir)) {
10789
+ if (!existsSync27(candidateDir)) {
9896
10790
  fail(opts, ExitCode.InputValidation, {
9897
10791
  error: `candidate directory not found: ${candidateDir}`,
9898
10792
  code: "candidate-missing",
9899
10793
  remediation: `Write ${task.entry} and styles.css into a directory and pass it.`
9900
10794
  });
9901
10795
  }
10796
+ appendScoreHistory(candidateDir, { event: "round-start", bar: opts.bar });
9902
10797
  if (!fontsResolved()) {
9903
10798
  fail(opts, ExitCode.FontsUnproven, {
9904
10799
  error: "resolved font cache absent \u2014 scoring under system font substitution is refused",
@@ -9906,8 +10801,8 @@ async function runEngineScore(opts) {
9906
10801
  remediation: fontsUnprovenRemediation(task.set)
9907
10802
  });
9908
10803
  }
9909
- if (lcdTextDisabled()) {
9910
- warn(opts, "EXPERIMENT override active (TENDRIL_DISABLE_LCD_TEXT=1): text renders greyscale \u2014 scores are NOT comparable to default runs; the environment stamp and report are marked");
10804
+ if (lcdTextEnabled()) {
10805
+ warn(opts, "EXPERIMENT override active (TENDRIL_ENABLE_LCD_TEXT=1): text renders with platform LCD antialiasing instead of canonical greyscale \u2014 scores are NOT comparable to default runs; the environment stamp and report are marked");
9911
10806
  }
9912
10807
  const substitutedFamilies = unprovisionedFamilies(task.set);
9913
10808
  if (substitutedFamilies.length > 0) {
@@ -9916,10 +10811,10 @@ async function runEngineScore(opts) {
9916
10811
  for (const g of missingWeights(task.set)) {
9917
10812
  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)`);
9918
10813
  }
9919
- if (opts.rebind !== true && existsSync26(path32.join(candidateDir, "component.json"))) {
10814
+ if (opts.rebind !== true && existsSync27(path34.join(candidateDir, "component.json"))) {
9920
10815
  const prior = (() => {
9921
10816
  try {
9922
- const read = readBundleManifest(readFileSync23(path32.join(candidateDir, "component.json"), "utf8"));
10817
+ const read = readBundleManifest(readFileSync23(path34.join(candidateDir, "component.json"), "utf8"));
9923
10818
  return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
9924
10819
  } catch {
9925
10820
  return { unreadable: true };
@@ -9941,7 +10836,7 @@ async function runEngineScore(opts) {
9941
10836
  }
9942
10837
  }
9943
10838
  const bar = BARS3[opts.bar];
9944
- const evidenceDir = path32.join(candidateDir, "verify-evidence");
10839
+ const evidenceDir = path34.join(candidateDir, "verify-evidence");
9945
10840
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
9946
10841
  const parity = await checkHoverParity(task, candidateDir);
9947
10842
  const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
@@ -9984,11 +10879,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
9984
10879
  environment: environmentStamp(taskFontFamilies(task.set)),
9985
10880
  substitutedFamilies
9986
10881
  });
9987
- appendFileSync(
9988
- path32.join(candidateDir, "score-history.jsonl"),
9989
- `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), bar: opts.bar, pass: obj[0], total, certified: certifiedReps.length, floor: obj[1], mean: obj[2] })}
9990
- `
9991
- );
10882
+ appendScoreHistory(candidateDir, { event: "round-scored", bar: opts.bar, pass: obj[0], total, certified: certifiedReps.length, floor: obj[1], mean: obj[2] });
9992
10883
  emitData(
9993
10884
  opts,
9994
10885
  {
@@ -10008,7 +10899,8 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
10008
10899
  bundleManifest: emitted.written[0],
10009
10900
  note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
10010
10901
  allPass,
10011
- ...lcdTextDisabled() ? { environmentOverrides: ["disable-lcd-text"] } : {},
10902
+ environment: { ...environmentStamp(taskFontFamilies(task.set)), ruler: cliVersion() },
10903
+ ...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
10012
10904
  // Run 11: generators read allPass:true and reported success on
10013
10905
  // bundles verify then FAILED on the interaction-evidence gate —
10014
10906
  // the oracle must say what verify will say, including this.
@@ -10062,6 +10954,7 @@ var init_engine2 = __esm({
10062
10954
  init_font_guidance();
10063
10955
  init_src4();
10064
10956
  init_output();
10957
+ init_invocation();
10065
10958
  init_entitlement();
10066
10959
  init_verify();
10067
10960
  init_src4();
@@ -10077,11 +10970,11 @@ var codeconnect_exports = {};
10077
10970
  __export(codeconnect_exports, {
10078
10971
  runCodeConnect: () => runCodeConnect
10079
10972
  });
10080
- import { existsSync as existsSync27, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
10081
- import path33 from "node:path";
10973
+ import { existsSync as existsSync28, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
10974
+ import path35 from "node:path";
10082
10975
  function runCodeConnect(opts) {
10083
10976
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
10084
- const bundleDir = path33.resolve(callerCwd, opts.bundleDir);
10977
+ const bundleDir = path35.resolve(callerCwd, opts.bundleDir);
10085
10978
  let url;
10086
10979
  try {
10087
10980
  url = new URL(opts.figmaUrl);
@@ -10097,7 +10990,7 @@ function runCodeConnect(opts) {
10097
10990
  }
10098
10991
  let manifest;
10099
10992
  try {
10100
- const read = readBundleManifest(readFileSync24(path33.join(bundleDir, "component.json"), "utf8"));
10993
+ const read = readBundleManifest(readFileSync24(path35.join(bundleDir, "component.json"), "utf8"));
10101
10994
  if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
10102
10995
  manifest = read.manifest;
10103
10996
  } catch (err) {
@@ -10107,8 +11000,8 @@ function runCodeConnect(opts) {
10107
11000
  remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
10108
11001
  });
10109
11002
  }
10110
- const setDir = path33.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
10111
- if (!existsSync27(path33.join(setDir, "recording-set.json"))) {
11003
+ const setDir = path35.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
11004
+ if (!existsSync28(path35.join(setDir, "recording-set.json"))) {
10112
11005
  fail(opts, ExitCode.InputValidation, {
10113
11006
  error: `recording set not found at ${setDir}`,
10114
11007
  code: "codeconnect-no-set",
@@ -10122,15 +11015,15 @@ function runCodeConnect(opts) {
10122
11015
  fail(opts, ExitCode.InputValidation, {
10123
11016
  error: `cannot author the API from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
10124
11017
  code: "codeconnect-authoring-failed",
10125
- remediation: "The set must be a complete protocol recording (tendril record status)."
11018
+ remediation: `The set must be a complete protocol recording (${tendrilCommand(`record status --set ${setDir}`)}).`
10126
11019
  });
10127
11020
  }
10128
11021
  const api = authored.api;
10129
11022
  const component = api.component;
10130
11023
  const recManifest = loadManifest(setDir);
10131
11024
  const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
10132
- const meta = path33.join(setDir, r.slug, "get_metadata.json");
10133
- if (!existsSync27(meta)) return void 0;
11025
+ const meta = path35.join(setDir, r.slug, "get_metadata.json");
11026
+ if (!existsSync28(meta)) return void 0;
10134
11027
  try {
10135
11028
  return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync24(meta, "utf8"))))?.[1];
10136
11029
  } catch {
@@ -10149,7 +11042,7 @@ function runCodeConnect(opts) {
10149
11042
  fail(opts, ExitCode.InputValidation, {
10150
11043
  error: "the recording set's content no longer matches the hash this bundle was scored against \u2014 its verification claims describe a different recording",
10151
11044
  code: "codeconnect-set-drift",
10152
- remediation: "Re-run `tendril verify` (or engine score) against the current set, then re-emit."
11045
+ remediation: `Re-run \`${tendrilCommand(`verify ${bundleDir} --set ${setDir}`)}\` (or engine score) against the current set, then re-emit.`
10153
11046
  });
10154
11047
  }
10155
11048
  const boolProps = api.props.filter((p) => p.kind === "boolean");
@@ -10190,7 +11083,7 @@ function runCodeConnect(opts) {
10190
11083
  axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
10191
11084
  fragmentVars.push(varName);
10192
11085
  }
10193
- const entryRel = path33.relative(callerCwd, path33.join(bundleDir, manifest.entry));
11086
+ const entryRel = path35.relative(callerCwd, path35.join(bundleDir, manifest.entry));
10194
11087
  const trust = manifest.trustStatement.split("\n")[0] ?? "";
10195
11088
  const lines = [
10196
11089
  `// url=${opts.figmaUrl}`,
@@ -10211,7 +11104,7 @@ function runCodeConnect(opts) {
10211
11104
  `}`,
10212
11105
  ``
10213
11106
  ].join("\n");
10214
- const outFile = path33.resolve(callerCwd, opts.out ?? path33.join(bundleDir, `${component}.figma.ts`));
11107
+ const outFile = path35.resolve(callerCwd, opts.out ?? path35.join(bundleDir, `${component}.figma.ts`));
10215
11108
  writeFileSync13(outFile, lines);
10216
11109
  emitData(
10217
11110
  opts,
@@ -10242,6 +11135,7 @@ var init_codeconnect = __esm({
10242
11135
  init_src7();
10243
11136
  init_src();
10244
11137
  init_src6();
11138
+ init_invocation();
10245
11139
  init_output();
10246
11140
  kebab4 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
10247
11141
  q = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
@@ -10250,17 +11144,17 @@ var init_codeconnect = __esm({
10250
11144
 
10251
11145
  // packages/mcp/src/server.ts
10252
11146
  import { createHash as createHash5 } from "node:crypto";
10253
- import { existsSync as existsSync28, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "node:fs";
11147
+ import { existsSync as existsSync29, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "node:fs";
10254
11148
  import os6 from "node:os";
10255
- import path34 from "node:path";
10256
- import { fileURLToPath as fileURLToPath5 } from "node:url";
11149
+ import path36 from "node:path";
11150
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
10257
11151
  import { z as z12 } from "zod";
10258
11152
  function sourceHash() {
10259
- const dir = path34.dirname(fileURLToPath5(import.meta.url));
11153
+ const dir = path36.dirname(fileURLToPath6(import.meta.url));
10260
11154
  const h = createHash5("sha256");
10261
11155
  for (const f of readdirSync8(dir).filter((n) => n.endsWith(".ts")).sort()) {
10262
11156
  h.update(f);
10263
- h.update(readFileSync25(path34.join(dir, f)));
11157
+ h.update(readFileSync25(path36.join(dir, f)));
10264
11158
  }
10265
11159
  return h.digest("hex").slice(0, 16);
10266
11160
  }
@@ -10268,10 +11162,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
10268
11162
  var init_server = __esm({
10269
11163
  "packages/mcp/src/server.ts"() {
10270
11164
  "use strict";
10271
- REPO_ROOT3 = path34.resolve(path34.dirname(fileURLToPath5(import.meta.url)), "..", "..", "..");
10272
- CLI_BIN = path34.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
10273
- BUNDLED_CLI = path34.join(path34.dirname(fileURLToPath5(import.meta.url)), "tendril.js");
10274
- CLI_SPAWN = existsSync28(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
11165
+ REPO_ROOT3 = path36.resolve(path36.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
11166
+ CLI_BIN = path36.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
11167
+ BUNDLED_CLI = path36.join(path36.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
11168
+ CLI_SPAWN = existsSync29(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
10275
11169
  str = (d) => z12.string().describe(d);
10276
11170
  optStr = (d) => z12.string().optional().describe(d);
10277
11171
  TOOLS = [
@@ -10301,7 +11195,7 @@ var init_server = __esm({
10301
11195
  const single = i["metadata"];
10302
11196
  const parts = i["metadataParts"];
10303
11197
  if (single !== void 0 || parts !== void 0) {
10304
- const tmp = path34.join(mkdtempSync3(path34.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
11198
+ const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
10305
11199
  if (single !== void 0) {
10306
11200
  writeFileSync14(tmp, single);
10307
11201
  argvOut.push("--metadata-raw-file", tmp);
@@ -10372,7 +11266,7 @@ var init_server = __esm({
10372
11266
  const bridge = (label, single, parts) => {
10373
11267
  if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
10374
11268
  if (single === void 0 && parts === void 0) return;
10375
- const tmp = path34.join(mkdtempSync3(path34.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
11269
+ const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
10376
11270
  if (single !== void 0) {
10377
11271
  writeFileSync14(tmp, single);
10378
11272
  argvOut.push(`--${label}-file`, tmp);
@@ -10415,7 +11309,7 @@ var init_server = __esm({
10415
11309
  const file = i["file"];
10416
11310
  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)");
10417
11311
  if (file !== void 0) return [...base, "--file", file];
10418
- const tmp = path34.join(mkdtempSync3(path34.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
11312
+ const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
10419
11313
  if (text !== void 0) {
10420
11314
  writeFileSync14(tmp, text);
10421
11315
  return [...base, "--file", tmp, "--raw"];
@@ -10570,12 +11464,12 @@ __export(permissions_exports, {
10570
11464
  mergeAllowlist: () => mergeAllowlist,
10571
11465
  runPermissions: () => runPermissions
10572
11466
  });
10573
- import { existsSync as existsSync29, mkdirSync as mkdirSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
11467
+ import { existsSync as existsSync30, mkdirSync as mkdirSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
10574
11468
  import os7 from "node:os";
10575
- import path35 from "node:path";
11469
+ import path37 from "node:path";
10576
11470
  function mergeAllowlist(file, entries) {
10577
11471
  let settings = {};
10578
- if (existsSync29(file) && readFileSync26(file, "utf8").trim() !== "") {
11472
+ if (existsSync30(file) && readFileSync26(file, "utf8").trim() !== "") {
10579
11473
  settings = JSON.parse(readFileSync26(file, "utf8"));
10580
11474
  if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
10581
11475
  }
@@ -10588,7 +11482,7 @@ function mergeAllowlist(file, entries) {
10588
11482
  const alreadyPresent = entries.filter((e) => present.has(e));
10589
11483
  if (added.length > 0) {
10590
11484
  allow.push(...added);
10591
- mkdirSync9(path35.dirname(file), { recursive: true });
11485
+ mkdirSync9(path37.dirname(file), { recursive: true });
10592
11486
  writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
10593
11487
  `);
10594
11488
  }
@@ -10624,7 +11518,7 @@ async function runPermissions(flags) {
10624
11518
  const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
10625
11519
  if (flags.write) {
10626
11520
  const base = process.env["INIT_CWD"] ?? process.cwd();
10627
- const file = flags.user ? path35.join(os7.homedir(), ".claude", "settings.json") : path35.join(base, ".claude", "settings.local.json");
11521
+ const file = flags.user ? path37.join(os7.homedir(), ".claude", "settings.json") : path37.join(base, ".claude", "settings.local.json");
10628
11522
  if (flags.dryRun) {
10629
11523
  emitData(flags, { file, wouldAdd: result.toolEntries }, () => {
10630
11524
  process.stdout.write(`dry-run: would merge ${result.toolEntries.length} per-tool entries into ${file}
@@ -10657,7 +11551,7 @@ async function runPermissions(flags) {
10657
11551
  `Claude Code allowlist for the Tendril pipeline.
10658
11552
  One command installs it (project-local, idempotent):
10659
11553
 
10660
- tendril permissions --claude --write
11554
+ ${tendrilCommand("permissions --claude --write")}
10661
11555
 
10662
11556
  Or paste into .claude/settings.json under permissions.allow:
10663
11557
 
@@ -10682,6 +11576,7 @@ var init_permissions = __esm({
10682
11576
  init_src();
10683
11577
  init_server();
10684
11578
  init_describe();
11579
+ init_invocation();
10685
11580
  init_output();
10686
11581
  init_doctor();
10687
11582
  FIGMA_TOOL_FALLBACK = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_motion_context", "get_figjam"];
@@ -10717,21 +11612,21 @@ __export(inspect_exports, {
10717
11612
  INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
10718
11613
  runInspect: () => runInspect
10719
11614
  });
10720
- import { existsSync as existsSync30, readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
10721
- import path36 from "node:path";
11615
+ import { existsSync as existsSync31, readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
11616
+ import path38 from "node:path";
10722
11617
  async function runInspect(opts) {
10723
11618
  if (opts.describe) {
10724
11619
  printDescription(INSPECT_DESCRIPTION);
10725
11620
  return;
10726
11621
  }
10727
- const bundleDir = path36.resolve(opts.bundleDir);
10728
- const evidenceDir = path36.join(bundleDir, "verify-evidence");
10729
- const manifestPath2 = path36.join(bundleDir, "component.json");
10730
- if (!existsSync30(evidenceDir) || !existsSync30(manifestPath2)) {
11622
+ const bundleDir = path38.resolve(opts.bundleDir);
11623
+ const evidenceDir = path38.join(bundleDir, "verify-evidence");
11624
+ const manifestPath2 = path38.join(bundleDir, "component.json");
11625
+ if (!existsSync31(evidenceDir) || !existsSync31(manifestPath2)) {
10731
11626
  fail(opts, ExitCode.InputValidation, {
10732
- error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync30(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
11627
+ error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync31(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
10733
11628
  code: "no-evidence",
10734
- remediation: "Run `tendril verify <bundleDir>` first \u2014 inspect reads the ref/render evidence that run writes."
11629
+ remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
10735
11630
  });
10736
11631
  }
10737
11632
  const { manifest } = readBundleManifest(readFileSync27(manifestPath2, "utf8"));
@@ -10742,27 +11637,27 @@ async function runInspect(opts) {
10742
11637
  remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
10743
11638
  });
10744
11639
  }
10745
- const setDir = path36.resolve(opts.set ?? manifest.provenance.recordingSet.path);
10746
- const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync30(path36.join(evidenceDir, `${rep}-ref.png`)) && existsSync30(path36.join(evidenceDir, `${rep}-render.png`)));
11640
+ const setDir = path38.resolve(opts.set ?? manifest.provenance.recordingSet.path);
11641
+ const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync31(path38.join(evidenceDir, `${rep}-ref.png`)) && existsSync31(path38.join(evidenceDir, `${rep}-render.png`)));
10747
11642
  if (reps.length === 0) {
10748
11643
  fail(opts, ExitCode.InputValidation, {
10749
11644
  error: "verify-evidence holds no ref/render pairs for this bundle's configs",
10750
11645
  code: "no-evidence",
10751
- remediation: "Run `tendril verify <bundleDir>` first \u2014 inspect reads the ref/render evidence that run writes."
11646
+ remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
10752
11647
  });
10753
11648
  }
10754
11649
  let crops = 0;
10755
11650
  const sections = [];
10756
11651
  for (const rep of reps) {
10757
- const ref = new Uint8Array(readFileSync27(path36.join(evidenceDir, `${rep}-ref.png`)));
10758
- const render = new Uint8Array(readFileSync27(path36.join(evidenceDir, `${rep}-render.png`)));
11652
+ const ref = new Uint8Array(readFileSync27(path38.join(evidenceDir, `${rep}-ref.png`)));
11653
+ const render = new Uint8Array(readFileSync27(path38.join(evidenceDir, `${rep}-render.png`)));
10759
11654
  const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
10760
11655
  const cells = [];
10761
11656
  for (const [i, n] of nodes.entries()) {
10762
11657
  const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
10763
11658
  try {
10764
- writeFileSync16(path36.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
10765
- writeFileSync16(path36.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
11659
+ writeFileSync16(path38.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
11660
+ writeFileSync16(path38.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
10766
11661
  } catch {
10767
11662
  continue;
10768
11663
  }
@@ -10775,7 +11670,7 @@ async function runInspect(opts) {
10775
11670
  `<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>`
10776
11671
  );
10777
11672
  }
10778
- const sheet = path36.join(evidenceDir, "inspect.html");
11673
+ const sheet = path38.join(evidenceDir, "inspect.html");
10779
11674
  writeFileSync16(
10780
11675
  sheet,
10781
11676
  `<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
@@ -10806,6 +11701,7 @@ var init_inspect = __esm({
10806
11701
  init_src6();
10807
11702
  init_src4();
10808
11703
  init_describe();
11704
+ init_invocation();
10809
11705
  init_output();
10810
11706
  INSPECT_DESCRIPTION = {
10811
11707
  name: "inspect",
@@ -10852,17 +11748,17 @@ __export(generate_recorded_exports, {
10852
11748
  runGenerateRecorded: () => runGenerateRecorded
10853
11749
  });
10854
11750
  import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
10855
- import { existsSync as existsSync31, readFileSync as readFileSync28 } from "node:fs";
10856
- import path37 from "node:path";
11751
+ import { existsSync as existsSync32, readFileSync as readFileSync28 } from "node:fs";
11752
+ import path39 from "node:path";
10857
11753
  async function runGenerateRecorded(opts) {
10858
11754
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
10859
- const outDirAbs = path37.resolve(callerCwd, opts.out);
10860
- const recordedAsPath = path37.resolve(callerCwd, opts.recorded);
11755
+ const outDirAbs = path39.resolve(callerCwd, opts.out);
11756
+ const recordedAsPath = path39.resolve(callerCwd, opts.recorded);
10861
11757
  let task;
10862
11758
  let taskName;
10863
11759
  let authoredApi;
10864
11760
  let composition;
10865
- const isSet = existsSync31(path37.join(recordedAsPath, "recording-set.json"));
11761
+ const isSet = existsSync32(path39.join(recordedAsPath, "recording-set.json"));
10866
11762
  const registry = TASKS[opts.recorded];
10867
11763
  if (registry !== void 0 && !isSet) {
10868
11764
  task = registry;
@@ -10871,7 +11767,7 @@ async function runGenerateRecorded(opts) {
10871
11767
  try {
10872
11768
  const authored = authorTaskFromSet(recordedAsPath);
10873
11769
  task = authored.task;
10874
- taskName = path37.basename(recordedAsPath);
11770
+ taskName = path39.basename(recordedAsPath);
10875
11771
  authoredApi = authored.api;
10876
11772
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
10877
11773
  if (roles.success) composition = roles.data;
@@ -10880,14 +11776,14 @@ async function runGenerateRecorded(opts) {
10880
11776
  fail(opts, ExitCode.InputValidation, {
10881
11777
  error: `cannot author a task from ${recordedAsPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
10882
11778
  code: "AUTHORING_FAILED",
10883
- remediation: "Complete the recording set (tendril record status) or use a reference-corpus task name."
11779
+ remediation: `Complete the recording set (${tendrilCommand(`record status --set ${recordedAsPath}`)}) or use a reference-corpus task name.`
10884
11780
  });
10885
11781
  }
10886
11782
  } else {
10887
11783
  fail(opts, ExitCode.InputValidation, {
10888
11784
  error: `"${opts.recorded}" is neither a reference task nor a recorded set (no recording-set.json found)`,
10889
11785
  code: "UNKNOWN_RECORDED_INPUT",
10890
- remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with \`tendril record\`.`
11786
+ remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with \`${tendrilCommand("record plan")}\`.`
10891
11787
  });
10892
11788
  }
10893
11789
  if (!fontsResolved()) {
@@ -10905,13 +11801,13 @@ async function runGenerateRecorded(opts) {
10905
11801
  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)`);
10906
11802
  }
10907
11803
  const missing = task.configs.filter(
10908
- (c) => !existsSync31(path37.join(task.set, c.rep, "get_screenshot.json")) || !existsSync31(path37.join(task.set, c.rep, "get_metadata.json")) || !existsSync31(path37.join(task.set, c.rep, "get_design_context.json"))
11804
+ (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"))
10909
11805
  );
10910
11806
  if (missing.length > 0) {
10911
11807
  fail(opts, ExitCode.RecordingIncomplete, {
10912
11808
  error: `recording set ${task.set} is missing envelopes for ${missing.length} rep(s): ${missing.slice(0, 3).map((c) => c.rep).join(", ")}${missing.length > 3 ? ", \u2026" : ""}`,
10913
11809
  code: "recording-incomplete",
10914
- remediation: "Resume the recording session (`tendril record next`) until `tendril record status` reports the set complete."
11810
+ remediation: `Resume the recording session (\`${tendrilCommand(`record next --set ${task.set}`)}\`) until \`${tendrilCommand(`record status --set ${task.set}`)}\` reports the set complete.`
10915
11811
  });
10916
11812
  }
10917
11813
  const modelId = opts.model ?? DEFAULT_MODEL_CONFIG.curatedDefault ?? DEFAULT_MODEL_CONFIG.bulk;
@@ -10932,7 +11828,7 @@ async function runGenerateRecorded(opts) {
10932
11828
  fail(opts, ExitCode.Auth, {
10933
11829
  error: "OPENROUTER_API_KEY is not configured \u2014 the curated engine needs an OpenRouter-compatible key.",
10934
11830
  code: "MISSING_OPENROUTER_KEY",
10935
- remediation: "Two paths: run `tendril init` to store your OpenRouter key (BYOK \u2014 your key, your inference), or drive the agent-harness engine explicitly with your own agent host."
11831
+ remediation: `Two paths: run \`${tendrilCommand("init")}\` to store your OpenRouter key (BYOK \u2014 your key, your inference), or drive the agent-harness engine explicitly with your own agent host.`
10936
11832
  });
10937
11833
  }
10938
11834
  const bar = BARS4[opts.bar];
@@ -10975,8 +11871,8 @@ async function runGenerateRecorded(opts) {
10975
11871
  ` : `${line}
10976
11872
  `);
10977
11873
  if (opts.dryRun) {
10978
- emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path37.join(outDirAbs, taskName) }, () => {
10979
- process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path37.join(outDirAbs, taskName)})
11874
+ emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path39.join(outDirAbs, taskName) }, () => {
11875
+ process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path39.join(outDirAbs, taskName)})
10980
11876
  `);
10981
11877
  });
10982
11878
  return;
@@ -10999,10 +11895,10 @@ async function runGenerateRecorded(opts) {
10999
11895
  });
11000
11896
  }
11001
11897
  }
11002
- const bundleDir = path37.join(outDirAbs, taskName);
11003
- if (existsSync31(path37.join(bundleDir, "component.json"))) {
11898
+ const bundleDir = path39.join(outDirAbs, taskName);
11899
+ if (existsSync32(path39.join(bundleDir, "component.json"))) {
11004
11900
  try {
11005
- const prior = readBundleManifest(readFileSync28(path37.join(bundleDir, "component.json"), "utf8")).manifest;
11901
+ const prior = readBundleManifest(readFileSync28(path39.join(bundleDir, "component.json"), "utf8")).manifest;
11006
11902
  if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
11007
11903
  fail(opts, ExitCode.InputValidation, {
11008
11904
  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`,
@@ -11139,6 +12035,7 @@ var init_generate_recorded = __esm({
11139
12035
  init_env();
11140
12036
  init_environment();
11141
12037
  init_font_guidance();
12038
+ init_invocation();
11142
12039
  init_output();
11143
12040
  BARS4 = {
11144
12041
  pass: { sim: 0.95, ink: 0.95 },
@@ -11149,11 +12046,13 @@ var init_generate_recorded = __esm({
11149
12046
 
11150
12047
  // packages/cli/src/bin.ts
11151
12048
  init_src3();
12049
+ init_invocation();
11152
12050
  import { CommanderError } from "commander";
11153
12051
 
11154
12052
  // packages/cli/src/program.ts
11155
12053
  init_src3();
11156
12054
  init_environment();
12055
+ init_invocation();
11157
12056
  init_doctor();
11158
12057
  import { Command } from "commander";
11159
12058
 
@@ -11161,10 +12060,11 @@ import { Command } from "commander";
11161
12060
  init_src3();
11162
12061
  init_describe();
11163
12062
  init_env();
12063
+ init_invocation();
11164
12064
  init_output();
11165
12065
  import { intro, isCancel, outro, password } from "@clack/prompts";
11166
12066
  import fs from "node:fs";
11167
- import path23 from "node:path";
12067
+ import path24 from "node:path";
11168
12068
  var INIT_DESCRIPTION = {
11169
12069
  name: "init",
11170
12070
  summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
@@ -11201,7 +12101,7 @@ async function runInit(flags) {
11201
12101
  printDescription(INIT_DESCRIPTION);
11202
12102
  return;
11203
12103
  }
11204
- const envPath = path23.resolve(process.cwd(), ".env");
12104
+ const envPath = path24.resolve(process.cwd(), ".env");
11205
12105
  const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
11206
12106
  let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
11207
12107
  let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
@@ -11210,7 +12110,7 @@ async function runInit(flags) {
11210
12110
  fail(flags, ExitCode.InputValidation, {
11211
12111
  error: "Missing credentials and prompts are disabled.",
11212
12112
  code: "MISSING_CREDENTIALS",
11213
- remediation: "Pass --figma-token <token> and --openrouter-key <key>, or run `tendril init` in an interactive terminal."
12113
+ remediation: `Pass --figma-token <token> and --openrouter-key <key>, or run \`${tendrilCommand("init")}\` in an interactive terminal.`
11214
12114
  });
11215
12115
  }
11216
12116
  if (interactive) {
@@ -11222,7 +12122,7 @@ async function runInit(flags) {
11222
12122
  next.set(ENV_KEYS.figma, figmaToken);
11223
12123
  next.set(ENV_KEYS.openrouter, openrouterKey);
11224
12124
  const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
11225
- const gitignorePath = path23.resolve(process.cwd(), ".gitignore");
12125
+ const gitignorePath = path24.resolve(process.cwd(), ".gitignore");
11226
12126
  const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
11227
12127
  const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
11228
12128
  if (flags.dryRun) {
@@ -11274,17 +12174,18 @@ init_src5();
11274
12174
  init_src2();
11275
12175
  init_describe();
11276
12176
  init_env();
12177
+ init_invocation();
11277
12178
  init_output();
11278
12179
  init_entitlement();
11279
12180
  import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
11280
- import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync18 } from "node:fs";
12181
+ import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync19 } from "node:fs";
11281
12182
 
11282
12183
  // packages/cli/src/pipeline.ts
11283
12184
  init_src2();
11284
12185
  init_src4();
11285
12186
  init_src6();
11286
12187
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
11287
- import path24 from "node:path";
12188
+ import path25 from "node:path";
11288
12189
 
11289
12190
  // packages/cli/src/assets-module.ts
11290
12191
  init_src();
@@ -11620,7 +12521,7 @@ async function runGenerationPipeline(input) {
11620
12521
  });
11621
12522
  const written = [];
11622
12523
  if (!input.dryRun) {
11623
- const dir = path24.resolve(input.outDir, semantics.componentName);
12524
+ const dir = path25.resolve(input.outDir, semantics.componentName);
11624
12525
  mkdirSync5(dir, { recursive: true });
11625
12526
  const files = {
11626
12527
  // Bundle-local tokens: THE emission the component's CSS resolves
@@ -11644,13 +12545,13 @@ async function runGenerationPipeline(input) {
11644
12545
  `
11645
12546
  };
11646
12547
  for (const [name, content] of Object.entries(files)) {
11647
- const filePath = path24.join(dir, name);
12548
+ const filePath = path25.join(dir, name);
11648
12549
  writeFileSync8(filePath, content);
11649
12550
  written.push(filePath);
11650
12551
  }
11651
12552
  for (const artifact of emitTokenArtifacts(input.mapping)) {
11652
- const filePath = path24.resolve(input.outDir, artifact.path);
11653
- mkdirSync5(path24.dirname(filePath), { recursive: true });
12553
+ const filePath = path25.resolve(input.outDir, artifact.path);
12554
+ mkdirSync5(path25.dirname(filePath), { recursive: true });
11654
12555
  writeFileSync8(filePath, artifact.content);
11655
12556
  written.push(filePath);
11656
12557
  }
@@ -11714,7 +12615,7 @@ function resolveProvidedSource(flags, contextFile) {
11714
12615
  fail(flags, ExitCode.InputValidation, {
11715
12616
  error: `Cannot read context file "${contextFile}".`,
11716
12617
  code: "CONTEXT_UNREADABLE",
11717
- remediation: "Pass a readable JSON file via --context; run `tendril generate --describe` for the payload shape."
12618
+ remediation: `Pass a readable JSON file via --context; run \`${tendrilCommand("generate --describe")}\` for the payload shape.`
11718
12619
  });
11719
12620
  }
11720
12621
  try {
@@ -11723,7 +12624,7 @@ function resolveProvidedSource(flags, contextFile) {
11723
12624
  fail(flags, ExitCode.InputValidation, {
11724
12625
  error: `Context file "${contextFile}" is not a valid design-context payload: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
11725
12626
  code: "CONTEXT_INVALID",
11726
- remediation: "The payload must match { nodeTree, variables[], metadata, screenshotBase64? } \u2014 see `tendril generate --describe` and docs/adr/003-figma-access-strategy.md."
12627
+ remediation: `The payload must match { nodeTree, variables[], metadata, screenshotBase64? } \u2014 see \`${tendrilCommand("generate --describe")}\` and docs/adr/003-figma-access-strategy.md.`
11727
12628
  });
11728
12629
  }
11729
12630
  }
@@ -11733,7 +12634,7 @@ function resolveSource(flags, url) {
11733
12634
  fail(flags, ExitCode.General, {
11734
12635
  error: "Live Figma transports (desktop MCP / REST) are not wired up yet.",
11735
12636
  code: "NOT_IMPLEMENTED",
11736
- remediation: "Use `tendril generate --context <file>` with an agent-fetched payload, or mock://button for the offline fixture; live transports are task 1.2 in docs/PLAN.md."
12637
+ remediation: `Use \`${tendrilCommand("generate --context <file>")}\` with an agent-fetched payload, or mock://button for the offline fixture; live transports are task 1.2 in docs/PLAN.md.`
11737
12638
  });
11738
12639
  }
11739
12640
  fail(flags, ExitCode.InputValidation, {
@@ -11752,7 +12653,7 @@ async function runGenerate(url, flags) {
11752
12653
  fail(flags, ExitCode.InputValidation, {
11753
12654
  error: "Missing input: pass a recording set or reference task (recorded truth, the default), mock://button, or --context <file>.",
11754
12655
  code: "MISSING_ARGUMENT",
11755
- remediation: "Run `tendril generate --describe` to see arguments and examples."
12656
+ remediation: `Run \`${tendrilCommand("generate --describe")}\` to see arguments and examples.`
11756
12657
  });
11757
12658
  }
11758
12659
  url ??= `provided://${flags.context}`;
@@ -11829,10 +12730,10 @@ token mapping (${mapping.flat.length} variables):
11829
12730
  let initialCode;
11830
12731
  let initialSemantics;
11831
12732
  try {
11832
- if (existsSync18(flags.out)) {
12733
+ if (existsSync19(flags.out)) {
11833
12734
  for (const entry of readdirSync4(flags.out)) {
11834
12735
  const cjPath = `${flags.out}/${entry}/component.json`;
11835
- if (!existsSync18(cjPath)) continue;
12736
+ if (!existsSync19(cjPath)) continue;
11836
12737
  const cj = JSON.parse(readFileSync15(cjPath, "utf8"));
11837
12738
  if (cj.name !== void 0 && Array.isArray(cj.props)) {
11838
12739
  previousApi = JSON.stringify({
@@ -11841,13 +12742,13 @@ token mapping (${mapping.flat.length} variables):
11841
12742
  });
11842
12743
  const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
11843
12744
  const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
11844
- if (flags.refine && existsSync18(tsxPath) && existsSync18(cssPath)) {
12745
+ if (flags.refine && existsSync19(tsxPath) && existsSync19(cssPath)) {
11845
12746
  initialCode = {
11846
12747
  tsx: readFileSync15(tsxPath, "utf8"),
11847
12748
  css: readFileSync15(cssPath, "utf8")
11848
12749
  };
11849
12750
  const semPath = `${flags.out}/${entry}/semantics.json`;
11850
- if (existsSync18(semPath)) {
12751
+ if (existsSync19(semPath)) {
11851
12752
  initialSemantics = JSON.parse(readFileSync15(semPath, "utf8"));
11852
12753
  }
11853
12754
  }
@@ -11877,7 +12778,7 @@ token mapping (${mapping.flat.length} variables):
11877
12778
  fail(flags, ExitCode.Auth, {
11878
12779
  error: "OPENROUTER_API_KEY is not configured.",
11879
12780
  code: "MISSING_OPENROUTER_KEY",
11880
- remediation: "Run `tendril init` to store your OpenRouter key in .env (BYOK \u2014 your key, your inference)."
12781
+ remediation: `Run \`${tendrilCommand("init")}\` to store your OpenRouter key in .env (BYOK \u2014 your key, your inference).`
11881
12782
  });
11882
12783
  }
11883
12784
  genModel = new OpenRouterGenerationModel(new OpenRouterClient({ apiKey }), model.id, {
@@ -12100,7 +13001,7 @@ function buildProgram() {
12100
13001
  fail2(flags, ExitCode.InputValidation, {
12101
13002
  error: family === void 0 ? "nothing to resolve \u2014 pass a family name or --set <recording-dir>" : "pass a family name OR --set, not both \u2014 --set derives families from the recording itself",
12102
13003
  code: "INPUT_VALIDATION",
12103
- remediation: 'tendril fonts resolve --set <recording-dir>, or tendril fonts resolve "<Family>" --weights 400 500 600'
13004
+ remediation: `${tendrilCommand("fonts resolve --set <recording-dir>")}, or ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}`
12104
13005
  });
12105
13006
  }
12106
13007
  if (set !== void 0) {
@@ -12254,7 +13155,7 @@ try {
12254
13155
  const cliError2 = {
12255
13156
  error: err.message,
12256
13157
  code: "INPUT_VALIDATION",
12257
- remediation: "Run `tendril --help` or `<command> --describe` for usage."
13158
+ remediation: `Run \`${tendrilCommand("--help")}\` or \`${tendrilCommand("<command> --describe")}\` for usage.`
12258
13159
  };
12259
13160
  process.stderr.write(`${JSON.stringify(cliError2)}
12260
13161
  `);