@tendrilapp/cli 0.1.39 → 0.1.41

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
@@ -1217,7 +1217,7 @@ function clampedReference(setDir, slug, payload) {
1217
1217
  const dims = referencePngDims(payload);
1218
1218
  if (dims === void 0) return void 0;
1219
1219
  if (dims.w >= box.w - 1 && dims.h >= box.h - 1) return void 0;
1220
- return `the reference image is ${dims.w}\xD7${dims.h} but the recorded box is ${box.w}\xD7${box.h} \u2014 it was SCALED DOWN, so it is not pixel ground truth. Re-run get_screenshot passing an explicit maxDimension of at least ${Math.max(box.w, box.h)} (the tool defaults to 1024 and scales to fit)`;
1220
+ return `the reference image is ${dims.w}\xD7${dims.h} but the recorded box is ${box.w}\xD7${box.h} \u2014 it was SCALED DOWN, so it is not pixel ground truth. Re-run get_screenshot passing contentsOnly: true and maxDimension: 4096 (the tool defaults to 1024 and clamps anything longer; a flat generous cap is always at least the ${Math.max(box.w, box.h)} this box needs, and contentsOnly keeps the editor's component-set chrome out of the padding)`;
1221
1221
  }
1222
1222
  function referencePngDims(payload) {
1223
1223
  const parts = payload?.content;
@@ -1980,8 +1980,8 @@ var init_src = __esm({
1980
1980
  function variableNameToPath(name) {
1981
1981
  return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
1982
1982
  }
1983
- function tokenPathToCssVar(path50) {
1984
- return `--${path50.join("-")}`;
1983
+ function tokenPathToCssVar(path54) {
1984
+ return `--${path54.join("-")}`;
1985
1985
  }
1986
1986
  function toDtcgToken(variable, defaultMode) {
1987
1987
  const modes = Object.keys(variable.valuesByMode);
@@ -2025,11 +2025,11 @@ function toDtcgToken(variable, defaultMode) {
2025
2025
  }
2026
2026
  function mapVariablesToDtcg(variables, defaultMode = "light") {
2027
2027
  const entries = variables.map((variable) => {
2028
- const path50 = variableNameToPath(variable.name);
2029
- if (path50.length === 0) {
2028
+ const path54 = variableNameToPath(variable.name);
2029
+ if (path54.length === 0) {
2030
2030
  throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
2031
2031
  }
2032
- return { variable, path: path50 };
2032
+ return { variable, path: path54 };
2033
2033
  });
2034
2034
  const groupPrefixes = /* @__PURE__ */ new Set();
2035
2035
  for (const e of entries) {
@@ -2050,21 +2050,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
2050
2050
  }
2051
2051
  const tokens = {};
2052
2052
  const flat = [];
2053
- for (const { variable, path: path50 } of entries) {
2053
+ for (const { variable, path: path54 } of entries) {
2054
2054
  const token = toDtcgToken(variable, defaultMode);
2055
2055
  let group = tokens;
2056
- for (const segment of path50.slice(0, -1)) {
2056
+ for (const segment of path54.slice(0, -1)) {
2057
2057
  const existing = group[segment];
2058
2058
  group = existing ?? (group[segment] = {});
2059
2059
  }
2060
- const leaf = path50[path50.length - 1];
2060
+ const leaf = path54[path54.length - 1];
2061
2061
  if (group[leaf] !== void 0) {
2062
- throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path50.join(".")}" (variable ${variable.id})`);
2062
+ throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path54.join(".")}" (variable ${variable.id})`);
2063
2063
  }
2064
2064
  group[leaf] = token;
2065
2065
  flat.push({
2066
- path: path50.join("."),
2067
- cssVar: tokenPathToCssVar(path50),
2066
+ path: path54.join("."),
2067
+ cssVar: tokenPathToCssVar(path54),
2068
2068
  type: token.$type,
2069
2069
  value: token.$value
2070
2070
  });
@@ -2253,9 +2253,9 @@ function boundId(value) {
2253
2253
  return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
2254
2254
  }
2255
2255
  function resolveBinding(ctx, id) {
2256
- const path50 = ctx.pathById.get(id);
2257
- if (path50 === void 0) ctx.unresolved.add(id);
2258
- return path50;
2256
+ const path54 = ctx.pathById.get(id);
2257
+ if (path54 === void 0) ctx.unresolved.add(id);
2258
+ return path54;
2259
2259
  }
2260
2260
  function parseVariantProps(name) {
2261
2261
  if (!name.includes("=")) return void 0;
@@ -2290,8 +2290,8 @@ function walk(ctx, raw) {
2290
2290
  if (!isObject(paint) || paint["visible"] === false) continue;
2291
2291
  const id = boundId(paint);
2292
2292
  if (id !== void 0) {
2293
- const path50 = resolveBinding(ctx, id);
2294
- if (path50 !== void 0) tokens.add(path50);
2293
+ const path54 = resolveBinding(ctx, id);
2294
+ if (path54 !== void 0) tokens.add(path54);
2295
2295
  } else if (typeof paint["color"] === "string") {
2296
2296
  ctx.hardcoded.push({ node: name, property, value: paint["color"] });
2297
2297
  }
@@ -2299,8 +2299,8 @@ function walk(ctx, raw) {
2299
2299
  }
2300
2300
  const radiusId = boundId(raw["cornerRadius"]);
2301
2301
  if (radiusId !== void 0) {
2302
- const path50 = resolveBinding(ctx, radiusId);
2303
- if (path50 !== void 0) tokens.add(path50);
2302
+ const path54 = resolveBinding(ctx, radiusId);
2303
+ if (path54 !== void 0) tokens.add(path54);
2304
2304
  } else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
2305
2305
  ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
2306
2306
  }
@@ -2310,10 +2310,10 @@ function walk(ctx, raw) {
2310
2310
  layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
2311
2311
  const gapId = boundId(raw["itemSpacing"]);
2312
2312
  if (gapId !== void 0) {
2313
- const path50 = resolveBinding(ctx, gapId);
2314
- if (path50 !== void 0) {
2315
- layout.gap = path50;
2316
- tokens.add(path50);
2313
+ const path54 = resolveBinding(ctx, gapId);
2314
+ if (path54 !== void 0) {
2315
+ layout.gap = path54;
2316
+ tokens.add(path54);
2317
2317
  }
2318
2318
  } else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
2319
2319
  ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
@@ -2322,10 +2322,10 @@ function walk(ctx, raw) {
2322
2322
  for (const field of PADDING_FIELDS) {
2323
2323
  const id = boundId(raw[field]);
2324
2324
  if (id !== void 0) {
2325
- const path50 = resolveBinding(ctx, id);
2326
- if (path50 !== void 0) {
2327
- paddingPaths.push(path50);
2328
- tokens.add(path50);
2325
+ const path54 = resolveBinding(ctx, id);
2326
+ if (path54 !== void 0) {
2327
+ paddingPaths.push(path54);
2328
+ tokens.add(path54);
2329
2329
  }
2330
2330
  } else if (typeof raw[field] === "number" && raw[field] !== 0) {
2331
2331
  ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
@@ -2533,9 +2533,17 @@ var init_src3 = __esm({
2533
2533
  });
2534
2534
 
2535
2535
  // packages/cli/src/invocation.ts
2536
- import { existsSync as existsSync5, realpathSync } from "node:fs";
2536
+ import { existsSync as existsSync5, readFileSync as readFileSync5, realpathSync } from "node:fs";
2537
2537
  import path5 from "node:path";
2538
2538
  import { fileURLToPath } from "node:url";
2539
+ function isDevTree(root) {
2540
+ try {
2541
+ const pkg = JSON.parse(readFileSync5(path5.join(root, "package.json"), "utf8"));
2542
+ return pkg.private === true && pkg.name === "@tendrilapp/cli";
2543
+ } catch {
2544
+ return false;
2545
+ }
2546
+ }
2539
2547
  function findPathTendril(pathEnv, platform) {
2540
2548
  const dirs = pathEnv.split(path5.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
2541
2549
  const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
@@ -2557,11 +2565,17 @@ function packageRootOf(file) {
2557
2565
  }
2558
2566
  }
2559
2567
  function resolveInvocation(pathEnv, platform, selfFile = fileURLToPath(import.meta.url)) {
2568
+ let self = null;
2569
+ try {
2570
+ self = packageRootOf(realpathSync(selfFile));
2571
+ } catch {
2572
+ self = null;
2573
+ }
2574
+ if (self !== null && isDevTree(self)) return DEV_INVOCATION;
2560
2575
  const binary = findPathTendril(pathEnv, platform);
2561
2576
  if (binary === null) return NPX_INVOCATION;
2562
2577
  try {
2563
2578
  const onPath = packageRootOf(realpathSync(binary));
2564
- const self = packageRootOf(realpathSync(selfFile));
2565
2579
  if (onPath === null || self === null) return NPX_INVOCATION;
2566
2580
  return realpathSync(onPath) === realpathSync(self) ? "tendril" : NPX_INVOCATION;
2567
2581
  } catch {
@@ -2579,11 +2593,12 @@ function quoteArg(value) {
2579
2593
  if (/["`$]/.test(value)) throw new Error(`refusing to emit a shell argument containing a double quote, backtick or $: ${value}`);
2580
2594
  return /[\s'*?[\]()&;|<>#~]/.test(value) ? `"${value}"` : value;
2581
2595
  }
2582
- var NPX_INVOCATION, cached;
2596
+ var NPX_INVOCATION, DEV_INVOCATION, cached;
2583
2597
  var init_invocation = __esm({
2584
2598
  "packages/cli/src/invocation.ts"() {
2585
2599
  "use strict";
2586
2600
  NPX_INVOCATION = "npx -y -p @tendrilapp/cli@latest tendril";
2601
+ DEV_INVOCATION = "pnpm --filter @tendrilapp/cli dev";
2587
2602
  cached = null;
2588
2603
  }
2589
2604
  });
@@ -3308,6 +3323,7 @@ function absentInkClusters(render, reference, background = [255, 255, 255]) {
3308
3323
  let x1 = sx;
3309
3324
  let y1 = sy;
3310
3325
  let px = 0;
3326
+ let chromePx = 0;
3311
3327
  const stack = [si];
3312
3328
  seen[si] = 1;
3313
3329
  while (stack.length > 0) {
@@ -3315,6 +3331,8 @@ function absentInkClusters(render, reference, background = [255, 255, 255]) {
3315
3331
  const cx = i % width;
3316
3332
  const cy = (i - cx) / width;
3317
3333
  px += 1;
3334
+ const ci = i * 4;
3335
+ if (canvasB.data[ci] === FIGMA_CHROME[0] && canvasB.data[ci + 1] === FIGMA_CHROME[1] && canvasB.data[ci + 2] === FIGMA_CHROME[2]) chromePx += 1;
3318
3336
  if (cx < x0) x0 = cx;
3319
3337
  if (cy < y0) y0 = cy;
3320
3338
  if (cx > x1) x1 = cx;
@@ -3332,7 +3350,7 @@ function absentInkClusters(render, reference, background = [255, 255, 255]) {
3332
3350
  }
3333
3351
  }
3334
3352
  }
3335
- if (px >= ABSENT_MIN_PX) found.push({ x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1, px, memberX: sx, memberY: sy });
3353
+ if (px >= ABSENT_MIN_PX) found.push({ x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1, px, memberX: sx, memberY: sy, ...chromePx * 2 >= px ? { chrome: true } : {} });
3336
3354
  }
3337
3355
  }
3338
3356
  }
@@ -3604,11 +3622,12 @@ function cropPng(png, x, y, width, height) {
3604
3622
  }
3605
3623
  return new Uint8Array(PNG.sync.write(out));
3606
3624
  }
3607
- var INK_DELTA, ABSENT_RADIUS, ABSENT_MIN_PX, DIFF_LEGEND;
3625
+ var INK_DELTA, FIGMA_CHROME, ABSENT_RADIUS, ABSENT_MIN_PX, DIFF_LEGEND, DIFF_LEGEND_TEXT;
3608
3626
  var init_image_diff = __esm({
3609
3627
  "packages/verify/src/image-diff.ts"() {
3610
3628
  "use strict";
3611
3629
  INK_DELTA = 30;
3630
+ FIGMA_CHROME = [138, 56, 245];
3612
3631
  ABSENT_RADIUS = 3;
3613
3632
  ABSENT_MIN_PX = 6;
3614
3633
  DIFF_LEGEND = {
@@ -3623,8 +3642,45 @@ var init_image_diff = __esm({
3623
3642
  * ALREADY FORGIVEN by inkRecall. Dimmed grey, so it reads as tolerated
3624
3643
  * rather than as a defect to chase (measured at only 3.0% of red, but
3625
3644
  * chasing it is exactly what the nudge saga was). */
3626
- shift: [150, 150, 150]
3645
+ shift: [150, 150, 150],
3646
+ /** pixelmatch's OWN anti-aliasing colour, passed through unrepainted.
3647
+ * `includeAA` is false, so these pixels are painted but NOT counted as
3648
+ * mismatches — they are in neither the similarity number nor the ink
3649
+ * deficit. Listed here because the first field run measured 343-380 of
3650
+ * them per config and the reader had a fourth colour with no key
3651
+ * (CompTest run, 2026-08-19). Do not repaint it: passing pixelmatch's
3652
+ * own marking through unchanged is what keeps "not counted" honest. */
3653
+ antialias: [255, 255, 0]
3627
3654
  };
3655
+ DIFF_LEGEND_TEXT = [
3656
+ "Tendril diff image legend \u2014 what each colour means.",
3657
+ "",
3658
+ "A verdict has TWO gates and this one image serves both, so the colour",
3659
+ "says WHICH number a pixel belongs to. Marked pixels are exactly the",
3660
+ "ones pixelmatch marks; only their colour carries the cause.",
3661
+ "",
3662
+ ` #ff0000 RED reference ink the render never painted, and no render`,
3663
+ ` ink within 1px. THE INK DEFICIT \u2014 the only category`,
3664
+ ` that moves inkRecall.`,
3665
+ ` #ffaa00 AMBER ink in BOTH images, different colour. Moves`,
3666
+ ` similarity. CANNOT move inkRecall.`,
3667
+ ` #0078ff BLUE render ink where the reference records none. Moves`,
3668
+ ` similarity. Cannot move inkRecall.`,
3669
+ ` #969696 GREY reference ink covered within 1px \u2014 a displacement`,
3670
+ ` inkRecall ALREADY FORGIVES. Not a defect to chase.`,
3671
+ ` #ffff00 YELLOW pixelmatch judged this anti-aliasing. Painted but`,
3672
+ ` NOT COUNTED: in neither the similarity number nor`,
3673
+ ` the ink deficit.`,
3674
+ "",
3675
+ "Raw marked-pixel count is a poor guide to cause: a field run measured a",
3676
+ "config where BLUE dominated 1797 to 270 while only the 270 red pixels",
3677
+ "demoted it.",
3678
+ "",
3679
+ "If red clusters sit OUTSIDE the recorded node box, suspect the",
3680
+ "reference rather than the code \u2014 Figma pads exports to contain outer",
3681
+ "effects, and the pad can capture the editor's own component-set chrome.",
3682
+ ""
3683
+ ].join("\n");
3628
3684
  }
3629
3685
  });
3630
3686
 
@@ -4309,7 +4365,7 @@ var init_font_collection = __esm({
4309
4365
  });
4310
4366
 
4311
4367
  // packages/verify/src/font-discovery.ts
4312
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2 } from "node:fs";
4368
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync6, realpathSync as realpathSync2 } from "node:fs";
4313
4369
  import os2 from "node:os";
4314
4370
  import path12 from "node:path";
4315
4371
  function weightFromSubfamily(subfamily) {
@@ -4357,7 +4413,7 @@ function faceAt(bytes, view, dirOffset, file, faceIndex) {
4357
4413
  }
4358
4414
  function facesInFile(file) {
4359
4415
  try {
4360
- const bytes = new Uint8Array(readFileSync5(file));
4416
+ const bytes = new Uint8Array(readFileSync6(file));
4361
4417
  if (bytes.length < 12) return [];
4362
4418
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
4363
4419
  if (isCollection(bytes)) {
@@ -4449,7 +4505,7 @@ var init_font_discovery = __esm({
4449
4505
 
4450
4506
  // packages/verify/src/font-resolve.ts
4451
4507
  import { createHash as createHash2 } from "node:crypto";
4452
- import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
4508
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
4453
4509
  import os3 from "node:os";
4454
4510
  import path13 from "node:path";
4455
4511
  function fontCacheDir() {
@@ -4528,7 +4584,7 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
4528
4584
  }
4529
4585
  }
4530
4586
  const mPath = path13.join(cacheDir, "manifest.json");
4531
- const prior = existsSync9(mPath) ? JSON.parse(readFileSync6(mPath, "utf8")) : [];
4587
+ const prior = existsSync9(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
4532
4588
  const portable2 = resolved.map((m) => ({ ...m, file: path13.basename(m.file) }));
4533
4589
  const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
4534
4590
  if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
@@ -4542,7 +4598,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
4542
4598
  if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
4543
4599
  throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
4544
4600
  }
4545
- let bytes = new Uint8Array(readFileSync6(src));
4601
+ let bytes = new Uint8Array(readFileSync7(src));
4546
4602
  if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
4547
4603
  let storedExt = ext;
4548
4604
  if (isCollection(bytes)) {
@@ -4565,16 +4621,16 @@ ${shown}`
4565
4621
  writeFileSync3(file, bytes);
4566
4622
  const face = { family, weight, source: `${provenance}:${path13.basename(src)}`, sha256, file, license: "unknown" };
4567
4623
  const mPath = path13.join(cacheDir, "manifest.json");
4568
- const prior = existsSync9(mPath) ? JSON.parse(readFileSync6(mPath, "utf8")) : [];
4624
+ const prior = existsSync9(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
4569
4625
  const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path13.basename(file) }];
4570
4626
  writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
4571
4627
  `);
4572
4628
  return face;
4573
4629
  }
4574
4630
  function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
4575
- const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
4631
+ const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
4576
4632
  const mPath = path13.join(cacheDir, "manifest.json");
4577
- const manifest = existsSync9(mPath) ? JSON.parse(readFileSync6(mPath, "utf8")) : [];
4633
+ const manifest = existsSync9(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
4578
4634
  return lock.map((l) => {
4579
4635
  const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
4580
4636
  if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
@@ -4586,7 +4642,7 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
4586
4642
  if (!existsSync9(mPath)) return [];
4587
4643
  let entries;
4588
4644
  try {
4589
- entries = JSON.parse(readFileSync6(mPath, "utf8"));
4645
+ entries = JSON.parse(readFileSync7(mPath, "utf8"));
4590
4646
  } catch {
4591
4647
  return [];
4592
4648
  }
@@ -4601,7 +4657,7 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
4601
4657
  }
4602
4658
  function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
4603
4659
  const mPath = path13.join(cacheDir, "manifest.json");
4604
- const manifest = existsSync9(mPath) ? JSON.parse(readFileSync6(mPath, "utf8")) : [];
4660
+ const manifest = existsSync9(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
4605
4661
  const wanted = new Set(families.map((f) => f.toLowerCase()));
4606
4662
  return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
4607
4663
  family: f.family,
@@ -4618,7 +4674,7 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
4618
4674
  if (!existsSync9(mPath)) return [];
4619
4675
  let entries;
4620
4676
  try {
4621
- entries = JSON.parse(readFileSync6(mPath, "utf8"));
4677
+ entries = JSON.parse(readFileSync7(mPath, "utf8"));
4622
4678
  } catch {
4623
4679
  return [];
4624
4680
  }
@@ -4631,7 +4687,7 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
4631
4687
  if (!existsSync9(mPath)) return [];
4632
4688
  let entries;
4633
4689
  try {
4634
- entries = JSON.parse(readFileSync6(mPath, "utf8"));
4690
+ entries = JSON.parse(readFileSync7(mPath, "utf8"));
4635
4691
  } catch {
4636
4692
  return [];
4637
4693
  }
@@ -4640,7 +4696,7 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
4640
4696
  if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
4641
4697
  const file = path13.isAbsolute(e.file) && existsSync9(e.file) ? e.file : path13.resolve(cacheDir, path13.basename(e.file));
4642
4698
  if (!existsSync9(file)) continue;
4643
- if (createHash2("sha256").update(readFileSync6(file)).digest("hex") !== e.sha256) continue;
4699
+ if (createHash2("sha256").update(readFileSync7(file)).digest("hex") !== e.sha256) continue;
4644
4700
  const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
4645
4701
  set.add(e.weight);
4646
4702
  byFamily.set(e.family, set);
@@ -4662,7 +4718,7 @@ function addSystemFamily(family, opts = {}) {
4662
4718
  const overwrote = [];
4663
4719
  const cacheDir = opts.cacheDir ?? DEFAULT_FONT_CACHE;
4664
4720
  const manifestFile = path13.join(cacheDir, "manifest.json");
4665
- const prior = existsSync9(manifestFile) ? JSON.parse(readFileSync6(manifestFile, "utf8")) : [];
4721
+ const prior = existsSync9(manifestFile) ? JSON.parse(readFileSync7(manifestFile, "utf8")) : [];
4666
4722
  const taken = /* @__PURE__ */ new Set();
4667
4723
  for (const face of faces) {
4668
4724
  const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
@@ -4724,17 +4780,17 @@ var init_font_resolve = __esm({
4724
4780
 
4725
4781
  // packages/verify/src/font-faces.ts
4726
4782
  import { createHash as createHash3 } from "node:crypto";
4727
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
4783
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
4728
4784
  import path14 from "node:path";
4729
4785
  function injectedGroups(manifestPath2) {
4730
4786
  if (!existsSync10(manifestPath2)) return { groups: [], shared: false };
4731
- const claimed = JSON.parse(readFileSync7(manifestPath2, "utf8"));
4787
+ const claimed = JSON.parse(readFileSync8(manifestPath2, "utf8"));
4732
4788
  const resolveFile = (f) => path14.isAbsolute(f) && existsSync10(f) ? f : path14.resolve(path14.dirname(manifestPath2), path14.basename(f));
4733
4789
  const byFile = /* @__PURE__ */ new Map();
4734
4790
  for (const f of claimed) {
4735
4791
  const file = resolveFile(f.file);
4736
4792
  if (!existsSync10(file)) continue;
4737
- if (createHash3("sha256").update(readFileSync7(file)).digest("hex") !== f.sha256) continue;
4793
+ if (createHash3("sha256").update(readFileSync8(file)).digest("hex") !== f.sha256) continue;
4738
4794
  const k = `${f.family}:${f.file}`;
4739
4795
  const e = byFile.get(k) ?? { family: f.family, weights: [], file };
4740
4796
  e.weights.push(f.weight);
@@ -4747,7 +4803,7 @@ function fontFaceCss(manifestPath2 = path14.join(fontCacheDir(), "manifest.json"
4747
4803
  const { groups, shared } = injectedGroups(manifestPath2);
4748
4804
  return groups.map((e) => {
4749
4805
  const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
4750
- return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync7(e.file).toString("base64")}) format('woff2'); }`;
4806
+ return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync8(e.file).toString("base64")}) format('woff2'); }`;
4751
4807
  }).join("\n");
4752
4808
  }
4753
4809
  function injectedFamilyWeights(manifestPath2 = path14.join(fontCacheDir(), "manifest.json")) {
@@ -4775,7 +4831,7 @@ var init_font_faces = __esm({
4775
4831
  });
4776
4832
 
4777
4833
  // packages/verify/src/admission.ts
4778
- import { readFileSync as readFileSync8, readdirSync as readdirSync4, existsSync as existsSync11, writeFileSync as writeFileSync4 } from "node:fs";
4834
+ import { readFileSync as readFileSync9, readdirSync as readdirSync4, existsSync as existsSync11, writeFileSync as writeFileSync4 } from "node:fs";
4779
4835
  import path15 from "node:path";
4780
4836
  import { build as build2 } from "esbuild";
4781
4837
  import postcss from "postcss";
@@ -4785,7 +4841,7 @@ function fontWeightsByFamily() {
4785
4841
  const mPath = path15.join(fontCacheDir(), "manifest.json");
4786
4842
  const out = /* @__PURE__ */ new Map();
4787
4843
  if (!existsSync11(mPath)) return out;
4788
- for (const f of JSON.parse(readFileSync8(mPath, "utf8")))
4844
+ for (const f of JSON.parse(readFileSync9(mPath, "utf8")))
4789
4845
  out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
4790
4846
  return out;
4791
4847
  }
@@ -4805,7 +4861,7 @@ var init_admission = __esm({
4805
4861
  });
4806
4862
 
4807
4863
  // packages/verify/src/candidate-css.ts
4808
- import { existsSync as existsSync12, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
4864
+ import { existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
4809
4865
  import path16 from "node:path";
4810
4866
  function candidateCss(bundleDir) {
4811
4867
  const files = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f));
@@ -4824,7 +4880,7 @@ function candidateCss(bundleDir) {
4824
4880
  }
4825
4881
  files.push(path16.join(dir, "tokens.css"), path16.join(dir, "styles.css"));
4826
4882
  }
4827
- return files.filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
4883
+ return files.filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
4828
4884
  }
4829
4885
  var init_candidate_css = __esm({
4830
4886
  "packages/verify/src/candidate-css.ts"() {
@@ -5042,7 +5098,7 @@ __export(behavior_exports, {
5042
5098
  compileMount: () => compileMount,
5043
5099
  recordingIsDark: () => recordingIsDark
5044
5100
  });
5045
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
5101
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
5046
5102
  import path18 from "node:path";
5047
5103
  import { build as build3 } from "esbuild";
5048
5104
  import { chromium as chromium3 } from "playwright-core";
@@ -5118,8 +5174,8 @@ async function runSteps(page, spec, renderPose) {
5118
5174
  if (renderPose === void 0) return { id: spec.id, pass: false, detail: "commitMatchesPose requires a pose renderer" };
5119
5175
  await settle(page);
5120
5176
  const idle = await shotRoot(page);
5121
- for (const delay of [70, 130, 190]) {
5122
- await page.waitForTimeout(delay);
5177
+ for (const delay2 of [70, 130, 190]) {
5178
+ await page.waitForTimeout(delay2);
5123
5179
  await freezeAnimations(page);
5124
5180
  if (!(await shotRoot(page)).equals(idle)) {
5125
5181
  return { id: spec.id, pass: false, detail: "unmeasurable: the component's pixels change at rest (free-running animation or a timer in YOUR candidate) \u2014 the committed state cannot be observed through it; this IS fixable from code: keep every scored pose reachable with all motion idle (freezable CSS animation, no JS-driven repaints)" };
@@ -5414,7 +5470,7 @@ function recordingIsDark(task) {
5414
5470
  const f = path18.join(task.set, rep, "get_screenshot.json");
5415
5471
  if (!existsSync13(f)) return false;
5416
5472
  try {
5417
- const env = JSON.parse(readFileSync10(f, "utf8")).content.find((c) => c.type === "image");
5473
+ const env = JSON.parse(readFileSync11(f, "utf8")).content.find((c) => c.type === "image");
5418
5474
  if (env?.data === void 0) return false;
5419
5475
  const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
5420
5476
  let sum = 0;
@@ -6030,6 +6086,11 @@ var init_bundle = __esm({
6030
6086
  rep: z9.string(),
6031
6087
  similarity: z9.number(),
6032
6088
  inkRecall: z9.number(),
6089
+ /** Pre-deadband measured values. Declared so the field SURVIVES the
6090
+ * schema round-trip — the emitter writes it (bundle-emit) and zod was
6091
+ * silently stripping it back out, so a reader could not tell a
6092
+ * rounded 0.970 from a measured one. */
6093
+ exact: z9.object({ similarity: z9.number(), inkRecall: z9.number() }).optional(),
6033
6094
  status: ConfigStatusSchema
6034
6095
  });
6035
6096
  BehaviorClaimSchema = z9.object({
@@ -6051,7 +6112,18 @@ var init_bundle = __esm({
6051
6112
  licenseNote: z9.string().optional(),
6052
6113
  /** Content hash over the set manifest + rep envelopes (the identity
6053
6114
  * verify compares against, not the path). */
6054
- hash: z9.string()
6115
+ hash: z9.string(),
6116
+ /** KIT IDENTITY (PROV-1). The Figma file key and component-set node
6117
+ * the recording came from. Both are captured at plan time in the
6118
+ * recording set and were being dropped here, which left a published
6119
+ * bundle unable to say which kit it derives from — while the portal's
6120
+ * consent model is scoped to exactly that file key (ADR-018 §3), and
6121
+ * the right to publish attaches to a kit rather than to a folder
6122
+ * someone named. Optional: recordings planned before this field
6123
+ * existed carry neither, and a set can be a handed-in selection with
6124
+ * no component set at all. Absent means NOT RECORDED, never "no kit". */
6125
+ figmaFile: z9.string().optional(),
6126
+ componentSetNode: z9.string().optional()
6055
6127
  }),
6056
6128
  environment: z9.object({
6057
6129
  chrome: z9.string(),
@@ -6100,6 +6172,198 @@ var init_bundle = __esm({
6100
6172
  }
6101
6173
  });
6102
6174
 
6175
+ // packages/metadata/src/published-surface.ts
6176
+ function classifyBundleSurface(files, opts) {
6177
+ const published = [];
6178
+ const excluded = [];
6179
+ const unknown = [];
6180
+ for (const raw of files) {
6181
+ const path54 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
6182
+ const inEvidence = path54.startsWith(`${EVIDENCE_DIR}/`);
6183
+ const name = inEvidence ? path54.slice(EVIDENCE_DIR.length + 1) : path54;
6184
+ if (name.includes("/")) {
6185
+ unknown.push(path54);
6186
+ continue;
6187
+ }
6188
+ if (inEvidence) {
6189
+ if (name === "verify-report.json") published.push({ path: path54, role: "verify-report" });
6190
+ else if (name === "diff-legend.txt") published.push({ path: path54, role: "diff-legend" });
6191
+ else if (name === "inspect.html") published.push({ path: path54, role: "inspect-sheet" });
6192
+ else {
6193
+ const hit = EVIDENCE_PATTERNS.find((p) => p.re.test(name));
6194
+ if (hit !== void 0) published.push({ path: path54, role: hit.role });
6195
+ else unknown.push(path54);
6196
+ }
6197
+ continue;
6198
+ }
6199
+ if (name === opts.entry) published.push({ path: path54, role: "entry" });
6200
+ else if (name === "styles.css") published.push({ path: path54, role: "styles" });
6201
+ else if (name === "tokens.css") published.push({ path: path54, role: "tokens" });
6202
+ else if (name === "fonts.css") published.push({ path: path54, role: "fonts" });
6203
+ else if (name === "component.json") published.push({ path: path54, role: "manifest" });
6204
+ else {
6205
+ const skip = EXCLUDED.find((e) => e.test(name));
6206
+ if (skip !== void 0) excluded.push({ path: path54, reason: skip.reason });
6207
+ else unknown.push(path54);
6208
+ }
6209
+ }
6210
+ const roles = new Set(published.map((p) => p.role));
6211
+ return { published, excluded, unknown, missingRequired: REQUIRED_ROLES.filter((r) => !roles.has(r)) };
6212
+ }
6213
+ function missingInspectCrops(sheetText, publishedPaths) {
6214
+ const held = new Set(publishedPaths);
6215
+ const missing = /* @__PURE__ */ new Set();
6216
+ for (const [, name] of sheetText.matchAll(/src="\.\/([^"]+)"/g)) {
6217
+ const path54 = `${EVIDENCE_DIR}/${name}`;
6218
+ if (!held.has(path54)) missing.add(path54);
6219
+ }
6220
+ return [...missing].sort();
6221
+ }
6222
+ var EVIDENCE_DIR, REQUIRED_ROLES, EXCLUDED, EVIDENCE_PATTERNS, SCORED_FILE_ROLES;
6223
+ var init_published_surface = __esm({
6224
+ "packages/metadata/src/published-surface.ts"() {
6225
+ "use strict";
6226
+ EVIDENCE_DIR = "verify-evidence";
6227
+ REQUIRED_ROLES = ["entry", "styles", "manifest", "verify-report", "evidence-triple"];
6228
+ EXCLUDED = [
6229
+ { test: (n) => n === "score-history.jsonl", reason: "generation loop score history" },
6230
+ { test: (n) => n === "loop-state.json", reason: "generation loop state" },
6231
+ { test: (n) => n === "run-log.json", reason: "generation run log" },
6232
+ { test: (n) => n.endsWith(".md"), reason: "brief / debrief / model response" },
6233
+ { test: (n) => n === ".DS_Store", reason: "macOS directory metadata" }
6234
+ ];
6235
+ EVIDENCE_PATTERNS = [
6236
+ { re: /^.+-absent-\d+-(?:ref|render)\.png$/, role: "evidence-absent-crop" },
6237
+ { re: /^.+-inspect-\d+-(?:ref|render)\.png$/, role: "evidence-zoom-crop" },
6238
+ { re: /^.+-(?:ref|render|diff)\.png$/, role: "evidence-triple" }
6239
+ ];
6240
+ SCORED_FILE_ROLES = /* @__PURE__ */ new Set([
6241
+ "entry",
6242
+ "styles",
6243
+ "tokens",
6244
+ "fonts",
6245
+ "manifest",
6246
+ "evidence-triple",
6247
+ "evidence-absent-crop",
6248
+ "diff-legend"
6249
+ ]);
6250
+ }
6251
+ });
6252
+
6253
+ // packages/metadata/src/bundle-files.ts
6254
+ import { createHash as createHash4 } from "node:crypto";
6255
+ import { readFileSync as readFileSync12, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
6256
+ import path19 from "node:path";
6257
+ function bundleFiles(dir, prefix = "") {
6258
+ const out = [];
6259
+ for (const entry of readdirSync6(dir).sort()) {
6260
+ const full = path19.join(dir, entry);
6261
+ const rel = prefix === "" ? entry : `${prefix}/${entry}`;
6262
+ if (statSync3(full).isDirectory()) out.push(...bundleFiles(full, rel));
6263
+ else out.push(rel);
6264
+ }
6265
+ return out;
6266
+ }
6267
+ function digestScoredFiles(bundleDir, entry) {
6268
+ const surface = classifyBundleSurface(bundleFiles(bundleDir), { entry });
6269
+ const out = {};
6270
+ for (const file of surface.published) {
6271
+ if (!SCORED_FILE_ROLES.has(file.role)) continue;
6272
+ out[file.path] = createHash4("sha256").update(readFileSync12(path19.join(bundleDir, file.path))).digest("hex");
6273
+ }
6274
+ return out;
6275
+ }
6276
+ var init_bundle_files = __esm({
6277
+ "packages/metadata/src/bundle-files.ts"() {
6278
+ "use strict";
6279
+ init_published_surface();
6280
+ }
6281
+ });
6282
+
6283
+ // packages/metadata/src/verify-report.ts
6284
+ function verifyReportForTransport(report, bundleName) {
6285
+ const { dir: _dir, ...evidence } = report.evidence;
6286
+ const { eyeCheck: _eyeCheck, ...rest } = report;
6287
+ return { ...rest, bundle: bundleName, evidence };
6288
+ }
6289
+ function nonEmptyArray(value) {
6290
+ return Array.isArray(value) && value.length > 0;
6291
+ }
6292
+ function undisclosedTrustFacts(report) {
6293
+ if (report === null || typeof report !== "object" || Array.isArray(report)) return [];
6294
+ const r = report;
6295
+ const facts = [];
6296
+ if (r["recordingSetDrift"] !== void 0 && r["recordingSetDrift"] !== null) {
6297
+ facts.push({
6298
+ pointer: "$.recordingSetDrift",
6299
+ consequence: "the recording set that was scored is not the one the bundle stamps, so these scores describe a different design than the bundle claims"
6300
+ });
6301
+ }
6302
+ if (nonEmptyArray(r["substitutedFamilies"])) {
6303
+ facts.push({
6304
+ pointer: "$.substitutedFamilies",
6305
+ consequence: "a font family was substituted, so the scores measure a substitute face and no config can be certified under one"
6306
+ });
6307
+ }
6308
+ if (nonEmptyArray(r["certBlockedByAbsentInk"])) {
6309
+ facts.push({
6310
+ pointer: "$.certBlockedByAbsentInk",
6311
+ consequence: "certification was blocked by absent ink, which a verdict alone does not say"
6312
+ });
6313
+ }
6314
+ return facts;
6315
+ }
6316
+ function isSetHash(value) {
6317
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
6318
+ }
6319
+ function readScoredFiles(report) {
6320
+ if (report === null || typeof report !== "object" || Array.isArray(report)) return void 0;
6321
+ const value = report["scoredFiles"];
6322
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
6323
+ const entries = Object.entries(value);
6324
+ if (entries.length === 0) return void 0;
6325
+ const out = {};
6326
+ for (const [path54, digest] of entries) {
6327
+ if (path54 === "" || path54.startsWith("/") || path54.includes("..")) return void 0;
6328
+ if (!isSetHash(digest)) return void 0;
6329
+ out[path54] = digest;
6330
+ }
6331
+ return out;
6332
+ }
6333
+ function compareScoredFiles(recorded, actual) {
6334
+ const missing = [];
6335
+ const unscored = [];
6336
+ const changed = [];
6337
+ for (const [path54, digest] of Object.entries(recorded)) {
6338
+ if (!(path54 in actual)) missing.push(path54);
6339
+ else if (actual[path54] !== digest) changed.push(path54);
6340
+ }
6341
+ for (const path54 of Object.keys(actual)) if (!(path54 in recorded)) unscored.push(path54);
6342
+ return { missing: missing.sort(), unscored: unscored.sort(), changed: changed.sort() };
6343
+ }
6344
+ function scoredRecordingSetHash(report) {
6345
+ if (report === null || typeof report !== "object" || Array.isArray(report)) return void 0;
6346
+ const r = report;
6347
+ const drift = r["recordingSetDrift"];
6348
+ if (drift !== null && typeof drift === "object" && !Array.isArray(drift)) {
6349
+ const current = drift["current"];
6350
+ if (isSetHash(current)) return current;
6351
+ }
6352
+ const manifest = r["bundleManifest"];
6353
+ if (manifest !== null && typeof manifest === "object" && !Array.isArray(manifest)) {
6354
+ const stamped = manifest["recordingSetHash"];
6355
+ if (isSetHash(stamped)) return stamped;
6356
+ }
6357
+ return void 0;
6358
+ }
6359
+ var VERIFY_REPORT_FILENAME;
6360
+ var init_verify_report = __esm({
6361
+ "packages/metadata/src/verify-report.ts"() {
6362
+ "use strict";
6363
+ VERIFY_REPORT_FILENAME = "verify-report.json";
6364
+ }
6365
+ });
6366
+
6103
6367
  // packages/metadata/src/motion-css.ts
6104
6368
  var MOTION_CSS_PATTERN, scannableCss, MOTION_TOKEN_NAMES;
6105
6369
  var init_motion_css = __esm({
@@ -6325,14 +6589,17 @@ var init_src4 = __esm({
6325
6589
  init_extract();
6326
6590
  init_recording_set();
6327
6591
  init_bundle();
6592
+ init_bundle_files();
6593
+ init_verify_report();
6594
+ init_published_surface();
6328
6595
  init_motion_css();
6329
6596
  init_profile();
6330
6597
  }
6331
6598
  });
6332
6599
 
6333
6600
  // packages/verify/src/bundle-quality.ts
6334
- import { readFileSync as readFileSync11, readdirSync as readdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync14 } from "node:fs";
6335
- import path19 from "node:path";
6601
+ import { readFileSync as readFileSync13, readdirSync as readdirSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync14 } from "node:fs";
6602
+ import path20 from "node:path";
6336
6603
  function unscopedReduceBlockFindings(file, css) {
6337
6604
  const stripped = css.replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, " "));
6338
6605
  const media = /@media[^{]*prefers-reduced-motion\s*:\s*reduce[^{]*\{/.exec(stripped);
@@ -6390,16 +6657,16 @@ function recordedAssetFindings(entryFile, entrySource, recordedPaths) {
6390
6657
  function recordedAssetPaths(setDir, reps) {
6391
6658
  const map = /* @__PURE__ */ new Map();
6392
6659
  for (const rep of reps) {
6393
- const dir = path19.join(setDir, rep);
6660
+ const dir = path20.join(setDir, rep);
6394
6661
  let files;
6395
6662
  try {
6396
- files = readdirSync6(dir).filter((f) => f.startsWith("asset-") && f.endsWith(".svg"));
6663
+ files = readdirSync7(dir).filter((f) => f.startsWith("asset-") && f.endsWith(".svg"));
6397
6664
  } catch {
6398
6665
  continue;
6399
6666
  }
6400
6667
  for (const f of files) {
6401
6668
  try {
6402
- const svg = readFileSync11(path19.join(dir, f), "utf8");
6669
+ const svg = readFileSync13(path20.join(dir, f), "utf8");
6403
6670
  for (const m of svg.matchAll(/\sd="([^"]+)"/g)) {
6404
6671
  const d = m[1];
6405
6672
  const owners = map.get(d) ?? [];
@@ -6467,17 +6734,17 @@ function recordedTokenMapState(setDir, reps) {
6467
6734
  const readMap = (file) => {
6468
6735
  if (!existsSync14(file)) return void 0;
6469
6736
  try {
6470
- const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync11(file, "utf8"))) || "{}");
6737
+ const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync13(file, "utf8"))) || "{}");
6471
6738
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
6472
6739
  } catch {
6473
6740
  return {};
6474
6741
  }
6475
6742
  };
6476
- const setLevel = readMap(path19.join(setDir, "get_variable_defs.json"));
6743
+ const setLevel = readMap(path20.join(setDir, "get_variable_defs.json"));
6477
6744
  if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
6478
6745
  let recorded = false;
6479
6746
  for (const rep of reps) {
6480
- const m = readMap(path19.join(setDir, rep, "get_variable_defs.json"));
6747
+ const m = readMap(path20.join(setDir, rep, "get_variable_defs.json"));
6481
6748
  if (m === void 0) continue;
6482
6749
  recorded = true;
6483
6750
  if (Object.keys(m).length > 0) return "populated";
@@ -6489,7 +6756,7 @@ function recordedTokensByValue(setDir, reps) {
6489
6756
  const readMap = (file) => {
6490
6757
  if (!existsSync14(file)) return void 0;
6491
6758
  try {
6492
- const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync11(file, "utf8"))) || "{}");
6759
+ const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync13(file, "utf8"))) || "{}");
6493
6760
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
6494
6761
  } catch {
6495
6762
  return void 0;
@@ -6507,8 +6774,8 @@ function recordedTokensByValue(setDir, reps) {
6507
6774
  byValue.set(key, candidates);
6508
6775
  }
6509
6776
  };
6510
- absorb(readMap(path19.join(setDir, "get_variable_defs.json")));
6511
- for (const rep of reps) absorb(readMap(path19.join(setDir, rep, "get_variable_defs.json")));
6777
+ absorb(readMap(path20.join(setDir, "get_variable_defs.json")));
6778
+ for (const rep of reps) absorb(readMap(path20.join(setDir, rep, "get_variable_defs.json")));
6512
6779
  return byValue;
6513
6780
  }
6514
6781
  function scannable(css) {
@@ -6617,11 +6884,11 @@ function conventionsFindings(entrySource, css, profile) {
6617
6884
  }
6618
6885
  async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, contract, profile) {
6619
6886
  const findings = [];
6620
- const entryPath = path19.join(bundleDir, entry);
6621
- const cssPath = path19.join(bundleDir, "styles.css");
6622
- const tokensPath = path19.join(bundleDir, "tokens.css");
6623
- const css = existsSync14(cssPath) ? readFileSync11(cssPath, "utf8") : "";
6624
- const tokensCss = existsSync14(tokensPath) ? readFileSync11(tokensPath, "utf8") : void 0;
6887
+ const entryPath = path20.join(bundleDir, entry);
6888
+ const cssPath = path20.join(bundleDir, "styles.css");
6889
+ const tokensPath = path20.join(bundleDir, "tokens.css");
6890
+ const css = existsSync14(cssPath) ? readFileSync13(cssPath, "utf8") : "";
6891
+ const tokensCss = existsSync14(tokensPath) ? readFileSync13(tokensPath, "utf8") : void 0;
6625
6892
  findings.push(
6626
6893
  ...fontStackFindings(
6627
6894
  [
@@ -6634,8 +6901,8 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, c
6634
6901
  if (existsSync14(entryPath)) {
6635
6902
  const workDir = newScratchDir("quality");
6636
6903
  try {
6637
- const tsxPath = path19.join(workDir, entry);
6638
- writeFileSync5(tsxPath, readFileSync11(entryPath, "utf8"));
6904
+ const tsxPath = path20.join(workDir, entry);
6905
+ writeFileSync5(tsxPath, readFileSync13(entryPath, "utf8"));
6639
6906
  for (const d of runTscStrict([tsxPath]).diagnostics) {
6640
6907
  if (d.code === 2307 && /['"]\.\/composed\//.test(d.message)) continue;
6641
6908
  findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
@@ -6645,7 +6912,7 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, c
6645
6912
  }
6646
6913
  }
6647
6914
  if (contract !== void 0 && existsSync14(entryPath)) {
6648
- findings.push(...expertLensFindings(entry, readFileSync11(entryPath, "utf8"), contract));
6915
+ findings.push(...expertLensFindings(entry, readFileSync13(entryPath, "utf8"), contract));
6649
6916
  }
6650
6917
  for (const sheet of [
6651
6918
  { file: "styles.css", text: css },
@@ -6656,7 +6923,7 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, c
6656
6923
  findings.push(...placeholderSelectorFindings(sheet.file, sheet.text));
6657
6924
  }
6658
6925
  if (set !== void 0 && existsSync14(entryPath)) {
6659
- findings.push(...recordedAssetFindings(entry, readFileSync11(entryPath, "utf8"), recordedAssetPaths(set.dir, set.reps)));
6926
+ findings.push(...recordedAssetFindings(entry, readFileSync13(entryPath, "utf8"), recordedAssetPaths(set.dir, set.reps)));
6660
6927
  }
6661
6928
  if (css !== "") {
6662
6929
  const mapState = set === void 0 ? void 0 : recordedTokenMapState(set.dir, set.reps);
@@ -6678,7 +6945,7 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, c
6678
6945
  })
6679
6946
  );
6680
6947
  if (profile !== void 0 && existsSync14(entryPath)) {
6681
- findings.push(...conventionsFindings(readFileSync11(entryPath, "utf8"), css, profile));
6948
+ findings.push(...conventionsFindings(readFileSync13(entryPath, "utf8"), css, profile));
6682
6949
  }
6683
6950
  return { findings, tokensAbsent: tokensCss === void 0 && !/var\(\s*--/.test(css) };
6684
6951
  }
@@ -6779,8 +7046,8 @@ var init_effect_geometry = __esm({
6779
7046
  });
6780
7047
 
6781
7048
  // packages/verify/src/bundle-score.ts
6782
- import { existsSync as existsSync15, mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "node:fs";
6783
- import path20 from "node:path";
7049
+ import { existsSync as existsSync15, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "node:fs";
7050
+ import path21 from "node:path";
6784
7051
  import { build as build4 } from "esbuild";
6785
7052
  import { chromium as chromium4 } from "playwright-core";
6786
7053
  function getFontFaces2() {
@@ -6834,7 +7101,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
6834
7101
  }
6835
7102
  function metadataRoot(set, rep) {
6836
7103
  try {
6837
- const text = JSON.parse(readFileSync12(path20.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
7104
+ const text = JSON.parse(readFileSync14(path21.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
6838
7105
  return parseMetadataStructure(text);
6839
7106
  } catch {
6840
7107
  return void 0;
@@ -6889,19 +7156,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
6889
7156
  });
6890
7157
  }
6891
7158
  function repMeta(set, rep) {
6892
- const text = JSON.parse(readFileSync12(path20.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
7159
+ const text = JSON.parse(readFileSync14(path21.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
6893
7160
  const root = parseMetadataStructure(text);
6894
7161
  return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
6895
7162
  }
6896
7163
  function repRef(set, rep) {
6897
- const env = JSON.parse(readFileSync12(path20.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
7164
+ const env = JSON.parse(readFileSync14(path21.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
6898
7165
  return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
6899
7166
  }
6900
7167
  function repEffectExtents(set, rep) {
6901
- const file = path20.join(set, rep, "get_design_context.json");
7168
+ const file = path21.join(set, rep, "get_design_context.json");
6902
7169
  if (!existsSync15(file)) return void 0;
6903
7170
  try {
6904
- const text = JSON.parse(readFileSync12(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
7171
+ const text = JSON.parse(readFileSync14(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
6905
7172
  const extents = shadowExtents(text);
6906
7173
  return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
6907
7174
  } catch {
@@ -6909,16 +7176,19 @@ function repEffectExtents(set, rep) {
6909
7176
  }
6910
7177
  }
6911
7178
  async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
6912
- if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
7179
+ if (opts.evidenceDir !== void 0) {
7180
+ mkdirSync3(opts.evidenceDir, { recursive: true });
7181
+ writeFileSync6(path21.join(opts.evidenceDir, "diff-legend.txt"), DIFF_LEGEND_TEXT);
7182
+ }
6913
7183
  const CONFIGS2 = task.configs;
6914
7184
  const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
6915
- const entryTsx = path20.join(bundleDir, task.entry);
7185
+ const entryTsx = path21.join(bundleDir, task.entry);
6916
7186
  if (!existsSync15(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
6917
7187
  const css = candidateCss(bundleDir);
6918
7188
  const mountSrc = `
6919
7189
  import { createElement } from "react";
6920
7190
  import { createRoot } from "react-dom/client";
6921
- import * as B from ${JSON.stringify(path20.resolve(entryTsx))};
7191
+ import * as B from ${JSON.stringify(path21.resolve(entryTsx))};
6922
7192
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
6923
7193
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
6924
7194
  const root = document.getElementById("root");
@@ -7012,16 +7282,17 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
7012
7282
  const pass = r.similarity >= bar.sim && r.inkRecall >= bar.ink && !slab;
7013
7283
  const region = pass ? void 0 : localizeDifference(scored, ref);
7014
7284
  const absent = absentInkClusters(scored, ref, backdrop).map((c) => {
7285
+ if (c.chrome === true) return c;
7015
7286
  const name = deepestNodeNameAt(task.set, cfg.rep, c.memberX - seat.x, c.memberY - seat.y);
7016
7287
  return name === void 0 ? c : { ...c, name };
7017
7288
  });
7018
7289
  if (opts.evidenceDir !== void 0) {
7019
- writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
7020
- writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
7021
- writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref, [], backdrop));
7290
+ writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
7291
+ writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
7292
+ writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref, [], backdrop));
7022
7293
  for (const [i, c] of absent.slice(0, 3).entries()) {
7023
- writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
7024
- writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
7294
+ writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
7295
+ writeFileSync6(path21.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
7025
7296
  }
7026
7297
  }
7027
7298
  return {
@@ -7268,7 +7539,7 @@ var init_parity = __esm({
7268
7539
  // packages/verify/src/composition.ts
7269
7540
  import { createRequire as createRequire2 } from "node:module";
7270
7541
  import { existsSync as existsSync16 } from "node:fs";
7271
- import path21 from "node:path";
7542
+ import path22 from "node:path";
7272
7543
  import { build as build6 } from "esbuild";
7273
7544
  import { chromium as chromium7 } from "playwright-core";
7274
7545
  function getFontFaces4() {
@@ -7276,9 +7547,9 @@ function getFontFaces4() {
7276
7547
  return _fontFaces4;
7277
7548
  }
7278
7549
  async function compileInstrumentedMount(task, bundleDir, composedParts = []) {
7279
- const entryTsx = path21.join(bundleDir, task.entry);
7550
+ const entryTsx = path22.join(bundleDir, task.entry);
7280
7551
  if (!existsSync16(entryTsx)) return { error: `${task.entry} missing` };
7281
- const requireFromVerify = createRequire2(path21.join(VERIFY_PKG_DIR, "package.json"));
7552
+ const requireFromVerify = createRequire2(path22.join(VERIFY_PKG_DIR, "package.json"));
7282
7553
  let realJsxPath;
7283
7554
  try {
7284
7555
  realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
@@ -7289,8 +7560,8 @@ async function compileInstrumentedMount(task, bundleDir, composedParts = []) {
7289
7560
  import { createElement } from "react";
7290
7561
  import { createRoot } from "react-dom/client";
7291
7562
  import { __registerParts } from "react/jsx-runtime";
7292
- import * as B from ${JSON.stringify(path21.resolve(entryTsx))};
7293
- ${composedParts.map((p, i) => `import * as CP${i} from ${JSON.stringify(path21.resolve(bundleDir, p.modulePath))};`).join("\n")}
7563
+ import * as B from ${JSON.stringify(path22.resolve(entryTsx))};
7564
+ ${composedParts.map((p, i) => `import * as CP${i} from ${JSON.stringify(path22.resolve(bundleDir, p.modulePath))};`).join("\n")}
7294
7565
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
7295
7566
  const pairs: Array<[unknown, string]> = [];
7296
7567
  for (const name of cfg.partComponents) {
@@ -7346,7 +7617,7 @@ function expectedParts(task, roles, mainSlug) {
7346
7617
  }
7347
7618
  function interiorRegions(setDir, roles) {
7348
7619
  const mains = roles.main;
7349
- const withInterior = mains.filter((m) => existsSync16(path21.join(setDir, m, "get_metadata_interior.json")));
7620
+ const withInterior = mains.filter((m) => existsSync16(path22.join(setDir, m, "get_metadata_interior.json")));
7350
7621
  if (withInterior.length === 0) {
7351
7622
  return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
7352
7623
  }
@@ -7620,16 +7891,16 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
7620
7891
 
7621
7892
  // packages/verify/src/occlusion.ts
7622
7893
  import { existsSync as existsSync17 } from "node:fs";
7623
- import path22 from "node:path";
7894
+ import path23 from "node:path";
7624
7895
  import { build as build7 } from "esbuild";
7625
7896
  import { chromium as chromium8 } from "playwright-core";
7626
7897
  async function compileTwoUp(task, bundleDir) {
7627
- const entryTsx = path22.join(bundleDir, task.entry);
7898
+ const entryTsx = path23.join(bundleDir, task.entry);
7628
7899
  if (!existsSync17(entryTsx)) return { error: `${task.entry} missing` };
7629
7900
  const src = `
7630
7901
  import { createElement } from "react";
7631
7902
  import { createRoot } from "react-dom/client";
7632
- import * as B from ${JSON.stringify(path22.resolve(entryTsx))};
7903
+ import * as B from ${JSON.stringify(path23.resolve(entryTsx))};
7633
7904
  const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
7634
7905
  const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
7635
7906
  for (const id of ["first", "second"]) {
@@ -7880,26 +8151,26 @@ var init_src5 = __esm({
7880
8151
  });
7881
8152
 
7882
8153
  // packages/cli/src/environment.ts
7883
- import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
7884
- import path23 from "node:path";
7885
- import { createHash as createHash4 } from "node:crypto";
8154
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "node:fs";
8155
+ import path24 from "node:path";
8156
+ import { createHash as createHash5 } from "node:crypto";
7886
8157
  import { fileURLToPath as fileURLToPath5 } from "node:url";
7887
8158
  function cliVersion() {
7888
8159
  try {
7889
- return JSON.parse(readFileSync14(path23.join(path23.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
8160
+ return JSON.parse(readFileSync16(path24.join(path24.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
7890
8161
  } catch {
7891
8162
  return "dev";
7892
8163
  }
7893
8164
  }
7894
8165
  function environmentStamp(taskFamilies) {
7895
- const manifestPath2 = path23.join(fontCacheDir(), "manifest.json");
8166
+ const manifestPath2 = path24.join(fontCacheDir(), "manifest.json");
7896
8167
  let fontsHash = null;
7897
8168
  if (existsSync18(manifestPath2)) {
7898
8169
  try {
7899
- const entries = JSON.parse(readFileSync14(manifestPath2, "utf8"));
8170
+ const entries = JSON.parse(readFileSync16(manifestPath2, "utf8"));
7900
8171
  const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
7901
8172
  const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
7902
- fontsHash = faces.length === 0 ? null : createHash4("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
8173
+ fontsHash = faces.length === 0 ? null : createHash5("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
7903
8174
  } catch {
7904
8175
  fontsHash = null;
7905
8176
  }
@@ -7940,8 +8211,8 @@ var init_describe = __esm({
7940
8211
  });
7941
8212
 
7942
8213
  // packages/cli/src/env.ts
7943
- import { existsSync as existsSync19, readFileSync as readFileSync15 } from "node:fs";
7944
- import path24 from "node:path";
8214
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "node:fs";
8215
+ import path25 from "node:path";
7945
8216
  function parseEnv(content) {
7946
8217
  const entries = /* @__PURE__ */ new Map();
7947
8218
  for (const line of content.split("\n")) {
@@ -7953,9 +8224,9 @@ function parseEnv(content) {
7953
8224
  function resolveCredential(name) {
7954
8225
  const fromProcess = process.env[name];
7955
8226
  if (fromProcess) return fromProcess;
7956
- const envPath = path24.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
8227
+ const envPath = path25.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
7957
8228
  if (!existsSync19(envPath)) return void 0;
7958
- return parseEnv(readFileSync15(envPath, "utf8")).get(name);
8229
+ return parseEnv(readFileSync17(envPath, "utf8")).get(name);
7959
8230
  }
7960
8231
  var init_env = __esm({
7961
8232
  "packages/cli/src/env.ts"() {
@@ -8014,18 +8285,200 @@ var init_output = __esm({
8014
8285
  }
8015
8286
  });
8016
8287
 
8288
+ // packages/cli/src/publish-client.ts
8289
+ import { chmodSync, existsSync as existsSync20, mkdirSync as mkdirSync4, readFileSync as readFileSync18, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "node:fs";
8290
+ import os4 from "node:os";
8291
+ import path26 from "node:path";
8292
+ function sessionPath() {
8293
+ return process.env["TENDRIL_SESSION_PATH"] ?? path26.join(os4.homedir(), ".tendril", "session.json");
8294
+ }
8295
+ function readStoredSession(file = sessionPath()) {
8296
+ if (!existsSync20(file)) return void 0;
8297
+ try {
8298
+ const parsed = JSON.parse(readFileSync18(file, "utf8"));
8299
+ if (typeof parsed.origin !== "string" || typeof parsed.token !== "string") return void 0;
8300
+ return { origin: parsed.origin, token: parsed.token };
8301
+ } catch {
8302
+ return void 0;
8303
+ }
8304
+ }
8305
+ function writeStoredSession(session, file = sessionPath()) {
8306
+ mkdirSync4(path26.dirname(file), { recursive: true });
8307
+ writeFileSync7(file, `${JSON.stringify(session, null, 2)}
8308
+ `, { mode: 384 });
8309
+ chmodSync(file, 384);
8310
+ }
8311
+ function clearStoredSession(file = sessionPath()) {
8312
+ if (existsSync20(file)) rmSync3(file);
8313
+ }
8314
+ function tokenFor(origin, file = sessionPath()) {
8315
+ const fromEnv = process.env["TENDRIL_TOKEN"];
8316
+ if (fromEnv !== void 0 && fromEnv !== "") {
8317
+ const pinned = (process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "");
8318
+ if (pinned !== "" && pinned !== origin) return { ok: false, reason: "origin-mismatch", from: "env", boundTo: pinned };
8319
+ return { ok: true, token: fromEnv, from: "env" };
8320
+ }
8321
+ const stored = readStoredSession(file);
8322
+ if (stored === void 0) return { ok: false, reason: "none" };
8323
+ if (stored.origin !== origin) return { ok: false, reason: "origin-mismatch", from: "file", boundTo: stored.origin };
8324
+ return { ok: true, token: stored.token, from: "file" };
8325
+ }
8326
+ function isSecureOrigin(origin) {
8327
+ let url;
8328
+ try {
8329
+ url = new URL(origin);
8330
+ } catch {
8331
+ return false;
8332
+ }
8333
+ if (url.protocol === "https:") return true;
8334
+ return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]");
8335
+ }
8336
+ function isRecord(value) {
8337
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8338
+ }
8339
+ function delay(ms) {
8340
+ return new Promise((resolve) => setTimeout(resolve, ms));
8341
+ }
8342
+ var InsecureOriginError, HttpPublishClient, REQUEST_TIMEOUT_MS, RETRIES, BACKOFF_MS;
8343
+ var init_publish_client = __esm({
8344
+ "packages/cli/src/publish-client.ts"() {
8345
+ "use strict";
8346
+ InsecureOriginError = class extends Error {
8347
+ constructor(origin) {
8348
+ super(`${origin} is not https, so a session token sent there would travel in the clear`);
8349
+ this.origin = origin;
8350
+ this.name = "InsecureOriginError";
8351
+ }
8352
+ origin;
8353
+ };
8354
+ HttpPublishClient = class {
8355
+ origin;
8356
+ token;
8357
+ send;
8358
+ constructor(options) {
8359
+ this.origin = options.origin.replace(/\/+$/, "");
8360
+ if (!isSecureOrigin(this.origin)) throw new InsecureOriginError(this.origin);
8361
+ this.token = options.token;
8362
+ this.send = options.fetch ?? globalThis.fetch;
8363
+ }
8364
+ acceptTerms(input) {
8365
+ return this.json("POST", "/api/consent", input);
8366
+ }
8367
+ begin(input) {
8368
+ return this.json("POST", "/api/publications", input, true);
8369
+ }
8370
+ async upload(input) {
8371
+ const encoded = input.relPath.split("/").map(encodeURIComponent).join("/");
8372
+ return this.request("PUT", `/api/publications/${encodeURIComponent(input.publicationId)}/objects/${encoded}`, {
8373
+ body: input.bytes,
8374
+ contentType: "application/octet-stream",
8375
+ retryable: true
8376
+ });
8377
+ }
8378
+ commit(input) {
8379
+ return this.json("POST", `/api/publications/${encodeURIComponent(input.publicationId)}/commit`, {}, true);
8380
+ }
8381
+ issueShareLink(input) {
8382
+ return this.json("POST", `/api/publications/${encodeURIComponent(input.publicationId)}/shares`, {
8383
+ expiresAt: input.expiresAt,
8384
+ recipientEmail: input.recipientEmail
8385
+ });
8386
+ }
8387
+ listShareLinks(input) {
8388
+ return this.request("GET", `/api/publications/${encodeURIComponent(input.publicationId)}/shares`, {
8389
+ body: "",
8390
+ contentType: "application/json",
8391
+ retryable: true
8392
+ });
8393
+ }
8394
+ revokeShareLink(input) {
8395
+ return this.request("DELETE", `/api/publications/${encodeURIComponent(input.publicationId)}/shares/${encodeURIComponent(input.shareLinkId)}`, {
8396
+ body: "",
8397
+ contentType: "application/json",
8398
+ retryable: true
8399
+ });
8400
+ }
8401
+ json(method, pathname, body, retryable = false) {
8402
+ return this.request(method, pathname, { body: JSON.stringify(body), contentType: "application/json", retryable });
8403
+ }
8404
+ async request(method, pathname, init) {
8405
+ let response;
8406
+ let lastError = "";
8407
+ const attempts = init.retryable === true ? RETRIES : 1;
8408
+ for (let attempt = 1; ; attempt += 1) {
8409
+ try {
8410
+ response = await this.send(`${this.origin}${pathname}`, {
8411
+ method,
8412
+ headers: { authorization: `Bearer ${this.token}`, "content-type": init.contentType },
8413
+ // GET and DELETE carry nothing. `fetch` THROWS on a GET with a
8414
+ // body — even an empty string — so this is a hard requirement
8415
+ // rather than tidiness.
8416
+ ...method === "GET" || method === "HEAD" || init.body === "" ? {} : { body: init.body },
8417
+ // A publish that hangs forever is worse than one that fails:
8418
+ // the user cannot tell it from slow, and there is nothing to
8419
+ // act on. Node's fetch has no default timeout at all.
8420
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
8421
+ });
8422
+ } catch (error) {
8423
+ lastError = error.message;
8424
+ if (attempt >= attempts) {
8425
+ return {
8426
+ ok: false,
8427
+ status: 0,
8428
+ refusal: `could not reach ${this.origin} after ${String(attempt)} ${attempt === 1 ? "try" : "tries"}: ${lastError}`
8429
+ };
8430
+ }
8431
+ await delay(BACKOFF_MS * attempt);
8432
+ continue;
8433
+ }
8434
+ if (response.status >= 500 && attempt < attempts) {
8435
+ await delay(BACKOFF_MS * attempt);
8436
+ continue;
8437
+ }
8438
+ break;
8439
+ }
8440
+ const text = await response.text();
8441
+ let parsed;
8442
+ try {
8443
+ parsed = JSON.parse(text);
8444
+ } catch {
8445
+ return {
8446
+ ok: false,
8447
+ status: response.status,
8448
+ refusal: `${this.origin} answered with ${String(response.status)} and something that is not JSON, so it is probably not a Tendril portal`
8449
+ };
8450
+ }
8451
+ const record = parsed ?? {};
8452
+ if (response.ok) return { ok: true, value: parsed };
8453
+ return {
8454
+ ok: false,
8455
+ status: response.status,
8456
+ refusal: typeof record["refusal"] === "string" ? record["refusal"] : `the portal refused with ${String(response.status)}`,
8457
+ ...Array.isArray(record["detail"]) ? { detail: record["detail"] } : {},
8458
+ ...Array.isArray(record["missing"]) ? { missing: record["missing"] } : {},
8459
+ ...isRecord(record["needsConsent"]) ? { needsConsent: record["needsConsent"] } : {},
8460
+ ...isRecord(record["needsConfirmation"]) ? { needsConfirmation: record["needsConfirmation"] } : {}
8461
+ };
8462
+ }
8463
+ };
8464
+ REQUEST_TIMEOUT_MS = 12e4;
8465
+ RETRIES = 3;
8466
+ BACKOFF_MS = 500;
8467
+ }
8468
+ });
8469
+
8017
8470
  // packages/cli/src/entitlement.ts
8018
- import { chmodSync, existsSync as existsSync20, mkdirSync as mkdirSync4, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "node:fs";
8471
+ import { chmodSync as chmodSync2, existsSync as existsSync21, mkdirSync as mkdirSync5, readFileSync as readFileSync19, writeFileSync as writeFileSync8 } from "node:fs";
8019
8472
  import crypto from "node:crypto";
8020
- import os4 from "node:os";
8021
- import path25 from "node:path";
8473
+ import os5 from "node:os";
8474
+ import path27 from "node:path";
8022
8475
  function entitlementPath() {
8023
- return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path25.join(os4.homedir(), ".tendril", "entitlement.json");
8476
+ return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path27.join(os5.homedir(), ".tendril", "entitlement.json");
8024
8477
  }
8025
8478
  function readStoredEntitlement(file = entitlementPath()) {
8026
- if (!existsSync20(file)) return void 0;
8479
+ if (!existsSync21(file)) return void 0;
8027
8480
  try {
8028
- const parsed = JSON.parse(readFileSync16(file, "utf8"));
8481
+ const parsed = JSON.parse(readFileSync19(file, "utf8"));
8029
8482
  if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
8030
8483
  return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
8031
8484
  } catch {
@@ -8033,10 +8486,10 @@ function readStoredEntitlement(file = entitlementPath()) {
8033
8486
  }
8034
8487
  }
8035
8488
  function writeStoredEntitlement(stored, file = entitlementPath()) {
8036
- mkdirSync4(path25.dirname(file), { recursive: true });
8037
- writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
8489
+ mkdirSync5(path27.dirname(file), { recursive: true });
8490
+ writeFileSync8(file, `${JSON.stringify(stored, null, 2)}
8038
8491
  `);
8039
- chmodSync(file, 384);
8492
+ chmodSync2(file, 384);
8040
8493
  }
8041
8494
  function parseEntitlementToken(token) {
8042
8495
  if (!token.startsWith(ENT_PREFIX)) return { error: "not a tendril entitlement token" };
@@ -8118,9 +8571,9 @@ var init_entitlement = __esm({
8118
8571
 
8119
8572
  // packages/cli/src/commands/doctor.ts
8120
8573
  import { spawnSync } from "node:child_process";
8121
- import { existsSync as existsSync21, readFileSync as readFileSync17, readdirSync as readdirSync7 } from "node:fs";
8122
- import os5 from "node:os";
8123
- import path26 from "node:path";
8574
+ import { existsSync as existsSync22, readFileSync as readFileSync20, readdirSync as readdirSync8 } from "node:fs";
8575
+ import os6 from "node:os";
8576
+ import path28 from "node:path";
8124
8577
  function withDeadline(work, ms) {
8125
8578
  return Promise.race([
8126
8579
  work,
@@ -8180,19 +8633,19 @@ async function runDoctorChecks(options) {
8180
8633
  remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
8181
8634
  });
8182
8635
  }
8183
- const fontManifest = path26.join(fontCacheDir(), "manifest.json");
8636
+ const fontManifest = path28.join(fontCacheDir(), "manifest.json");
8184
8637
  checks.push(
8185
- existsSync21(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync17(fontManifest, "utf8")).length} faces)` } : {
8638
+ existsSync22(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync20(fontManifest, "utf8")).length} faces)` } : {
8186
8639
  name: "font-cache",
8187
8640
  ok: true,
8188
8641
  detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
8189
8642
  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.`
8190
8643
  }
8191
8644
  );
8192
- const pluginRoot = path26.join(os5.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
8193
- if (existsSync21(pluginRoot)) {
8645
+ const pluginRoot = path28.join(os6.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
8646
+ if (existsSync22(pluginRoot)) {
8194
8647
  try {
8195
- const versions = readdirSync7(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
8648
+ const versions = readdirSync8(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
8196
8649
  const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
8197
8650
  if (newest !== void 0) {
8198
8651
  const skewed = versionIsNewer(newest, cliVersion());
@@ -8226,13 +8679,52 @@ async function runDoctorChecks(options) {
8226
8679
  checks.push(
8227
8680
  ent.ok ? ent.mode === "pre-launch" ? { name: "entitlement", ok: true, detail: "pre-launch build \u2014 entitlement gates present but unarmed (no public key shipped); record/generate run freely" } : { name: "entitlement", ok: true, detail: `active plan "${ent.claims.plan}" until ${new Date(ent.claims.exp).toISOString().slice(0, 10)}${ent.stale ? " (stale \u2014 renews at next opportunity)" : ""}` } : { name: "entitlement", ok: false, detail: ent.error, remediation: ent.remediation }
8228
8681
  );
8682
+ checks.push(await portalSessionCheck(options.fetchImpl ?? fetch));
8229
8683
  const figmaToken = resolveCredential("FIGMA_TOKEN");
8230
8684
  checks.push({
8231
8685
  name: "figma-pat",
8232
8686
  ok: true,
8233
8687
  detail: figmaToken ? "FIGMA_TOKEN configured but unused \u2014 recording runs over the Figma MCP transport" : "FIGMA_TOKEN not set \u2014 not needed; recording runs over the Figma MCP transport"
8234
8688
  });
8235
- return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key" && c.name !== "path-skew").every((c) => c.ok), checks };
8689
+ return {
8690
+ ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key" && c.name !== "path-skew" && c.name !== "portal-session").every((c) => c.ok),
8691
+ checks
8692
+ };
8693
+ }
8694
+ async function portalSessionCheck(fetchImpl) {
8695
+ const stored = readStoredSession();
8696
+ const connectHint = `Run ${tendrilCommand("login")} and approve in your browser (agents: call the tendril_login tool, relay the link and code, then tendril_login_wait) \u2014 or mint a token under "Connect your CLI" in your portal library and use ${tendrilCommand("login --paste")}.`;
8697
+ if (stored === void 0) {
8698
+ return { name: "portal-session", ok: true, detail: `not signed in to a portal \u2014 publishing needs it, verification never does. ${connectHint}` };
8699
+ }
8700
+ let response;
8701
+ try {
8702
+ const ctl = new AbortController();
8703
+ const timer = setTimeout(() => ctl.abort(), 5e3);
8704
+ timer.unref?.();
8705
+ response = await fetchImpl(`${stored.origin}/api/whoami`, { headers: { authorization: `Bearer ${stored.token}` }, signal: ctl.signal });
8706
+ clearTimeout(timer);
8707
+ } catch {
8708
+ return { name: "portal-session", ok: true, detail: `${stored.origin} did not answer \u2014 the stored session could not be checked. Publishing will say so precisely if it is actually dead.` };
8709
+ }
8710
+ if (!response.ok) {
8711
+ return {
8712
+ name: "portal-session",
8713
+ ok: false,
8714
+ detail: `the session stored for ${stored.origin} no longer works there (${String(response.status)}) \u2014 it was revoked or expired`,
8715
+ remediation: connectHint
8716
+ };
8717
+ }
8718
+ const body = await response.json();
8719
+ const email = typeof body.email === "string" && body.email !== "" ? ` as ${body.email}` : "";
8720
+ const expiry = typeof body.sessionExpiresAt === "string" ? body.sessionExpiresAt : null;
8721
+ const daysLeft = expiry === null ? null : Math.floor((Date.parse(expiry) - Date.now()) / 864e5);
8722
+ return {
8723
+ name: "portal-session",
8724
+ ok: true,
8725
+ detail: `signed in to ${stored.origin}${email}${expiry === null ? "" : `, session good until ${expiry.slice(0, 10)}`}`,
8726
+ ...daysLeft !== null && daysLeft <= 7 ? { remediation: `That is ${String(daysLeft)} day(s) away \u2014 run ${tendrilCommand("login")} soon for a fresh session.` } : {}
8727
+ };
8236
8728
  }
8237
8729
  function probeVersion(binary) {
8238
8730
  const windowsShim = /\.(cmd|bat)$/i.test(binary);
@@ -8307,6 +8799,7 @@ var init_doctor = __esm({
8307
8799
  init_environment();
8308
8800
  init_invocation();
8309
8801
  init_output();
8802
+ init_publish_client();
8310
8803
  init_entitlement();
8311
8804
  DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
8312
8805
  DOCTOR_DESCRIPTION = {
@@ -9422,8 +9915,8 @@ var init_engine_curated = __esm({
9422
9915
  });
9423
9916
 
9424
9917
  // packages/generate/src/loop.ts
9425
- import { existsSync as existsSync23, mkdirSync as mkdirSync6, readFileSync as readFileSync19, renameSync, writeFileSync as writeFileSync9 } from "node:fs";
9426
- import path29 from "node:path";
9918
+ import { existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync22, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
9919
+ import path31 from "node:path";
9427
9920
  import { z as z13 } from "zod";
9428
9921
  function objective(scores, behaviors) {
9429
9922
  const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
@@ -9465,9 +9958,9 @@ ${absentLines.join("\n")}` : ""}
9465
9958
  Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Reading the pair: ink credits a recorded pixel when ANY render ink lands within 1px of it, so ink=1 proves coverage \u2014 not presence, shape, or colour. A wrong-shaped mark over the right region, or a wrong-coloured one that is still ink, scores 1.000; and a recorded mark within ~30 channel-sum of the backdrop (#f6f6f6 on white is 27) is not ink at all, so it can be absent at ink 1.000 with no MISSING line. With ink=1 AND low sim, coverage held while pixels differ, so start with the TWO causes below \u2014 but never read ink=1 as "nothing is missing": confirm every faint or small recorded mark (hairlines, dividers, low-contrast controls) exists in your render. GEOMETRY: wrong position/size/radius; the diff shows shifted edges and bands. WRONG TEXT WEIGHT OR FACE: the diff shows a uniform haze over glyph runs; CSS binds the font FAMILY before the weight, a later family in the stack is never consulted for a weight the first one lacks, and font-synthesis: none rules out faux-bold \u2014 so a stack led by a family the kit holds at one weight renders every other weight in that face, silently. Check the font stack against the kit's cached faces before concluding geometry is at fault. LOW ink with high sim means recorded marks are absent or painted invisibly. Do not regress PASS configs. ${mode === "files" ? "Update the files in your candidate directory and re-run the score." : "Reply with the complete corrected files in the same FILE format."}`;
9466
9959
  }
9467
9960
  function archivePriorRun(outDir) {
9468
- if (!existsSync23(path29.join(outDir, "run-log.json")) && !existsSync23(path29.join(outDir, "loop-state.json"))) return void 0;
9961
+ if (!existsSync24(path31.join(outDir, "run-log.json")) && !existsSync24(path31.join(outDir, "loop-state.json"))) return void 0;
9469
9962
  let n = 1;
9470
- while (existsSync23(`${outDir}-prev-${n}`)) n += 1;
9963
+ while (existsSync24(`${outDir}-prev-${n}`)) n += 1;
9471
9964
  renameSync(outDir, `${outDir}-prev-${n}`);
9472
9965
  return `${outDir}-prev-${n}`;
9473
9966
  }
@@ -9476,14 +9969,14 @@ async function runEngineLoop(opts) {
9476
9969
  const plateau = opts.plateau ?? 2;
9477
9970
  const progress = opts.onProgress ?? (() => {
9478
9971
  });
9479
- const statePath = path29.join(opts.outDir, "loop-state.json");
9480
- const resuming = opts.resume === true && existsSync23(statePath);
9972
+ const statePath = path31.join(opts.outDir, "loop-state.json");
9973
+ const resuming = opts.resume === true && existsSync24(statePath);
9481
9974
  if (!resuming) {
9482
9975
  const archived = archivePriorRun(opts.outDir);
9483
9976
  if (archived !== void 0) progress(`previous run archived to ${archived}`);
9484
9977
  }
9485
- mkdirSync6(opts.outDir, { recursive: true });
9486
- const scratch = path29.join(opts.outDir, ".candidate");
9978
+ mkdirSync7(opts.outDir, { recursive: true });
9979
+ const scratch = path31.join(opts.outDir, ".candidate");
9487
9980
  let attempts = [];
9488
9981
  let log = [];
9489
9982
  let best;
@@ -9491,7 +9984,7 @@ async function runEngineLoop(opts) {
9491
9984
  let nonAccepted = 0;
9492
9985
  let stopReason = "max-iterations";
9493
9986
  if (resuming) {
9494
- const restored = LoopStateSchema.parse(JSON.parse(readFileSync19(statePath, "utf8")));
9987
+ const restored = LoopStateSchema.parse(JSON.parse(readFileSync22(statePath, "utf8")));
9495
9988
  attempts = restored.attempts;
9496
9989
  log = restored.iterations;
9497
9990
  spentUsd = restored.spentUsd;
@@ -9506,12 +9999,12 @@ async function runEngineLoop(opts) {
9506
9999
  progress(`resumed: ${log.length} iteration(s), $${spentUsd.toFixed(3)} spent, best pass=${best?.objective[0] ?? 0}`);
9507
10000
  }
9508
10001
  const persist = () => {
9509
- writeFileSync9(statePath, `${JSON.stringify({ version: 1, spentUsd, attempts, iterations: log }, null, 1)}
10002
+ writeFileSync10(statePath, `${JSON.stringify({ version: 1, spentUsd, attempts, iterations: log }, null, 1)}
9510
10003
  `);
9511
10004
  };
9512
10005
  const writeCandidate = (files) => {
9513
- mkdirSync6(scratch, { recursive: true });
9514
- for (const [name, content] of Object.entries(files)) writeFileSync9(path29.join(scratch, name), content);
10006
+ mkdirSync7(scratch, { recursive: true });
10007
+ for (const [name, content] of Object.entries(files)) writeFileSync10(path31.join(scratch, name), content);
9515
10008
  };
9516
10009
  const scoreCandidate = async (candidate, iter, usd, modelMs) => {
9517
10010
  writeCandidate(candidate.files);
@@ -9569,8 +10062,8 @@ async function runEngineLoop(opts) {
9569
10062
  const usd = candidate.usage?.usd ?? 0;
9570
10063
  spentUsd += usd;
9571
10064
  if (candidate.raw !== void 0) {
9572
- mkdirSync6(path29.join(opts.outDir, "responses"), { recursive: true });
9573
- writeFileSync9(path29.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
10065
+ mkdirSync7(path31.join(opts.outDir, "responses"), { recursive: true });
10066
+ writeFileSync10(path31.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
9574
10067
  }
9575
10068
  if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
9576
10069
  const finish = candidate.usage?.finishReason ?? "?";
@@ -9596,10 +10089,10 @@ async function runEngineLoop(opts) {
9596
10089
  }
9597
10090
  }
9598
10091
  }
9599
- if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync9(path29.join(opts.outDir, name), content);
10092
+ if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path31.join(opts.outDir, name), content);
9600
10093
  const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
9601
- writeFileSync9(
9602
- path29.join(opts.outDir, "run-log.json"),
10094
+ writeFileSync10(
10095
+ path31.join(opts.outDir, "run-log.json"),
9603
10096
  `${JSON.stringify(
9604
10097
  {
9605
10098
  ...opts.meta,
@@ -9666,8 +10159,8 @@ var init_loop2 = __esm({
9666
10159
  });
9667
10160
 
9668
10161
  // packages/generate/src/brief.ts
9669
- import { existsSync as existsSync24, readFileSync as readFileSync20 } from "node:fs";
9670
- import path30 from "node:path";
10162
+ import { existsSync as existsSync25, readFileSync as readFileSync23 } from "node:fs";
10163
+ import path32 from "node:path";
9671
10164
  import { PNG as PNG3 } from "pngjs";
9672
10165
  function singleAxes2(name) {
9673
10166
  const parsed = parseVariantAxes(name);
@@ -10107,15 +10600,15 @@ function authorBehaviors(api, extras = {}) {
10107
10600
  };
10108
10601
  }
10109
10602
  function envelopeText(file) {
10110
- return envelopeFirstTextPart(JSON.parse(readFileSync20(file, "utf8")));
10603
+ return envelopeFirstTextPart(JSON.parse(readFileSync23(file, "utf8")));
10111
10604
  }
10112
10605
  function metadataText(file) {
10113
- return envelopeTextContent(JSON.parse(readFileSync20(file, "utf8")));
10606
+ return envelopeTextContent(JSON.parse(readFileSync23(file, "utf8")));
10114
10607
  }
10115
10608
  function dismissEvidence(setDir, repSlugs) {
10116
10609
  for (const slug of repSlugs) {
10117
- const f = path30.join(setDir, slug, "get_design_context.json");
10118
- if (!existsSync24(f)) continue;
10610
+ const f = path32.join(setDir, slug, "get_design_context.json");
10611
+ if (!existsSync25(f)) continue;
10119
10612
  const text = envelopeText(f);
10120
10613
  const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*true\b/i.exec(text);
10121
10614
  if (propHit !== null) return { evidence: `emission prop "${propHit[1]}"`, visibleReps: [] };
@@ -10142,9 +10635,9 @@ function dismissEvidence(setDir, repSlugs) {
10142
10635
  const glyphIsTheComponent = (() => {
10143
10636
  const slugToCheck = vis.visibleIn[0];
10144
10637
  if (slugToCheck === void 0) return false;
10145
- const metaFile = path30.join(setDir, slugToCheck, "get_metadata.json");
10638
+ const metaFile = path32.join(setDir, slugToCheck, "get_metadata.json");
10146
10639
  try {
10147
- const root = parseMetadataStructure(envelopeTextContent(JSON.parse(readFileSync20(metaFile, "utf8"))));
10640
+ const root = parseMetadataStructure(envelopeTextContent(JSON.parse(readFileSync23(metaFile, "utf8"))));
10148
10641
  if (root.children.length !== 1) return false;
10149
10642
  const contains = (n) => normalizedLayerName(n.name) === norm2 || n.children.some(contains);
10150
10643
  return contains(root.children[0]);
@@ -10164,10 +10657,10 @@ function dismissEvidence(setDir, repSlugs) {
10164
10657
  return void 0;
10165
10658
  }
10166
10659
  function recordedReferencePng(setDir, slug) {
10167
- const f = path30.join(setDir, slug, "get_screenshot.json");
10168
- if (!existsSync24(f)) return void 0;
10660
+ const f = path32.join(setDir, slug, "get_screenshot.json");
10661
+ if (!existsSync25(f)) return void 0;
10169
10662
  try {
10170
- const env = JSON.parse(readFileSync20(f, "utf8")).content.find((c) => c.type === "image");
10663
+ const env = JSON.parse(readFileSync23(f, "utf8")).content.find((c) => c.type === "image");
10171
10664
  return env?.data === void 0 ? void 0 : Uint8Array.from(Buffer.from(env.data, "base64"));
10172
10665
  } catch {
10173
10666
  return void 0;
@@ -10208,7 +10701,7 @@ function recordedFontNeeds(setDir, opts = {}) {
10208
10701
  for (const m of text.matchAll(/font-\['([^':\]]+)(?::([^'\]]+))?'/g)) famAdd(m[1], m[2] === void 0 ? void 0 : styleWeight(m[2]));
10209
10702
  for (const m of text.matchAll(/family-name:var\([^,)]*,\s*'([^':\]]+)(?::([^'\]]+))?'/g)) famAdd(m[1], m[2] === void 0 ? void 0 : styleWeight(m[2]));
10210
10703
  for (const m of text.matchAll(/\bfont-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)\b/g)) unpaired.add(STYLE_WEIGHTS2[m[1]]);
10211
- for (const m of text.matchAll(/font-weight:\s*([1-9]00)\b/g)) unpaired.add(Number(m[1]));
10704
+ for (const m of text.matchAll(/font-weight:\s*([1-9]\d{2})\b/g)) unpaired.add(Number(m[1]));
10212
10705
  };
10213
10706
  const fromDefs = (text) => {
10214
10707
  let defs;
@@ -10226,15 +10719,34 @@ function recordedFontNeeds(setDir, opts = {}) {
10226
10719
  }
10227
10720
  if (/^[A-Za-z][A-Za-z0-9 ]{1,39}$/.test(v.trim())) famAdd(v.trim());
10228
10721
  }
10722
+ fromFontCalls(defs);
10229
10723
  };
10724
+ function fromFontCalls(defs) {
10725
+ for (const v of Object.values(defs)) {
10726
+ if (typeof v !== "string") continue;
10727
+ const call = /\bFont\(\s*family:\s*"?([^",]+)"?\s*,([^)]*)\)/.exec(v);
10728
+ if (call === null) continue;
10729
+ const rest = call[2];
10730
+ const explicit = /\bweight:\s*([1-9]\d{2})\b/.exec(rest);
10731
+ const styleRef = /\bstyle:\s*([^,)]+)/.exec(rest);
10732
+ let weight = explicit === null ? void 0 : Number(explicit[1]);
10733
+ if (weight === void 0 && styleRef !== null) {
10734
+ const ref = styleRef[1].trim().replace(/^"|"$/g, "");
10735
+ weight = styleWeight((typeof defs[ref] === "string" ? defs[ref] : ref).trim());
10736
+ }
10737
+ const famRef = call[1].trim();
10738
+ const family = typeof defs[famRef] === "string" ? defs[famRef].trim() : famRef;
10739
+ if (/^[A-Za-z][A-Za-z0-9 ]{1,39}$/.test(family)) famAdd(family, weight);
10740
+ }
10741
+ }
10230
10742
  const manifest = loadManifest(setDir);
10231
- const setDefs = path30.join(setDir, "get_variable_defs.json");
10232
- if (existsSync24(setDefs)) fromDefs(envelopeText(setDefs));
10743
+ const setDefs = path32.join(setDir, "get_variable_defs.json");
10744
+ if (existsSync25(setDefs)) fromDefs(envelopeText(setDefs));
10233
10745
  for (const rep of manifest.reps) {
10234
- const ctx = path30.join(setDir, rep.slug, "get_design_context.json");
10235
- if (existsSync24(ctx)) fromEmission(envelopeText(ctx));
10236
- const defs = path30.join(setDir, rep.slug, "get_variable_defs.json");
10237
- if (existsSync24(defs)) fromDefs(envelopeText(defs));
10746
+ const ctx = path32.join(setDir, rep.slug, "get_design_context.json");
10747
+ if (existsSync25(ctx)) fromEmission(envelopeText(ctx));
10748
+ const defs = path32.join(setDir, rep.slug, "get_variable_defs.json");
10749
+ if (existsSync25(defs)) fromDefs(envelopeText(defs));
10238
10750
  }
10239
10751
  return [...byFamily.entries()].map(([family, paired]) => {
10240
10752
  const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
@@ -10245,10 +10757,10 @@ function symbolFontGlyphCount(setDir, reps) {
10245
10757
  const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
10246
10758
  const glyphs = /* @__PURE__ */ new Set();
10247
10759
  for (const rep of reps) {
10248
- const file = path30.join(setDir, rep, "get_metadata.json");
10249
- if (!existsSync24(file)) continue;
10760
+ const file = path32.join(setDir, rep, "get_metadata.json");
10761
+ if (!existsSync25(file)) continue;
10250
10762
  try {
10251
- const text = JSON.parse(readFileSync20(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
10763
+ const text = JSON.parse(readFileSync23(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
10252
10764
  for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
10253
10765
  const name = decodeXmlEntities(m[1]).replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
10254
10766
  if (PUA.test(name)) glyphs.add(name);
@@ -10274,8 +10786,8 @@ function recordedTextSlots(setDir, repSlugs) {
10274
10786
  const propRep = [];
10275
10787
  const perRep = [];
10276
10788
  for (const slug of repSlugs) {
10277
- const f = path30.join(setDir, slug, "get_design_context.json");
10278
- if (!existsSync24(f)) continue;
10789
+ const f = path32.join(setDir, slug, "get_design_context.json");
10790
+ if (!existsSync25(f)) continue;
10279
10791
  const code = envelopeText(f);
10280
10792
  const props = /* @__PURE__ */ new Map();
10281
10793
  for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
@@ -10301,8 +10813,8 @@ function recordedTextSlots(setDir, repSlugs) {
10301
10813
  const axisValuesBySlug = /* @__PURE__ */ new Map();
10302
10814
  const valuesByAxis = /* @__PURE__ */ new Map();
10303
10815
  for (const slug of repSlugs) {
10304
- const metaFile = path30.join(setDir, slug, "get_metadata.json");
10305
- if (!existsSync24(metaFile)) continue;
10816
+ const metaFile = path32.join(setDir, slug, "get_metadata.json");
10817
+ if (!existsSync25(metaFile)) continue;
10306
10818
  const name = symbolName(metadataText(metaFile));
10307
10819
  if (name === void 0) continue;
10308
10820
  const values = /* @__PURE__ */ new Set();
@@ -10417,8 +10929,8 @@ function authorTaskFromSet(setDir, opts = {}) {
10417
10929
  const poses = [];
10418
10930
  const missing = [];
10419
10931
  for (const rep of manifest.reps) {
10420
- const metaFile = path30.join(setDir, rep.slug, "get_metadata.json");
10421
- if (!existsSync24(metaFile)) {
10932
+ const metaFile = path32.join(setDir, rep.slug, "get_metadata.json");
10933
+ if (!existsSync25(metaFile)) {
10422
10934
  missing.push(rep.slug);
10423
10935
  continue;
10424
10936
  }
@@ -10432,8 +10944,8 @@ function authorTaskFromSet(setDir, opts = {}) {
10432
10944
  if (missing.length > 0) {
10433
10945
  throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
10434
10946
  }
10435
- const setMeta = path30.join(setDir, "get_metadata.json");
10436
- const latticeNames = manifest.latticeNames ?? (existsSync24(setMeta) ? [...metadataText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
10947
+ const setMeta = path32.join(setDir, "get_metadata.json");
10948
+ const latticeNames = manifest.latticeNames ?? (existsSync25(setMeta) ? [...metadataText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
10437
10949
  const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
10438
10950
  const recordedFonts = recordedFontFamilies(setDir);
10439
10951
  const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
@@ -10634,17 +11146,17 @@ var init_brief = __esm({
10634
11146
  });
10635
11147
 
10636
11148
  // packages/generate/src/segments.ts
10637
- import { existsSync as existsSync25, readFileSync as readFileSync21, readdirSync as readdirSync9 } from "node:fs";
10638
- import path31 from "node:path";
11149
+ import { existsSync as existsSync26, readFileSync as readFileSync24, readdirSync as readdirSync10 } from "node:fs";
11150
+ import path33 from "node:path";
10639
11151
  function repText(set, rep, tool) {
10640
- const env = JSON.parse(readFileSync21(path31.join(set, rep, `${tool}.json`), "utf8"));
11152
+ const env = JSON.parse(readFileSync24(path33.join(set, rep, `${tool}.json`), "utf8"));
10641
11153
  return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
10642
11154
  }
10643
11155
  function refPngDims(set, rep) {
10644
- const f = path31.join(set, rep, "get_screenshot.json");
10645
- if (!existsSync25(f)) return void 0;
11156
+ const f = path33.join(set, rep, "get_screenshot.json");
11157
+ if (!existsSync26(f)) return void 0;
10646
11158
  try {
10647
- const env = JSON.parse(readFileSync21(f, "utf8")).content.find((c) => c.type === "image");
11159
+ const env = JSON.parse(readFileSync24(f, "utf8")).content.find((c) => c.type === "image");
10648
11160
  if (env?.data === void 0) return void 0;
10649
11161
  const buf = Buffer.from(env.data, "base64");
10650
11162
  if (buf.length < 24 || buf.readUInt32BE(0) !== 2303741511) return void 0;
@@ -10710,20 +11222,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
10710
11222
  }
10711
11223
  function buildSegments(task, mode = "fenced") {
10712
11224
  const SET = task.set;
10713
- let defsRecorded = existsSync25(path31.join(SET, "get_variable_defs.json"));
11225
+ let defsRecorded = existsSync26(path33.join(SET, "get_variable_defs.json"));
10714
11226
  let rawDefs = {};
10715
- if (existsSync25(path31.join(SET, "get_variable_defs.json"))) {
10716
- const text = envelopeFirstTextPart(JSON.parse(readFileSync21(path31.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
11227
+ if (existsSync26(path33.join(SET, "get_variable_defs.json"))) {
11228
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync24(path33.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
10717
11229
  try {
10718
11230
  rawDefs = JSON.parse(text);
10719
11231
  } catch {
10720
11232
  }
10721
11233
  } else {
10722
11234
  for (const cfg of task.configs) {
10723
- const f = path31.join(SET, cfg.rep, "get_variable_defs.json");
10724
- if (!existsSync25(f)) continue;
11235
+ const f = path33.join(SET, cfg.rep, "get_variable_defs.json");
11236
+ if (!existsSync26(f)) continue;
10725
11237
  defsRecorded = true;
10726
- const text = envelopeFirstTextPart(JSON.parse(readFileSync21(f, "utf8"))) || "{}";
11238
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync24(f, "utf8"))) || "{}";
10727
11239
  try {
10728
11240
  for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
10729
11241
  } catch {
@@ -10731,8 +11243,8 @@ function buildSegments(task, mode = "fenced") {
10731
11243
  }
10732
11244
  }
10733
11245
  const emissionTexts = task.configs.map((cfg) => {
10734
- const f = path31.join(SET, cfg.rep, "get_design_context.json");
10735
- return existsSync25(f) ? envelopeFirstTextPart(JSON.parse(readFileSync21(f, "utf8"))) : "";
11246
+ const f = path33.join(SET, cfg.rep, "get_design_context.json");
11247
+ return existsSync26(f) ? envelopeFirstTextPart(JSON.parse(readFileSync24(f, "utf8"))) : "";
10736
11248
  });
10737
11249
  const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
10738
11250
  const defs = JSON.stringify(map, null, 1);
@@ -10750,9 +11262,9 @@ TOKEN MAP NEVER RECORDED: this set holds no get_variable_defs envelope, so wheth
10750
11262
  for (const cfg of task.configs) {
10751
11263
  const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
10752
11264
  const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
10753
- const assets = readdirSync9(path31.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
11265
+ const assets = readdirSync10(path33.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
10754
11266
  \`\`\`svg
10755
- ${readFileSync21(path31.join(SET, cfg.rep, f), "utf8")}
11267
+ ${readFileSync24(path33.join(SET, cfg.rep, f), "utf8")}
10756
11268
  \`\`\``).join("\n");
10757
11269
  const refNote = (() => {
10758
11270
  const dims = refPngDims(SET, cfg.rep);
@@ -10788,7 +11300,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
10788
11300
  } else {
10789
11301
  parts.push(`
10790
11302
  ## Output format
10791
- Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path31.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.`);
11303
+ Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path33.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.`);
10792
11304
  }
10793
11305
  return parts.join("\n");
10794
11306
  }
@@ -10855,9 +11367,9 @@ var init_adapter = __esm({
10855
11367
  });
10856
11368
 
10857
11369
  // packages/generate/src/bundle-emit.ts
10858
- import { createHash as createHash5 } from "node:crypto";
10859
- import { copyFileSync, existsSync as existsSync26, mkdirSync as mkdirSync7, readFileSync as readFileSync22, readdirSync as readdirSync10, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
10860
- import path32 from "node:path";
11370
+ import { createHash as createHash6 } from "node:crypto";
11371
+ import { copyFileSync, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync25, readdirSync as readdirSync11, rmSync as rmSync4, writeFileSync as writeFileSync11 } from "node:fs";
11372
+ import path34 from "node:path";
10861
11373
  function pinFromConfigs(configs) {
10862
11374
  const domains = /* @__PURE__ */ new Map();
10863
11375
  const kinds = /* @__PURE__ */ new Map();
@@ -10926,9 +11438,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
10926
11438
  const notices = [];
10927
11439
  const licenseTexts = /* @__PURE__ */ new Map();
10928
11440
  for (const face of faces) {
10929
- const src = path32.join(cacheDir, path32.basename(face.file));
10930
- const target = `./fonts/${path32.basename(face.file)}`;
10931
- const format = FONT_FORMATS[path32.extname(face.file).toLowerCase()] ?? "truetype";
11441
+ const src = path34.join(cacheDir, path34.basename(face.file));
11442
+ const target = `./fonts/${path34.basename(face.file)}`;
11443
+ const format = FONT_FORMATS[path34.extname(face.file).toLowerCase()] ?? "truetype";
10932
11444
  const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
10933
11445
  const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
10934
11446
  const license = normalizeFontLicense(face.license);
@@ -10966,14 +11478,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
10966
11478
  `/* ${decl} */`
10967
11479
  );
10968
11480
  }
10969
- } else if (existsSync26(src) && createHash5("sha256").update(readFileSync22(src)).digest("hex") === face.sha256) {
10970
- mkdirSync7(path32.join(bundleDir, "fonts"), { recursive: true });
10971
- copyFileSync(src, path32.join(bundleDir, "fonts", path32.basename(face.file)));
11481
+ } else if (existsSync27(src) && createHash6("sha256").update(readFileSync25(src)).digest("hex") === face.sha256) {
11482
+ mkdirSync8(path34.join(bundleDir, "fonts"), { recursive: true });
11483
+ copyFileSync(src, path34.join(bundleDir, "fonts", path34.basename(face.file)));
10972
11484
  licenseTexts.set(terms.file, terms.text);
10973
11485
  const upstream = upstreamAttribution(face);
10974
11486
  notices.push(
10975
11487
  "",
10976
- `${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path32.basename(face.file)}`,
11488
+ `${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path34.basename(face.file)}`,
10977
11489
  ` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
10978
11490
  ` source: ${face.source}`,
10979
11491
  ` sha256: ${face.sha256}`,
@@ -10987,9 +11499,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
10987
11499
  }
10988
11500
  if (lines.length === 0) return null;
10989
11501
  if (notices.length > 0) {
10990
- const fontsDir = path32.join(bundleDir, "fonts");
10991
- for (const [file, text] of licenseTexts) writeFileSync10(path32.join(fontsDir, file), text);
10992
- writeFileSync10(path32.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
11502
+ const fontsDir = path34.join(bundleDir, "fonts");
11503
+ for (const [file, text] of licenseTexts) writeFileSync11(path34.join(fontsDir, file), text);
11504
+ writeFileSync11(path34.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
10993
11505
  `);
10994
11506
  header.push(
10995
11507
  "/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
@@ -11001,10 +11513,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
11001
11513
  `;
11002
11514
  }
11003
11515
  function countLatticeSymbols(setDir) {
11004
- const manifestFile = path32.join(setDir, "recording-set.json");
11005
- if (existsSync26(manifestFile)) {
11516
+ const manifestFile = path34.join(setDir, "recording-set.json");
11517
+ if (existsSync27(manifestFile)) {
11006
11518
  try {
11007
- const stored = JSON.parse(readFileSync22(manifestFile, "utf8"));
11519
+ const stored = JSON.parse(readFileSync25(manifestFile, "utf8"));
11008
11520
  if (stored.variantScope !== "component-set") return null;
11009
11521
  const lattice = stored.latticeNames;
11010
11522
  if (lattice !== void 0 && lattice.length > 0) return lattice.length;
@@ -11012,13 +11524,13 @@ function countLatticeSymbols(setDir) {
11012
11524
  }
11013
11525
  }
11014
11526
  const files = [
11015
- path32.join(setDir, "get_metadata.json"),
11016
- ...existsSync26(setDir) ? readdirSync10(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path32.join(setDir, f)) : []
11017
- ].filter((f) => existsSync26(f));
11527
+ path34.join(setDir, "get_metadata.json"),
11528
+ ...existsSync27(setDir) ? readdirSync11(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path34.join(setDir, f)) : []
11529
+ ].filter((f) => existsSync27(f));
11018
11530
  if (files.length === 0) return null;
11019
11531
  let count = 0;
11020
11532
  for (const f of files) {
11021
- const text = envelopeTextContent(JSON.parse(readFileSync22(f, "utf8")));
11533
+ const text = envelopeTextContent(JSON.parse(readFileSync25(f, "utf8")));
11022
11534
  count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
11023
11535
  }
11024
11536
  return count > 0 ? count : null;
@@ -11026,23 +11538,23 @@ function countLatticeSymbols(setDir) {
11026
11538
  function recordingSetHash(setDir, configs) {
11027
11539
  const relPaths = [];
11028
11540
  for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
11029
- if (existsSync26(path32.join(setDir, name))) relPaths.push(name);
11541
+ if (existsSync27(path34.join(setDir, name))) relPaths.push(name);
11030
11542
  }
11031
11543
  for (const cfg of configs) {
11032
11544
  for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
11033
- if (existsSync26(path32.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
11545
+ if (existsSync27(path34.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
11034
11546
  }
11035
- if (existsSync26(path32.join(setDir, cfg.rep))) {
11036
- for (const asset of readdirSync10(path32.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
11547
+ if (existsSync27(path34.join(setDir, cfg.rep))) {
11548
+ for (const asset of readdirSync11(path34.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
11037
11549
  relPaths.push(`${cfg.rep}/${asset}`);
11038
11550
  }
11039
11551
  }
11040
11552
  }
11041
11553
  return hashRecordingSet(
11042
11554
  relPaths,
11043
- (p) => new Uint8Array(readFileSync22(path32.join(setDir, p))),
11555
+ (p) => new Uint8Array(readFileSync25(path34.join(setDir, p))),
11044
11556
  (chunks) => {
11045
- const h = createHash5("sha256");
11557
+ const h = createHash6("sha256");
11046
11558
  for (const c of chunks) h.update(c);
11047
11559
  return h.digest("hex");
11048
11560
  }
@@ -11052,6 +11564,17 @@ function statusOf(s) {
11052
11564
  const tier = tierOf(s, BARS.cert);
11053
11565
  return tier === "certified" && (s.absentInk?.length ?? 0) > 0 ? "pass" : tier;
11054
11566
  }
11567
+ function kitIdentity(setDir) {
11568
+ try {
11569
+ const m = loadManifest(setDir);
11570
+ return {
11571
+ ...typeof m.figmaFile === "string" ? { figmaFile: m.figmaFile } : {},
11572
+ ...typeof m.componentSetNode === "string" ? { componentSetNode: m.componentSetNode } : {}
11573
+ };
11574
+ } catch {
11575
+ return {};
11576
+ }
11577
+ }
11055
11578
  function emitBundleV1(opts) {
11056
11579
  const substituted = (opts.substitutedFamilies ?? []).length > 0;
11057
11580
  const parityFailed = new Set(opts.behaviors.filter((b) => b.id.startsWith("parity:") && !b.pass).map((b) => b.id.slice("parity:".length)));
@@ -11070,8 +11593,8 @@ function emitBundleV1(opts) {
11070
11593
  const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
11071
11594
  const parity = opts.behaviors.filter((b) => b.id.startsWith("parity:"));
11072
11595
  const contract = opts.behaviors.filter((b) => CONTRACT_CHECK_IDS.has(b.id));
11073
- const cssFiles = ["styles.css", "tokens.css"].map((f) => path32.join(opts.bundleDir, f)).filter((f) => existsSync26(f));
11074
- const families = cssFontFamilies(cssFiles.map((f) => readFileSync22(f, "utf8")).join("\n"));
11596
+ const cssFiles = ["styles.css", "tokens.css"].map((f) => path34.join(opts.bundleDir, f)).filter((f) => existsSync27(f));
11597
+ const families = cssFontFamilies(cssFiles.map((f) => readFileSync25(f, "utf8")).join("\n"));
11075
11598
  const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
11076
11599
  family: f.family,
11077
11600
  weight: f.weight,
@@ -11100,12 +11623,21 @@ function emitBundleV1(opts) {
11100
11623
  // resolvable via verify's --set override).
11101
11624
  path: (() => {
11102
11625
  const base = process.env["INIT_CWD"] ?? process.cwd();
11103
- const rel = path32.relative(base, opts.task.set);
11626
+ const rel = path34.relative(base, opts.task.set);
11104
11627
  return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
11105
11628
  })(),
11106
11629
  component: opts.componentName,
11107
11630
  ...opts.licenseNote !== void 0 ? { licenseNote: opts.licenseNote } : {},
11108
- hash: recordingSetHash(opts.task.set, opts.task.configs)
11631
+ hash: recordingSetHash(opts.task.set, opts.task.configs),
11632
+ // PROV-1: kit identity, carried through from the recording set.
11633
+ // Captured at plan time and previously dropped here, which left
11634
+ // a published bundle unable to name the kit it derives from —
11635
+ // and the portal's consent is scoped to exactly this file key
11636
+ // (ADR-018 §3), because the right to publish attaches to a kit.
11637
+ // Read defensively: a set planned before these fields existed,
11638
+ // or one built from a handed-in selection, legitimately has
11639
+ // neither, and absent must read as NOT RECORDED.
11640
+ ...kitIdentity(opts.task.set)
11109
11641
  },
11110
11642
  environment: { ...opts.environment, ...(opts.substitutedFamilies ?? []).length > 0 ? { substitutedFamilies: opts.substitutedFamilies } : {} },
11111
11643
  coverage: { recordedConfigs: statuses.length, latticeConfigs: lattice },
@@ -11128,25 +11660,25 @@ function emitBundleV1(opts) {
11128
11660
  })
11129
11661
  };
11130
11662
  const written = [];
11131
- const manifestPath2 = path32.join(opts.bundleDir, "component.json");
11132
- writeFileSync10(manifestPath2, `${JSON.stringify(manifest, null, 2)}
11663
+ const manifestPath2 = path34.join(opts.bundleDir, "component.json");
11664
+ writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
11133
11665
  `);
11134
11666
  written.push(manifestPath2);
11135
- const stylesPath = path32.join(opts.bundleDir, "styles.css");
11136
- if (existsSync26(stylesPath)) {
11667
+ const stylesPath = path34.join(opts.bundleDir, "styles.css");
11668
+ if (existsSync27(stylesPath)) {
11137
11669
  const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
11138
- const current = readFileSync22(stylesPath, "utf8");
11670
+ const current = readFileSync25(stylesPath, "utf8");
11139
11671
  const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
11140
- writeFileSync10(stylesPath, `${comment}
11672
+ writeFileSync11(stylesPath, `${comment}
11141
11673
  ${stripped}`);
11142
11674
  written.push(stylesPath);
11143
11675
  }
11144
- const fontsCssPath = path32.join(opts.bundleDir, "fonts.css");
11145
- rmSync3(path32.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
11146
- rmSync3(fontsCssPath, { force: true });
11676
+ const fontsCssPath = path34.join(opts.bundleDir, "fonts.css");
11677
+ rmSync4(path34.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
11678
+ rmSync4(fontsCssPath, { force: true });
11147
11679
  const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
11148
11680
  if (fontsCss !== null) {
11149
- writeFileSync10(fontsCssPath, fontsCss);
11681
+ writeFileSync11(fontsCssPath, fontsCss);
11150
11682
  written.push(fontsCssPath);
11151
11683
  }
11152
11684
  const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
@@ -11574,9 +12106,9 @@ DEALINGS IN THE FONT SOFTWARE.
11574
12106
  });
11575
12107
 
11576
12108
  // packages/generate/src/compose-pins.ts
11577
- import { createHash as createHash6 } from "node:crypto";
11578
- import { existsSync as existsSync27, readFileSync as readFileSync23, readdirSync as readdirSync11, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
11579
- import path33 from "node:path";
12109
+ import { createHash as createHash7 } from "node:crypto";
12110
+ import { existsSync as existsSync28, readFileSync as readFileSync26, readdirSync as readdirSync12, realpathSync as realpathSync3, statSync as statSync4 } from "node:fs";
12111
+ import path35 from "node:path";
11580
12112
  function bundleDirs(roots, depth = 4) {
11581
12113
  const found = [];
11582
12114
  const seen = /* @__PURE__ */ new Set();
@@ -11585,31 +12117,31 @@ function bundleDirs(roots, depth = 4) {
11585
12117
  try {
11586
12118
  key = realpathSync3(dir);
11587
12119
  } catch {
11588
- key = path33.resolve(dir);
12120
+ key = path35.resolve(dir);
11589
12121
  }
11590
12122
  if (seen.has(key)) return;
11591
12123
  seen.add(key);
11592
- if (existsSync27(path33.join(dir, "component.json"))) {
12124
+ if (existsSync28(path35.join(dir, "component.json"))) {
11593
12125
  found.push(key);
11594
12126
  return;
11595
12127
  }
11596
12128
  if (remaining === 0) return;
11597
12129
  let entries;
11598
12130
  try {
11599
- entries = readdirSync11(dir);
12131
+ entries = readdirSync12(dir);
11600
12132
  } catch {
11601
12133
  return;
11602
12134
  }
11603
12135
  for (const e of entries) {
11604
12136
  if (e === "node_modules" || e.startsWith(".")) continue;
11605
- const full = path33.join(dir, e);
12137
+ const full = path35.join(dir, e);
11606
12138
  try {
11607
- if (statSync3(full).isDirectory()) walk2(full, remaining - 1);
12139
+ if (statSync4(full).isDirectory()) walk2(full, remaining - 1);
11608
12140
  } catch {
11609
12141
  }
11610
12142
  }
11611
12143
  };
11612
- for (const r of roots) walk2(path33.resolve(r), depth);
12144
+ for (const r of roots) walk2(path35.resolve(r), depth);
11613
12145
  return found;
11614
12146
  }
11615
12147
  function composedPins(hostSet, libraryRoots) {
@@ -11628,7 +12160,7 @@ function composedPins(hostSet, libraryRoots) {
11628
12160
  let pinned = false;
11629
12161
  const failures = [];
11630
12162
  for (const rel of partnerRels) {
11631
- const partnerSet = path33.resolve(hostSet, rel);
12163
+ const partnerSet = path35.resolve(hostSet, rel);
11632
12164
  let partnerTask;
11633
12165
  let partnerManifest;
11634
12166
  try {
@@ -11657,7 +12189,7 @@ function composedPins(hostSet, libraryRoots) {
11657
12189
  const wantHash = recordingSetHash(partnerSet, partnerTask.configs);
11658
12190
  const matches = candidates.filter((dir) => {
11659
12191
  try {
11660
- const parsed = readBundleManifest(readFileSync23(path33.join(dir, "component.json"), "utf8"));
12192
+ const parsed = readBundleManifest(readFileSync26(path35.join(dir, "component.json"), "utf8"));
11661
12193
  return parsed.manifest?.provenance.recordingSet.hash === wantHash;
11662
12194
  } catch {
11663
12195
  return false;
@@ -11670,13 +12202,13 @@ function composedPins(hostSet, libraryRoots) {
11670
12202
  continue;
11671
12203
  }
11672
12204
  if (matches.length > 1) {
11673
- failures.push(`${rel}: ${matches.length} bundles join the same recording hash (${matches.map((m) => path33.basename(m)).join(", ")}) \u2014 ambiguous; remove or point --library away from the duplicates`);
12205
+ failures.push(`${rel}: ${matches.length} bundles join the same recording hash (${matches.map((m) => path35.basename(m)).join(", ")}) \u2014 ambiguous; remove or point --library away from the duplicates`);
11674
12206
  continue;
11675
12207
  }
11676
12208
  const bundleDir = matches[0];
11677
12209
  let manifest;
11678
12210
  try {
11679
- manifest = readBundleManifest(readFileSync23(path33.join(bundleDir, "component.json"), "utf8")).manifest;
12211
+ manifest = readBundleManifest(readFileSync26(path35.join(bundleDir, "component.json"), "utf8")).manifest;
11680
12212
  } catch {
11681
12213
  manifest = void 0;
11682
12214
  }
@@ -11693,8 +12225,8 @@ function composedPins(hostSet, libraryRoots) {
11693
12225
  const moduleFiles = [];
11694
12226
  let fileIssue;
11695
12227
  for (const name of [manifest.entry, "styles.css", "tokens.css", "fonts.css"]) {
11696
- const file = path33.join(bundleDir, name);
11697
- if (!existsSync27(file)) {
12228
+ const file = path35.join(bundleDir, name);
12229
+ if (!existsSync28(file)) {
11698
12230
  if (name === manifest.entry || name === "styles.css") {
11699
12231
  fileIssue = `${rel}: partner bundle is missing ${name}`;
11700
12232
  break;
@@ -11703,7 +12235,7 @@ function composedPins(hostSet, libraryRoots) {
11703
12235
  }
11704
12236
  let bytes;
11705
12237
  try {
11706
- bytes = readFileSync23(file);
12238
+ bytes = readFileSync26(file);
11707
12239
  } catch {
11708
12240
  fileIssue = `${rel}: partner file ${name} at ${bundleDir} is unreadable`;
11709
12241
  break;
@@ -11712,7 +12244,7 @@ function composedPins(hostSet, libraryRoots) {
11712
12244
  fileIssue = `${rel}: partner file ${name} exceeds the pin size cap (${bytes.byteLength} bytes)`;
11713
12245
  break;
11714
12246
  }
11715
- moduleFiles.push({ name, content: bytes.toString("utf8"), sha256: createHash6("sha256").update(bytes).digest("hex") });
12247
+ moduleFiles.push({ name, content: bytes.toString("utf8"), sha256: createHash7("sha256").update(bytes).digest("hex") });
11716
12248
  }
11717
12249
  if (fileIssue !== void 0) {
11718
12250
  failures.push(fileIssue);
@@ -11775,14 +12307,14 @@ function composedChecks(candidateDir, hostEntry, pins) {
11775
12307
  const checks = [];
11776
12308
  let entrySource = "";
11777
12309
  try {
11778
- entrySource = readFileSync23(path33.join(candidateDir, hostEntry), "utf8");
12310
+ entrySource = readFileSync26(path35.join(candidateDir, hostEntry), "utf8");
11779
12311
  } catch {
11780
12312
  }
11781
- const candidateRoot = path33.resolve(candidateDir);
12313
+ const candidateRoot = path35.resolve(candidateDir);
11782
12314
  for (const pin of pins) {
11783
12315
  const dir = composedModuleDir(pin.partnerName);
11784
- const resolvedDir = path33.resolve(candidateDir, dir);
11785
- if (!resolvedDir.startsWith(candidateRoot + path33.sep)) {
12316
+ const resolvedDir = path35.resolve(candidateDir, dir);
12317
+ if (!resolvedDir.startsWith(candidateRoot + path35.sep)) {
11786
12318
  checks.push({ id: `composition:${pin.pairKey}:module-verbatim`, pass: false, detail: `pin path ${dir}/ escapes the candidate directory \u2014 refused` });
11787
12319
  checks.push({ id: `composition:${pin.pairKey}:imported`, pass: false, detail: `pin path ${dir}/ escapes the candidate directory \u2014 refused` });
11788
12320
  continue;
@@ -11793,12 +12325,12 @@ function composedChecks(candidateDir, hostEntry, pins) {
11793
12325
  wrong.push(`${f.name} refused (not a plain path segment)`);
11794
12326
  continue;
11795
12327
  }
11796
- const target = path33.join(candidateDir, dir, f.name);
11797
- if (!existsSync27(target)) {
12328
+ const target = path35.join(candidateDir, dir, f.name);
12329
+ if (!existsSync28(target)) {
11798
12330
  wrong.push(`${f.name} missing`);
11799
12331
  continue;
11800
12332
  }
11801
- const sha = createHash6("sha256").update(readFileSync23(target)).digest("hex");
12333
+ const sha = createHash7("sha256").update(readFileSync26(target)).digest("hex");
11802
12334
  if (sha !== f.sha256) wrong.push(`${f.name} differs from the pinned partner bytes`);
11803
12335
  }
11804
12336
  checks.push({
@@ -11823,10 +12355,10 @@ function rootClassesFor(emission, nodeId) {
11823
12355
  }
11824
12356
  function regionOverrides(hostSet, partnerSet, instances) {
11825
12357
  const read = (setDir, rep) => {
11826
- const f = path33.join(setDir, rep, "get_design_context.json");
11827
- if (!existsSync27(f)) return void 0;
12358
+ const f = path35.join(setDir, rep, "get_design_context.json");
12359
+ if (!existsSync28(f)) return void 0;
11828
12360
  try {
11829
- return envelopeFirstTextPart(JSON.parse(readFileSync23(f, "utf8")));
12361
+ return envelopeFirstTextPart(JSON.parse(readFileSync26(f, "utf8")));
11830
12362
  } catch {
11831
12363
  return void 0;
11832
12364
  }
@@ -11856,7 +12388,7 @@ var init_compose_pins = __esm({
11856
12388
  init_src4();
11857
12389
  init_brief();
11858
12390
  init_bundle_emit();
11859
- composedModuleDir = (partnerName) => path33.posix.join("composed", partnerName);
12391
+ composedModuleDir = (partnerName) => path35.posix.join("composed", partnerName);
11860
12392
  safeSegment = (s) => /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(s) && !s.includes("..");
11861
12393
  MAX_PINNED_FILE_BYTES = 1024 * 1024;
11862
12394
  CONTEXT_LAYOUT_TOKENS = /* @__PURE__ */ new Set(["shrink-0", "grow", "flex-1", "basis-0", "self-stretch", "w-full", "h-full"]);
@@ -11864,8 +12396,8 @@ var init_compose_pins = __esm({
11864
12396
  });
11865
12397
 
11866
12398
  // packages/generate/src/motion.ts
11867
- import { existsSync as existsSync28, readFileSync as readFileSync24, readdirSync as readdirSync12, statSync as statSync4 } from "node:fs";
11868
- import path34 from "node:path";
12399
+ import { existsSync as existsSync29, readFileSync as readFileSync27, readdirSync as readdirSync13, statSync as statSync5 } from "node:fs";
12400
+ import path36 from "node:path";
11869
12401
  function springProgress(u, bounce) {
11870
12402
  const decay = Math.log(100);
11871
12403
  if (bounce <= 0) {
@@ -11936,10 +12468,10 @@ function reportsNoMotion(text) {
11936
12468
  });
11937
12469
  }
11938
12470
  function motionTruthFor(setDir) {
11939
- const file = path34.join(setDir, "get_motion_context.json");
11940
- if (existsSync28(file) && usableEnvelope(file, "get_motion_context").ok) {
12471
+ const file = path36.join(setDir, "get_motion_context.json");
12472
+ if (existsSync29(file) && usableEnvelope(file, "get_motion_context").ok) {
11941
12473
  try {
11942
- const text = envelopeTextContent(JSON.parse(readFileSync24(file, "utf8")));
12474
+ const text = envelopeTextContent(JSON.parse(readFileSync27(file, "utf8")));
11943
12475
  if (text.trim() === "") return { state: "recorded-empty" };
11944
12476
  return reportsNoMotion(text) ? { state: "recorded-no-motion", text } : { state: "recorded", text };
11945
12477
  } catch {
@@ -11952,21 +12484,21 @@ function motionTruthFor(setDir) {
11952
12484
  }
11953
12485
  }
11954
12486
  function motionDisclosure(bundleDir, setDir) {
11955
- const sheets = ["styles.css", "tokens.css"].map((f) => path34.join(bundleDir, f));
11956
- const composedRoot = path34.join(bundleDir, "composed");
12487
+ const sheets = ["styles.css", "tokens.css"].map((f) => path36.join(bundleDir, f));
12488
+ const composedRoot = path36.join(bundleDir, "composed");
11957
12489
  try {
11958
- for (const entry of readdirSync12(composedRoot).sort()) {
11959
- const dir = path34.join(composedRoot, entry);
12490
+ for (const entry of readdirSync13(composedRoot).sort()) {
12491
+ const dir = path36.join(composedRoot, entry);
11960
12492
  try {
11961
- if (!statSync4(dir).isDirectory()) continue;
12493
+ if (!statSync5(dir).isDirectory()) continue;
11962
12494
  } catch {
11963
12495
  continue;
11964
12496
  }
11965
- sheets.push(path34.join(dir, "styles.css"), path34.join(dir, "tokens.css"));
12497
+ sheets.push(path36.join(dir, "styles.css"), path36.join(dir, "tokens.css"));
11966
12498
  }
11967
12499
  } catch {
11968
12500
  }
11969
- const css = sheets.filter((f) => existsSync28(f)).map((f) => readFileSync24(f, "utf8")).join("\n");
12501
+ const css = sheets.filter((f) => existsSync29(f)).map((f) => readFileSync27(f, "utf8")).join("\n");
11970
12502
  if (!MOTION_CSS_PATTERN.test(scannableCss(css))) return { present: false };
11971
12503
  return {
11972
12504
  present: true,
@@ -12333,12 +12865,12 @@ var init_components = __esm({
12333
12865
 
12334
12866
  // packages/generate/src/codebase/walk.ts
12335
12867
  import fs2 from "node:fs";
12336
- import path35 from "node:path";
12868
+ import path37 from "node:path";
12337
12869
  function resolvedPathIsExcluded(real, roots) {
12338
- if (isNeverRead(path35.basename(real))) return true;
12870
+ if (isNeverRead(path37.basename(real))) return true;
12339
12871
  for (const root of roots) {
12340
- if (real !== root && !real.startsWith(root + path35.sep)) continue;
12341
- for (const segment of path35.relative(root, real).split(path35.sep).slice(0, -1)) {
12872
+ if (real !== root && !real.startsWith(root + path37.sep)) continue;
12873
+ for (const segment of path37.relative(root, real).split(path37.sep).slice(0, -1)) {
12342
12874
  if (segment.startsWith(".") || EXCLUDED_DIRS.has(segment)) return true;
12343
12875
  }
12344
12876
  }
@@ -12352,7 +12884,7 @@ function containedRealpath(abs, roots) {
12352
12884
  return null;
12353
12885
  }
12354
12886
  for (const root of roots) {
12355
- if (real === root || real.startsWith(root + path35.sep)) return real;
12887
+ if (real === root || real.startsWith(root + path37.sep)) return real;
12356
12888
  }
12357
12889
  return null;
12358
12890
  }
@@ -12385,7 +12917,7 @@ function walkRepo(roots, limits, accept) {
12385
12917
  continue;
12386
12918
  }
12387
12919
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
12388
- const abs = path35.join(frame.dir, entry.name);
12920
+ const abs = path37.join(frame.dir, entry.name);
12389
12921
  if (EXCLUDED_DIRS.has(entry.name)) continue;
12390
12922
  if (isNeverRead(entry.name)) continue;
12391
12923
  const real = containedRealpath(abs, realRoots);
@@ -12473,18 +13005,18 @@ var init_walk = __esm({
12473
13005
  /^\.netrc$/i
12474
13006
  ];
12475
13007
  isNeverRead = (basename) => NEVER_READ.some((re) => re.test(basename));
12476
- toRel = (root, abs) => path35.relative(root, abs).split(path35.sep).join(path35.posix.sep);
13008
+ toRel = (root, abs) => path37.relative(root, abs).split(path37.sep).join(path37.posix.sep);
12477
13009
  }
12478
13010
  });
12479
13011
 
12480
13012
  // packages/generate/src/codebase/scan.ts
12481
13013
  import crypto2 from "node:crypto";
12482
13014
  import fs3 from "node:fs";
12483
- import path36 from "node:path";
13015
+ import path38 from "node:path";
12484
13016
  import postcss3 from "postcss";
12485
13017
  function scanCodebase(options) {
12486
13018
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
12487
- const roots = options.roots.map((r) => path36.resolve(r));
13019
+ const roots = options.roots.map((r) => path38.resolve(r));
12488
13020
  const walk2 = walkRepo(
12489
13021
  roots,
12490
13022
  {
@@ -12504,7 +13036,7 @@ function scanCodebase(options) {
12504
13036
  let bytesRead = 0;
12505
13037
  let filesRead = 0;
12506
13038
  for (const file of walk2.files) {
12507
- const base = path36.posix.basename(file.rel);
13039
+ const base = path38.posix.basename(file.rel);
12508
13040
  configFiles.add(file.rel);
12509
13041
  if (/^tailwind\.config\./.test(base) || file.rel === "babel.config.js") continue;
12510
13042
  const text = readTextFile(file.abs);
@@ -12520,7 +13052,7 @@ function scanCodebase(options) {
12520
13052
  }
12521
13053
  const css = extractCssCustomProperties(cssFiles.filter((f) => !f.rel.includes("..")));
12522
13054
  const components = scanComponents(componentFiles);
12523
- const packages = manifests.filter((m) => path36.posix.basename(m.rel) === "package.json");
13055
+ const packages = manifests.filter((m) => path38.posix.basename(m.rel) === "package.json");
12524
13056
  const styling = detectStyling(configFiles, cssFiles, componentFiles, manifests);
12525
13057
  const classNameStyle = representativeClassNames(cssFiles, css.unparsed.length);
12526
13058
  const disclosures = buildDisclosures(
@@ -12563,13 +13095,13 @@ function scanCodebase(options) {
12563
13095
  },
12564
13096
  components: {
12565
13097
  entries: components.entries.slice(0, PROFILE_LIMITS.maxComponents),
12566
- fileNaming: buildHistogram(componentFiles.map((f) => classifyFileName(path36.posix.basename(f.rel)))),
13098
+ fileNaming: buildHistogram(componentFiles.map((f) => classifyFileName(path38.posix.basename(f.rel)))),
12567
13099
  directoryLayout: buildHistogram(componentFiles.map((f) => classifyDirectoryLayout(f.rel))),
12568
13100
  exportStyle: buildHistogram(components.entries.map((e) => e.exportStyle)),
12569
13101
  classNameStyle,
12570
13102
  colocation: buildHistogram(collectColocation(componentFiles, cssFiles)),
12571
13103
  barrelFiles: componentFiles.filter(
12572
- (f) => /^index\.[tj]sx?$/.test(path36.posix.basename(f.rel)) && isReExportOnly(f.text)
13104
+ (f) => /^index\.[tj]sx?$/.test(path38.posix.basename(f.rel)) && isReExportOnly(f.text)
12573
13105
  ).length,
12574
13106
  refForwarding: {
12575
13107
  forwardRef: componentFiles.filter((f) => /\bforwardRef\s*[(<]/.test(f.text)).length,
@@ -12592,7 +13124,7 @@ function detectStyling(configFiles, cssFiles, componentFiles, manifests) {
12592
13124
  };
12593
13125
  const deps = /* @__PURE__ */ new Map();
12594
13126
  for (const manifest of manifests) {
12595
- if (path36.posix.basename(manifest.rel) !== "package.json") continue;
13127
+ if (path38.posix.basename(manifest.rel) !== "package.json") continue;
12596
13128
  try {
12597
13129
  const parsed = JSON.parse(manifest.text);
12598
13130
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
@@ -12604,7 +13136,7 @@ function detectStyling(configFiles, cssFiles, componentFiles, manifests) {
12604
13136
  }
12605
13137
  }
12606
13138
  for (const cfg of configFiles) {
12607
- const base = path36.posix.basename(cfg);
13139
+ const base = path38.posix.basename(cfg);
12608
13140
  if (/^tailwind\.config\./.test(base)) add("tailwind-v3", "file", cfg);
12609
13141
  if (base === "components.json") add("shadcn-style", "file", cfg);
12610
13142
  }
@@ -12662,8 +13194,8 @@ function collectClassNames(cssFiles) {
12662
13194
  return [...distinct].sort().map(classifyClassName);
12663
13195
  }
12664
13196
  function classifyDirectoryLayout(rel) {
12665
- const base = path36.posix.basename(rel).replace(/\.[^.]+$/, "");
12666
- const dir = path36.posix.basename(path36.posix.dirname(rel));
13197
+ const base = path38.posix.basename(rel).replace(/\.[^.]+$/, "");
13198
+ const dir = path38.posix.basename(path38.posix.dirname(rel));
12667
13199
  if (base === "index") return "component-dir";
12668
13200
  if (base === dir) return "component-dir";
12669
13201
  return "flat-file";
@@ -12687,7 +13219,7 @@ function detectFormatting(manifests, componentFiles) {
12687
13219
  (a, b) => a.rel.split("/").length - b.rel.split("/").length || a.rel.localeCompare(b.rel)
12688
13220
  );
12689
13221
  for (const manifest of byDepth) {
12690
- const base = path36.posix.basename(manifest.rel);
13222
+ const base = path38.posix.basename(manifest.rel);
12691
13223
  if (!/^\.prettierrc/.test(base) && base !== "package.json") continue;
12692
13224
  try {
12693
13225
  const parsed = JSON.parse(manifest.text);
@@ -12705,7 +13237,7 @@ function detectFormatting(manifests, componentFiles) {
12705
13237
  }
12706
13238
  }
12707
13239
  for (const manifest of byDepth) {
12708
- if (path36.posix.basename(manifest.rel) !== ".editorconfig") continue;
13240
+ if (path38.posix.basename(manifest.rel) !== ".editorconfig") continue;
12709
13241
  const style = /indent_style\s*=\s*(tab|space)/.exec(manifest.text)?.[1];
12710
13242
  const width = /indent_size\s*=\s*(\d+)/.exec(manifest.text)?.[1];
12711
13243
  if (style || width) {
@@ -12776,12 +13308,12 @@ function buildDisclosures(detected, css, cappedOut, unrepresentativeClassNames)
12776
13308
  return out;
12777
13309
  }
12778
13310
  function outPathIsGitIgnored(outPath) {
12779
- const dir = path36.dirname(outPath);
13311
+ const dir = path38.dirname(outPath);
12780
13312
  try {
12781
- const ignoreFile = path36.join(path36.dirname(dir), ".gitignore");
13313
+ const ignoreFile = path38.join(path38.dirname(dir), ".gitignore");
12782
13314
  if (!fs3.existsSync(ignoreFile)) return false;
12783
13315
  const patterns = fs3.readFileSync(ignoreFile, "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
12784
- const base = path36.basename(dir);
13316
+ const base = path38.basename(dir);
12785
13317
  return patterns.some((p) => p === base || p === `${base}/` || p === `/${base}` || p === `/${base}/`);
12786
13318
  } catch {
12787
13319
  return false;
@@ -12928,8 +13460,8 @@ __export(profile_exports, {
12928
13460
  PROFILE_DESCRIPTION: () => PROFILE_DESCRIPTION,
12929
13461
  runProfile: () => runProfile
12930
13462
  });
12931
- import { closeSync, constants, existsSync as existsSync29, mkdirSync as mkdirSync8, openSync, realpathSync as realpathSync4, writeFileSync as writeFileSync11 } from "node:fs";
12932
- import path37 from "node:path";
13463
+ import { closeSync, constants, existsSync as existsSync30, mkdirSync as mkdirSync9, openSync, realpathSync as realpathSync4, writeFileSync as writeFileSync12 } from "node:fs";
13464
+ import path39 from "node:path";
12933
13465
  function escapesScanRoot(outPath, scanRoot) {
12934
13466
  const resolveExisting = (target) => {
12935
13467
  let cursor = target;
@@ -12937,23 +13469,23 @@ function escapesScanRoot(outPath, scanRoot) {
12937
13469
  try {
12938
13470
  return realpathSync4(cursor);
12939
13471
  } catch {
12940
- const parent = path37.dirname(cursor);
13472
+ const parent = path39.dirname(cursor);
12941
13473
  if (parent === cursor) return cursor;
12942
13474
  cursor = parent;
12943
13475
  }
12944
13476
  }
12945
13477
  };
12946
13478
  const root = resolveExisting(scanRoot);
12947
- const dir = resolveExisting(path37.dirname(outPath));
12948
- return dir !== root && !dir.startsWith(root + path37.sep);
13479
+ const dir = resolveExisting(path39.dirname(outPath));
13480
+ return dir !== root && !dir.startsWith(root + path39.sep);
12949
13481
  }
12950
13482
  function runProfile(options) {
12951
13483
  if (options.describe) {
12952
13484
  printDescription(PROFILE_DESCRIPTION);
12953
13485
  return;
12954
13486
  }
12955
- const dir = path37.resolve(options.dir ?? ".");
12956
- if (!existsSync29(dir)) {
13487
+ const dir = path39.resolve(options.dir ?? ".");
13488
+ if (!existsSync30(dir)) {
12957
13489
  fail(options, ExitCode.InputValidation, {
12958
13490
  error: `no such directory: ${dir}`,
12959
13491
  code: "profile_dir_missing",
@@ -12961,7 +13493,7 @@ function runProfile(options) {
12961
13493
  });
12962
13494
  }
12963
13495
  const profile = scanCodebase({ roots: [dir], ...options.now ? { now: options.now } : {} });
12964
- const outPath = path37.resolve(options.out ?? path37.join(dir, "tendril-out", "codebase-profile.json"));
13496
+ const outPath = path39.resolve(options.out ?? path39.join(dir, "tendril-out", "codebase-profile.json"));
12965
13497
  if (!options.dryRun) {
12966
13498
  if (options.out === void 0 && escapesScanRoot(outPath, dir)) {
12967
13499
  fail(options, ExitCode.InputValidation, {
@@ -12970,10 +13502,10 @@ function runProfile(options) {
12970
13502
  remediation: `\`tendril-out\` in that project is a symlink pointing outside it, so writing the profile there could overwrite an unrelated file. Remove the symlink, or choose an explicit destination: \`${tendrilCommand(`profile --dir ${quoteArg(dir)} --out ./codebase-profile.json`)}\`.`
12971
13503
  });
12972
13504
  }
12973
- mkdirSync8(path37.dirname(outPath), { recursive: true });
13505
+ mkdirSync9(path39.dirname(outPath), { recursive: true });
12974
13506
  const handle = openSync(outPath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 420);
12975
13507
  try {
12976
- writeFileSync11(handle, `${JSON.stringify(profile, null, 2)}
13508
+ writeFileSync12(handle, `${JSON.stringify(profile, null, 2)}
12977
13509
  `, "utf8");
12978
13510
  } finally {
12979
13511
  closeSync(handle);
@@ -13035,7 +13567,7 @@ Written to ${outPath}
13035
13567
  `);
13036
13568
  if (!ignored) {
13037
13569
  process.stdout.write(
13038
- ` NOTE: ${path37.basename(path37.dirname(outPath))}/ is not gitignored here \u2014 add it to .gitignore, or this profile will show up in your next commit.
13570
+ ` NOTE: ${path39.basename(path39.dirname(outPath))}/ is not gitignored here \u2014 add it to .gitignore, or this profile will show up in your next commit.
13039
13571
  `
13040
13572
  );
13041
13573
  }
@@ -13174,11 +13706,11 @@ __export(compose_exports, {
13174
13706
  compositionPairsFor: () => compositionPairsFor,
13175
13707
  runCompose: () => runCompose
13176
13708
  });
13177
- import { createHash as createHash7 } from "node:crypto";
13178
- import { existsSync as existsSync30, readFileSync as readFileSync25 } from "node:fs";
13179
- import path38 from "node:path";
13709
+ import { createHash as createHash8 } from "node:crypto";
13710
+ import { existsSync as existsSync31, readFileSync as readFileSync28 } from "node:fs";
13711
+ import path40 from "node:path";
13180
13712
  function compositionPairsFor(hostSet, roots) {
13181
- const scanRoots = [.../* @__PURE__ */ new Set([...roots, path38.dirname(hostSet)])];
13713
+ const scanRoots = [.../* @__PURE__ */ new Set([...roots, path40.dirname(hostSet)])];
13182
13714
  const edges = composeReport(buildComposeIndex(scanRoots));
13183
13715
  const pairs = substitutionPairs(edges, hostSet);
13184
13716
  const { raw } = readManifestFile(hostSet);
@@ -13210,7 +13742,7 @@ function substitutionPairs(edges, hostSet) {
13210
13742
  });
13211
13743
  }
13212
13744
  const pair = pairs.get(key);
13213
- const poseDisplay = e.pose.reps.map((r) => `${path38.basename(r.dir)}:${r.slug}`).join(", ");
13745
+ const poseDisplay = e.pose.reps.map((r) => `${path40.basename(r.dir)}:${r.slug}`).join(", ");
13214
13746
  pair.instances.push({ hostRep: e.hostRep, instanceId: e.instanceId, poseVariantNodeId: e.pose.variantNodeId, ...poseDisplay !== "" ? { poseDisplay } : {} });
13215
13747
  for (const d of e.disclosures) if (!pair.disclosures.includes(d)) pair.disclosures.push(d);
13216
13748
  }
@@ -13222,7 +13754,7 @@ function runCompose(flags) {
13222
13754
  return;
13223
13755
  }
13224
13756
  const base = process.env["INIT_CWD"] ?? process.cwd();
13225
- const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) => path38.resolve(base, d)) : [base];
13757
+ const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) => path40.resolve(base, d)) : [base];
13226
13758
  if (flags.set === void 0 && (flags.confirmCompositions === true || flags.decline !== void 0 && flags.decline.length > 0)) {
13227
13759
  fail(flags, ExitCode.InputValidation, {
13228
13760
  error: "--confirm-compositions/--decline require --set <host> \u2014 a decision needs the host set it decides for",
@@ -13231,7 +13763,7 @@ function runCompose(flags) {
13231
13763
  });
13232
13764
  }
13233
13765
  if (flags.set !== void 0) {
13234
- runComposeConfirm(flags, path38.resolve(base, flags.set), roots);
13766
+ runComposeConfirm(flags, path40.resolve(base, flags.set), roots);
13235
13767
  return;
13236
13768
  }
13237
13769
  const index = buildComposeIndex(roots);
@@ -13249,7 +13781,7 @@ function runCompose(flags) {
13249
13781
  }
13250
13782
  let lastHost = "";
13251
13783
  for (const e of edges) {
13252
- const host = `${path38.basename(e.hostSet)}`;
13784
+ const host = `${path40.basename(e.hostSet)}`;
13253
13785
  if (host !== lastHost) {
13254
13786
  process.stdout.write(`
13255
13787
  ${host}
@@ -13257,7 +13789,7 @@ ${host}
13257
13789
  lastHost = host;
13258
13790
  }
13259
13791
  const partners = e.partners.map((p) => p.displayName).join(" / ") || "\u2014";
13260
- const pose = e.pose !== void 0 ? ` pose=${e.pose.variantNodeId} (${e.pose.reps.map((r) => `${path38.basename(r.dir)}:${r.slug}`).join(", ")})` : "";
13792
+ const pose = e.pose !== void 0 ? ` pose=${e.pose.variantNodeId} (${e.pose.reps.map((r) => `${path40.basename(r.dir)}:${r.slug}`).join(", ")})` : "";
13261
13793
  process.stdout.write(` ${e.kind.toUpperCase().padEnd(12)} ${e.hostRep}/${e.instanceId} "${e.instanceName}" \u2192 ${partners}${pose}
13262
13794
  `);
13263
13795
  for (const d of e.disclosures) process.stdout.write(` \xB7 ${d}
@@ -13269,14 +13801,14 @@ ${NOTE}
13269
13801
  });
13270
13802
  }
13271
13803
  function runComposeConfirm(flags, hostSet, roots) {
13272
- if (!existsSync30(path38.join(hostSet, "recording-set.json"))) {
13804
+ if (!existsSync31(path40.join(hostSet, "recording-set.json"))) {
13273
13805
  fail(flags, ExitCode.InputValidation, {
13274
13806
  error: `no recording-set.json in ${hostSet}`,
13275
13807
  code: "no-recording-set",
13276
13808
  remediation: "Point --set at a recorded host set (the directory holding recording-set.json)."
13277
13809
  });
13278
13810
  }
13279
- const scanRoots = [.../* @__PURE__ */ new Set([...roots, path38.dirname(hostSet)])];
13811
+ const scanRoots = [.../* @__PURE__ */ new Set([...roots, path40.dirname(hostSet)])];
13280
13812
  const index = buildComposeIndex(scanRoots);
13281
13813
  const edges = composeReport(index);
13282
13814
  const pairs = substitutionPairs(edges, hostSet);
@@ -13359,7 +13891,7 @@ PAIR ${p.displayName}${p.figmaFile !== void 0 ? ` (file ${p.figmaFile})` : " \u2
13359
13891
  // full recording-set hash join lands with pin authoring, where
13360
13892
  // task configs exist.)
13361
13893
  manifestSha256: Object.fromEntries(
13362
- p.partnerDirs.map((d) => [path38.relative(hostSet, d), createHash7("sha256").update(readFileSync25(path38.join(d, "recording-set.json"))).digest("hex")])
13894
+ p.partnerDirs.map((d) => [path40.relative(hostSet, d), createHash8("sha256").update(readFileSync28(path40.join(d, "recording-set.json"))).digest("hex")])
13363
13895
  )
13364
13896
  },
13365
13897
  // Schema shape ONLY (review: the display-side poseDisplay field
@@ -13438,10 +13970,10 @@ __export(record_exports, {
13438
13970
  runRecordPlan: () => runRecordPlan,
13439
13971
  runRecordStatus: () => runRecordStatus
13440
13972
  });
13441
- import { existsSync as existsSync31, mkdtempSync as mkdtempSync2, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
13442
- import os6 from "node:os";
13443
- import path39 from "node:path";
13444
- import { writeFileSync as writeFileSync12 } from "node:fs";
13973
+ import { existsSync as existsSync32, mkdtempSync as mkdtempSync2, readFileSync as readFileSync29, readdirSync as readdirSync14 } from "node:fs";
13974
+ import os7 from "node:os";
13975
+ import path41 from "node:path";
13976
+ import { writeFileSync as writeFileSync13 } from "node:fs";
13445
13977
  function recordsInteractionState(reports) {
13446
13978
  const evident = (s) => stateTokens(s).some((t) => INTERACTION_TREATMENT_VALUES.has(t));
13447
13979
  return reports.some((r) => evident(r.axis) || r.domain.some(evident));
@@ -13463,7 +13995,7 @@ function interactionDisclosure(component, reports) {
13463
13995
  };
13464
13996
  }
13465
13997
  function symbolsFromMetadataEnvelope(file, sourceFrame) {
13466
- const env = JSON.parse(readFileSync26(file, "utf8"));
13998
+ const env = JSON.parse(readFileSync29(file, "utf8"));
13467
13999
  const text = env.content.map((c) => c.text ?? "").join("\n");
13468
14000
  const symbols = [];
13469
14001
  const walk2 = (node, ancestor) => {
@@ -13521,8 +14053,8 @@ function runRecordPlan(opts) {
13521
14053
  if (rawFile !== void 0) {
13522
14054
  try {
13523
14055
  const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
13524
- const tmp = path39.join(mkdtempSync2(path39.join(os6.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
13525
- writeFileSync12(tmp, JSON.stringify(envelope));
14056
+ const tmp = path41.join(mkdtempSync2(path41.join(os7.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
14057
+ writeFileSync13(tmp, JSON.stringify(envelope));
13526
14058
  metadataEntries.push({ file: tmp });
13527
14059
  } catch (err) {
13528
14060
  fail(opts, ExitCode.InputValidation, {
@@ -13543,7 +14075,7 @@ function runRecordPlan(opts) {
13543
14075
  let metadataTruncated = false;
13544
14076
  for (const { file, frame } of metadataEntries) {
13545
14077
  try {
13546
- const parsed = symbolsFromMetadataEnvelope(path39.resolve(file), frame);
14078
+ const parsed = symbolsFromMetadataEnvelope(path41.resolve(file), frame);
13547
14079
  symbols.push(...parsed.symbols);
13548
14080
  if (parsed.truncated) metadataTruncated = true;
13549
14081
  } catch (err) {
@@ -13577,7 +14109,7 @@ function runRecordPlan(opts) {
13577
14109
  if (symbols.length === 0) {
13578
14110
  const leads = metadataEntries.flatMap(({ file }) => {
13579
14111
  try {
13580
- const env = JSON.parse(readFileSync26(path39.resolve(file), "utf8"));
14112
+ const env = JSON.parse(readFileSync29(path41.resolve(file), "utf8"));
13581
14113
  return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
13582
14114
  } catch {
13583
14115
  return [];
@@ -13670,7 +14202,7 @@ function runRecordPlan(opts) {
13670
14202
  // a CLI flag and the MCP surface has no parameter for it.
13671
14203
  decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
13672
14204
  text: "Record a reduced pose set (anchor + one-factor sweeps + conflict crosses) instead of the full variant matrix. This buys calls with coverage \u2014 sampling is blind to multi-axis interactions and the report discloses the poses it skipped \u2014 so only the user may accept that trade. There is no tool parameter for it; the user runs it themselves in their own terminal, before any pose is recorded.",
13673
- userRuns: [`rm ${quoteArg(path39.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
14205
+ userRuns: [`rm ${quoteArg(path41.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
13674
14206
  },
13675
14207
  {
13676
14208
  id: "larger-allowance",
@@ -13868,7 +14400,7 @@ function runRecordNext(opts) {
13868
14400
  const progress = payload["progress"];
13869
14401
  process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
13870
14402
  \u2192 ${payload["note"]}
13871
- \u2192 then: ${tendrilCommand(`record ingest --set ${path39.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
14403
+ \u2192 then: ${tendrilCommand(`record ingest --set ${path41.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
13872
14404
  `);
13873
14405
  });
13874
14406
  }
@@ -13942,7 +14474,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
13942
14474
  const skipped = [];
13943
14475
  const failed = [];
13944
14476
  for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
13945
- if (existsSync31(path39.join(setDir, rep, name))) {
14477
+ if (existsSync32(path41.join(setDir, rep, name))) {
13946
14478
  skipped.push(name);
13947
14479
  continue;
13948
14480
  }
@@ -13964,16 +14496,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
13964
14496
  }
13965
14497
  function rawEnvelopeFromFile(file, parts) {
13966
14498
  if (parts) {
13967
- const blocks = JSON.parse(readFileSync26(path39.resolve(file), "utf8"));
14499
+ const blocks = JSON.parse(readFileSync29(path41.resolve(file), "utf8"));
13968
14500
  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");
13969
14501
  return { content: blocks.map((text) => ({ type: "text", text })) };
13970
14502
  }
13971
- return { content: [{ type: "text", text: readFileSync26(path39.resolve(file), "utf8") }] };
14503
+ return { content: [{ type: "text", text: readFileSync29(path41.resolve(file), "utf8") }] };
13972
14504
  }
13973
14505
  async function runRecordIngest(opts) {
13974
14506
  let payload;
13975
14507
  try {
13976
- payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync26(path39.resolve(opts.file), "utf8"));
14508
+ payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync29(path41.resolve(opts.file), "utf8"));
13977
14509
  } catch (err) {
13978
14510
  fail(opts, ExitCode.InputValidation, {
13979
14511
  error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
@@ -13985,7 +14517,7 @@ async function runRecordIngest(opts) {
13985
14517
  fail(opts, ExitCode.InputValidation, {
13986
14518
  error: "--raw is for text tool responses; screenshots are binary",
13987
14519
  code: "envelope-invalid",
13988
- remediation: `Use \`${tendrilCommand(`record fetch --set ${path39.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
14520
+ remediation: `Use \`${tendrilCommand(`record fetch --set ${path41.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
13989
14521
  });
13990
14522
  }
13991
14523
  if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
@@ -14005,7 +14537,7 @@ async function runRecordIngest(opts) {
14005
14537
  remediation: REINGEST_GUIDANCE
14006
14538
  });
14007
14539
  }
14008
- writeFileSync12(path39.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
14540
+ writeFileSync13(path41.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
14009
14541
  `);
14010
14542
  emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
14011
14543
  process.stdout.write("set-level get_variable_defs ingested\n");
@@ -14029,7 +14561,7 @@ async function runRecordIngest(opts) {
14029
14561
  remediation: REINGEST_GUIDANCE
14030
14562
  });
14031
14563
  }
14032
- writeFileSync12(path39.join(opts.setDir, "get_motion_context.json"), `${JSON.stringify(payload, null, 1)}
14564
+ writeFileSync13(path41.join(opts.setDir, "get_motion_context.json"), `${JSON.stringify(payload, null, 1)}
14033
14565
  `);
14034
14566
  emitData(opts, { ingested: "get_motion_context", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
14035
14567
  process.stdout.write("set-level get_motion_context ingested\n");
@@ -14045,7 +14577,7 @@ async function runRecordIngest(opts) {
14045
14577
  if (assets !== void 0) {
14046
14578
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
14047
14579
  `);
14048
- for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path39.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
14580
+ for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path41.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
14049
14581
  `);
14050
14582
  }
14051
14583
  });
@@ -14118,15 +14650,15 @@ async function runRecordIngestRep(opts) {
14118
14650
  if (assets !== void 0) {
14119
14651
  for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
14120
14652
  `);
14121
- for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path39.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
14653
+ for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path41.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
14122
14654
  `);
14123
14655
  }
14124
14656
  });
14125
14657
  }
14126
14658
  function runRecordAsset(opts) {
14127
14659
  if (opts.dir !== void 0) {
14128
- const dir = path39.resolve(opts.dir);
14129
- const names = readdirSync13(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
14660
+ const dir = path41.resolve(opts.dir);
14661
+ const names = readdirSync14(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
14130
14662
  if (names.length === 0) {
14131
14663
  fail(opts, ExitCode.InputValidation, {
14132
14664
  error: `no asset-*.<ext> files found in ${dir}`,
@@ -14137,7 +14669,7 @@ function runRecordAsset(opts) {
14137
14669
  const ingested = [];
14138
14670
  try {
14139
14671
  for (const name of names) {
14140
- ingestAsset(opts.setDir, opts.rep, name, readFileSync26(path39.join(dir, name)));
14672
+ ingestAsset(opts.setDir, opts.rep, name, readFileSync29(path41.join(dir, name)));
14141
14673
  ingested.push(name);
14142
14674
  }
14143
14675
  } catch (err) {
@@ -14157,11 +14689,11 @@ function runRecordAsset(opts) {
14157
14689
  fail(opts, ExitCode.InputValidation, {
14158
14690
  error: "pass --name and --file for a single asset, or --dir for a batch",
14159
14691
  code: "asset-rejected",
14160
- remediation: tendrilCommand(`record asset --set ${path39.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
14692
+ remediation: tendrilCommand(`record asset --set ${path41.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
14161
14693
  });
14162
14694
  }
14163
14695
  try {
14164
- ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync26(path39.resolve(opts.file)));
14696
+ ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync29(path41.resolve(opts.file)));
14165
14697
  emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
14166
14698
  process.stdout.write(`ingested ${opts.rep}/${opts.name}
14167
14699
  `);
@@ -14178,8 +14710,8 @@ function runRecordStatus(opts) {
14178
14710
  const status = sessionStatus(opts.setDir);
14179
14711
  const composition = (() => {
14180
14712
  try {
14181
- const setDir = path39.resolve(opts.setDir);
14182
- const { open, standing, invalid } = compositionPairsFor(setDir, [path39.dirname(setDir)]);
14713
+ const setDir = path41.resolve(opts.setDir);
14714
+ const { open, standing, invalid } = compositionPairsFor(setDir, [path41.dirname(setDir)]);
14183
14715
  return {
14184
14716
  openPairs: open.map((p) => ({ displayName: p.displayName, pairKey: p.key, instances: p.instances.length })),
14185
14717
  confirmed: standing.filter((s) => s.status === "confirmed").length,
@@ -14200,7 +14732,7 @@ function runRecordStatus(opts) {
14200
14732
  }
14201
14733
  }
14202
14734
  process.stdout.write(
14203
- status.motion.recorded ? motionTruthFor(path39.resolve(opts.setDir)).state === "recorded-no-motion" ? "MOTION set-level motion context recorded \u2014 the response reports NO motion data (no keyframe tracks, no snippets); briefs prescribe default doctrine and say so. This is the instrument's answer, not proof the design has no transitions\n" : status.motion.asked ? "MOTION set-level motion context recorded\n" : "MOTION set-level motion context recorded (ingested onto a set that predates the obligation \u2014 briefs will quote it as recorded truth)\n" : status.motion.asked ? status.motion.invalid !== void 0 ? `MOTION set-level motion file is UNUSABLE (${status.motion.invalid}) \u2014 re-record it via \`record next\`
14735
+ status.motion.recorded ? motionTruthFor(path41.resolve(opts.setDir)).state === "recorded-no-motion" ? "MOTION set-level motion context recorded \u2014 the response reports NO motion data (no keyframe tracks, no snippets); briefs prescribe default doctrine and say so. This is the instrument's answer, not proof the design has no transitions\n" : status.motion.asked ? "MOTION set-level motion context recorded\n" : "MOTION set-level motion context recorded (ingested onto a set that predates the obligation \u2014 briefs will quote it as recorded truth)\n" : status.motion.asked ? status.motion.invalid !== void 0 ? `MOTION set-level motion file is UNUSABLE (${status.motion.invalid}) \u2014 re-record it via \`record next\`
14204
14736
  ` : "MOTION set-level motion context not yet recorded \u2014 `record next` names the call once the reps and token map are done\n" : "MOTION never asked \u2014 this set predates the motion-capture obligation (fresh plans record it; briefs prescribe default motion doctrine only)\n"
14205
14737
  );
14206
14738
  if ("unavailable" in composition) {
@@ -14208,7 +14740,7 @@ function runRecordStatus(opts) {
14208
14740
  `);
14209
14741
  } else if (composition.openPairs.length > 0) {
14210
14742
  process.stdout.write(
14211
- `COMPOSITION ${composition.openPairs.length} unconfirmed partner pair(s): ${composition.openPairs.map((p) => `${p.displayName} [pair-key ${p.pairKey}] (${p.instances} instance(s))`).join(", ")} \u2014 this set's recording references other recorded components. Before generating the HOST, record/generate the partner and have a human decide the pairing: \`${tendrilCommand(`compose --set ${path39.resolve(opts.setDir)}`)}\` lists it; confirmation is human-only.
14743
+ `COMPOSITION ${composition.openPairs.length} unconfirmed partner pair(s): ${composition.openPairs.map((p) => `${p.displayName} [pair-key ${p.pairKey}] (${p.instances} instance(s))`).join(", ")} \u2014 this set's recording references other recorded components. Before generating the HOST, record/generate the partner and have a human decide the pairing: \`${tendrilCommand(`compose --set ${path41.resolve(opts.setDir)}`)}\` lists it; confirmation is human-only.
14212
14744
  `
14213
14745
  );
14214
14746
  } else if (composition.confirmed > 0) {
@@ -14248,7 +14780,7 @@ function narrowedRoles(derived, override) {
14248
14780
  function rolesFromFile(opts, file, derived) {
14249
14781
  let json;
14250
14782
  try {
14251
- json = JSON.parse(readFileSync26(path39.resolve(file), "utf8"));
14783
+ json = JSON.parse(readFileSync29(path41.resolve(file), "utf8"));
14252
14784
  } catch (err) {
14253
14785
  fail(opts, ExitCode.InputValidation, {
14254
14786
  error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -14286,11 +14818,11 @@ function rolesFromFile(opts, file, derived) {
14286
14818
  };
14287
14819
  }
14288
14820
  function runRecordFinish(opts) {
14289
- if (!existsSync31(path39.join(opts.setDir, "recording-set.json"))) {
14821
+ if (!existsSync32(path41.join(opts.setDir, "recording-set.json"))) {
14290
14822
  fail(opts, ExitCode.InputValidation, {
14291
14823
  error: `no recording-set.json in ${opts.setDir}`,
14292
14824
  code: "no-recording-set",
14293
- remediation: `Run \`${tendrilCommand(`record plan --set ${path39.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
14825
+ remediation: `Run \`${tendrilCommand(`record plan --set ${path41.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
14294
14826
  });
14295
14827
  }
14296
14828
  const { manifest, raw } = readManifestFile(opts.setDir);
@@ -14318,17 +14850,17 @@ function runRecordFinish(opts) {
14318
14850
  fail(opts, ExitCode.ConfirmationRequired, {
14319
14851
  error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
14320
14852
  code: "roles-confirmation-not-interactive",
14321
- remediation: `A human runs \`${tendrilCommand(`record finish --set ${path39.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
14853
+ remediation: `A human runs \`${tendrilCommand(`record finish --set ${path41.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
14322
14854
  });
14323
14855
  }
14324
14856
  const merged = { ...raw, roles };
14325
- const { issues } = validateRecordingSet(merged, (rel) => existsSync31(path39.join(opts.setDir, rel)));
14857
+ const { issues } = validateRecordingSet(merged, (rel) => existsSync32(path41.join(opts.setDir, rel)));
14326
14858
  const errors = issues.filter((i) => i.severity === "error");
14327
14859
  if (errors.length > 0) {
14328
14860
  fail(opts, ExitCode.InputValidation, {
14329
14861
  error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
14330
14862
  code: "recording-set-invalid",
14331
- remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path39.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
14863
+ remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path41.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
14332
14864
  });
14333
14865
  }
14334
14866
  for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
@@ -14371,16 +14903,16 @@ var init_record = __esm({
14371
14903
  const shown = items.length <= MAX_SPOKEN_VALUES ? items : [...items.slice(0, MAX_SPOKEN_VALUES), `${items.length - MAX_SPOKEN_VALUES} more`];
14372
14904
  return shown.length <= 1 ? shown[0] ?? "" : `${shown.slice(0, -1).join(", ")} and ${shown[shown.length - 1]}`;
14373
14905
  };
14374
- ENVELOPE_HELP = `Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; VERBATIM MEANS EVERY PART, NOT JUST THE TEXT ONE: if a response carries additional content blocks \u2014 an image block especially \u2014 keep them in the envelope you save. get_design_context is documented to return a screenshot alongside the code, and whether it actually does is a question our whole recorded corpus cannot answer because every saved envelope holds text only. Saving what arrives settles it at no extra call. get_screenshot: pass an explicit maxDimension of AT LEAST the node's longer edge from the get_metadata you just recorded \u2014 the tool DEFAULTS TO 1024 and SCALES the node to fit, so a component wider or taller than that silently becomes a downscaled reference, which is not pixel ground truth (ingest now refuses a reference smaller than its own recorded box). Then 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.`;
14906
+ ENVELOPE_HELP = `Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; VERBATIM MEANS EVERY PART, NOT JUST THE TEXT ONE: if a response carries additional content blocks \u2014 an image block especially \u2014 keep them in the envelope you save. get_design_context is documented to return a screenshot alongside the code, and whether it actually does is a question our whole recorded corpus cannot answer because every saved envelope holds text only. Saving what arrives settles it at no extra call. get_screenshot: pass contentsOnly: true AND maxDimension: 4096 \u2014 always, on every call, whatever the node's size. contentsOnly renders the node in isolation; without it the export is what the CANVAS looks like, which for a node inside a component set includes the editor's own component-set chrome (#8a38f5 dashes) baked into the padding. That chrome is reference ink no implementation can paint, so it lands as absent-ink clusters and demotes an otherwise perfect component: measured 2026-08-19, a Modal scored 0/3 certified whose every absent cluster was chrome, and the same render against a contentsOnly reference scored 3/3 (ink 0.9949 -> 0.9996). Isolation does NOT drop the outer effects we deliberately capture \u2014 the padded dimensions come back byte-identical (1276x199 and 1836x175, drop shadow intact). maxDimension is a CAP, not a target: a 56x28 node requested at a high cap comes back 56x28, never upscaled (measured across 24 poses, 2026-08-19). The tool DEFAULTS TO 1024 and clamps anything longer, which silently yields a downscaled reference that is not pixel ground truth, and ingest refuses that. Do NOT compute the cap from the node box: Figma PADS the export to contain outer effects, so the reference is routinely larger than the box it came from \u2014 a recorded 1800px-wide modal exported at 1836px, and a cap sized to the box would have been refused. A flat generous cap removes the arithmetic and the whole failure class. Then 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.`;
14375
14907
  isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
14376
14908
  describeRoleLoss = (loss) => loss.kind === "main" ? `drops main "${loss.main}"` : `stops "${loss.part}" being a part of main "${loss.main}"`;
14377
14909
  }
14378
14910
  });
14379
14911
 
14380
14912
  // packages/cli/src/font-guidance.ts
14381
- import path40 from "node:path";
14913
+ import path42 from "node:path";
14382
14914
  function fontsUnprovenRemediation(setDir) {
14383
- const set = setDir === void 0 ? void 0 : path40.resolve(setDir);
14915
+ const set = setDir === void 0 ? void 0 : path42.resolve(setDir);
14384
14916
  if (set !== void 0) {
14385
14917
  try {
14386
14918
  const needs = recordedFontNeeds(set);
@@ -14455,8 +14987,8 @@ __export(fonts_exports, {
14455
14987
  runFontsResolveSet: () => runFontsResolveSet,
14456
14988
  runFontsStatus: () => runFontsStatus
14457
14989
  });
14458
- import { existsSync as existsSync32, readFileSync as readFileSync27 } from "node:fs";
14459
- import path41 from "node:path";
14990
+ import { existsSync as existsSync33, readFileSync as readFileSync30 } from "node:fs";
14991
+ import path43 from "node:path";
14460
14992
  async function runFontsResolve(opts) {
14461
14993
  const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
14462
14994
  const queryRefusal = systemFaceRefusal(opts.family.trim());
@@ -14477,7 +15009,7 @@ async function runFontsResolve(opts) {
14477
15009
  }
14478
15010
  }
14479
15011
  async function runFontsResolveSet(opts) {
14480
- const setDir = path41.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
15012
+ const setDir = path43.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
14481
15013
  let needs = [];
14482
15014
  try {
14483
15015
  needs = recordedFontNeeds(setDir);
@@ -14528,7 +15060,10 @@ async function runFontsResolveSet(opts) {
14528
15060
  }
14529
15061
  }
14530
15062
  const refusals = [...new Set(failures.map((f) => f.family))].map((family) => ({ family, reason: systemFaceRefusal(family.trim()) })).filter((r) => r.reason !== void 0);
14531
- emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures, ...refusals.length > 0 ? { refusals } : {} }, () => {
15063
+ const withAFace = new Set([...cached2, ...resolved, ...relicensed].map((f) => f.family.toLowerCase()));
15064
+ const familyMisses = failures.filter((f) => !withAFace.has(f.family.toLowerCase()));
15065
+ const weightMisses = failures.filter((f) => withAFace.has(f.family.toLowerCase()));
15066
+ emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures, familyMisses, weightMisses, ...refusals.length > 0 ? { refusals } : {} }, () => {
14532
15067
  process.stdout.write(`set declares: ${needs.map((n) => `${n.family} (${n.weights.join(", ")})`).join(" \xB7 ")}
14533
15068
  `);
14534
15069
  for (const f of cached2) process.stdout.write(`cached ${f.family} ${f.weight}
@@ -14554,22 +15089,31 @@ async function runFontsResolveSet(opts) {
14554
15089
  if (byteDrift.length > 0) {
14555
15090
  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`);
14556
15091
  }
14557
- if (failures.length > 0) {
14558
- warn(opts, `${failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
15092
+ if (weightMisses.length > 0) {
15093
+ const byFamily = [...new Set(weightMisses.map((f) => f.family))].map(
15094
+ (fam) => `"${fam}" ${weightMisses.filter((f) => f.family === fam).map((f) => f.weight).join(", ")}`
15095
+ );
15096
+ warn(
15097
+ opts,
15098
+ `weight instance(s) unavailable: ${byFamily.join("; ")} \u2014 the FAMILY is provisioned, so the mount renders these at the nearest cached weight. Verify reports that as an advisory and never gates on it, so this run is not a failure. If the exact instance matters, register a file you licence: ${tendrilCommand('fonts add "<Family>" <weight> <file>')}.`
15099
+ );
15100
+ }
15101
+ if (familyMisses.length > 0) {
15102
+ warn(opts, `${familyMisses.length} face(s) unresolved with NO cached face for the family \u2014 verification will refuse to certify under substitution`);
14559
15103
  process.exitCode = ExitCode.FontsUnproven;
14560
15104
  }
14561
15105
  }
14562
15106
  function runFontsStatus(opts) {
14563
- const manifestPath2 = path41.join(opts.cacheDir, "manifest.json");
14564
- if (!existsSync32(manifestPath2)) {
15107
+ const manifestPath2 = path43.join(opts.cacheDir, "manifest.json");
15108
+ if (!existsSync33(manifestPath2)) {
14565
15109
  fail(opts, ExitCode.FontsUnproven, {
14566
15110
  error: `no font cache at ${opts.cacheDir}`,
14567
15111
  code: "fonts-unresolved",
14568
15112
  remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
14569
15113
  });
14570
15114
  }
14571
- const faces = JSON.parse(readFileSync27(manifestPath2, "utf8"));
14572
- const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path41.resolve(opts.lock), opts.cacheDir) : null;
15115
+ const faces = JSON.parse(readFileSync30(manifestPath2, "utf8"));
15116
+ const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path43.resolve(opts.lock), opts.cacheDir) : null;
14573
15117
  emitData(opts, { faces, lockVerdicts }, () => {
14574
15118
  for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
14575
15119
  `);
@@ -14613,13 +15157,13 @@ function familyMismatch(family, declared) {
14613
15157
  }
14614
15158
  function runFontsAdd(opts) {
14615
15159
  if (opts.set !== void 0) {
14616
- const declared = taskFontFamilies(path41.resolve(opts.set)) ?? [];
15160
+ const declared = taskFontFamilies(path43.resolve(opts.set)) ?? [];
14617
15161
  const mismatch = familyMismatch(opts.family, declared);
14618
15162
  if (mismatch !== void 0) {
14619
15163
  fail(opts, ExitCode.InputValidation, {
14620
15164
  error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. Adding it under this name would cache a face the mount never matches, and scoring would keep refusing for the family that is still missing.`,
14621
15165
  code: "font-family-not-declared",
14622
- remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${path41.resolve(opts.set)}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set to add a family this set does not declare.`
15166
+ remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${path43.resolve(opts.set)}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set to add a family this set does not declare.`
14623
15167
  });
14624
15168
  }
14625
15169
  } else {
@@ -14678,13 +15222,13 @@ cache them: ${tendrilCommand(`fonts add-system ${quoteArg(opts.family)}`)}
14678
15222
  }
14679
15223
  function runFontsAddSystem(opts) {
14680
15224
  if (opts.set !== void 0) {
14681
- const declared = taskFontFamilies(path41.resolve(opts.set)) ?? [];
15225
+ const declared = taskFontFamilies(path43.resolve(opts.set)) ?? [];
14682
15226
  const mismatch = familyMismatch(opts.family, declared);
14683
15227
  if (mismatch !== void 0) {
14684
15228
  fail(opts, ExitCode.InputValidation, {
14685
15229
  error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. A face cached under a name the mount never matches leaves scoring refusing for the family that is still missing.`,
14686
15230
  code: "font-family-not-declared",
14687
- remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(path41.resolve(opts.set))}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set.`
15231
+ remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(path43.resolve(opts.set))}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set.`
14688
15232
  });
14689
15233
  }
14690
15234
  } else {
@@ -14734,12 +15278,12 @@ var init_fonts = __esm({
14734
15278
  });
14735
15279
 
14736
15280
  // packages/cli/src/profile-input.ts
14737
- import { existsSync as existsSync33, readFileSync as readFileSync28 } from "node:fs";
14738
- import path42 from "node:path";
15281
+ import { existsSync as existsSync34, readFileSync as readFileSync31 } from "node:fs";
15282
+ import path44 from "node:path";
14739
15283
  function loadCodebaseProfile(flags, profilePath) {
14740
15284
  if (profilePath === void 0) return null;
14741
- const abs = path42.resolve(profilePath);
14742
- if (!existsSync33(abs)) {
15285
+ const abs = path44.resolve(profilePath);
15286
+ if (!existsSync34(abs)) {
14743
15287
  fail(flags, ExitCode.InputValidation, {
14744
15288
  error: `no profile at ${abs}`,
14745
15289
  code: "profile_missing",
@@ -14747,7 +15291,7 @@ function loadCodebaseProfile(flags, profilePath) {
14747
15291
  });
14748
15292
  }
14749
15293
  try {
14750
- return readCodebaseProfile(readFileSync28(abs, "utf8"));
15294
+ return readCodebaseProfile(readFileSync31(abs, "utf8"));
14751
15295
  } catch (error) {
14752
15296
  fail(flags, ExitCode.InputValidation, {
14753
15297
  error: `profile at ${abs} is not a valid codebase profile: ${error instanceof Error ? error.message : String(error)}`,
@@ -14789,8 +15333,8 @@ __export(verify_exports, {
14789
15333
  runVerify: () => runVerify,
14790
15334
  verdictCaveatsFor: () => verdictCaveatsFor
14791
15335
  });
14792
- import { existsSync as existsSync34, readFileSync as readFileSync29 } from "node:fs";
14793
- import path43 from "node:path";
15336
+ import { existsSync as existsSync35, readFileSync as readFileSync32, rmSync as rmSync5, writeFileSync as writeFileSync14 } from "node:fs";
15337
+ import path45 from "node:path";
14794
15338
  function interactionCoverage(behaviors) {
14795
15339
  const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
14796
15340
  const content = behaviors.filter((b) => b.id.startsWith("content-prop-renders("));
@@ -14867,20 +15411,29 @@ function operabilityLine(state) {
14867
15411
  return void 0;
14868
15412
  }
14869
15413
  function foldConfigStatus(s, failDemotions, substitutedFamilies) {
14870
- const { exact: _exact, ...reported } = s;
14871
15414
  let status = tierOf(s, BARS2.cert);
14872
15415
  const certDemote = [];
14873
15416
  let absentInkDemoted = false;
14874
15417
  if (status === "certified" && s.absentInk !== void 0 && s.absentInk.length > 0) {
14875
15418
  status = "pass";
14876
15419
  absentInkDemoted = true;
14877
- certDemote.push(...s.absentInk.map((c) => `absent ink: ${c.name ?? "unnamed region"} \u2014 ${c.px}px of recorded ink with no render ink within reach (missing or invisible feature)`));
15420
+ certDemote.push(
15421
+ ...s.absentInk.map(
15422
+ (c) => (
15423
+ // A chrome cluster is a defect in the RECORDING, and saying
15424
+ // "missing or invisible feature" points the reader at code that
15425
+ // is not wrong — run 26's operator spent a round hunting a
15426
+ // layout bug that did not exist.
15427
+ c.chrome === true ? `absent ink: ${c.px}px of Figma's component-set CHROME baked into the reference \u2014 this is the editor's own outline, not your component, and no implementation can paint it. RE-RECORD this rep with get_screenshot contentsOnly: true (the outer effect survives isolation; only the chrome goes).` : `absent ink: ${c.name ?? "unnamed region"} \u2014 ${c.px}px of recorded ink with no render ink within reach (missing or invisible feature)`
15428
+ )
15429
+ )
15430
+ );
14878
15431
  }
14879
15432
  if (status === "certified" && substitutedFamilies.length > 0) {
14880
15433
  status = "pass";
14881
15434
  certDemote.push(`substituted fonts: cache lacks ${substitutedFamilies.map((f) => `"${f}"`).join(", ")} \u2014 certification never measures a substitute face`);
14882
15435
  }
14883
- const base = certDemote.length > 0 ? { ...reported, status, demotedBy: certDemote } : { ...reported, status };
15436
+ const base = certDemote.length > 0 ? { ...s, status, demotedBy: certDemote } : { ...s, status };
14884
15437
  return {
14885
15438
  row: failDemotions === void 0 ? base : { ...base, status: "fail", demotedBy: [...certDemote, ...failDemotions] },
14886
15439
  absentInkDemoted
@@ -15022,10 +15575,26 @@ function compositionReport(input) {
15022
15575
  function eyeCheck(bundleDir) {
15023
15576
  return {
15024
15577
  command: tendrilCommand(`inspect "${bundleDir}"`),
15025
- sheetPath: path43.join(bundleDir, "verify-evidence", "inspect.html"),
15578
+ sheetPath: path45.join(bundleDir, "verify-evidence", "inspect.html"),
15026
15579
  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."
15027
15580
  };
15028
15581
  }
15582
+ function evidenceArtifacts(evidenceDir, reps) {
15583
+ const named = (name) => existsSync35(path45.join(evidenceDir, name)) ? name : null;
15584
+ return {
15585
+ legend: named("diff-legend.txt"),
15586
+ configs: reps.map((rep) => {
15587
+ const absentInk = [];
15588
+ for (let i = 0; ; i += 1) {
15589
+ const ref = named(`${rep}-absent-${i}-ref.png`);
15590
+ const render = named(`${rep}-absent-${i}-render.png`);
15591
+ if (ref === null || render === null) break;
15592
+ absentInk.push({ ref, render });
15593
+ }
15594
+ return { rep, render: named(`${rep}-render.png`), ref: named(`${rep}-ref.png`), diff: named(`${rep}-diff.png`), absentInk };
15595
+ })
15596
+ };
15597
+ }
15029
15598
  function failureTally(t) {
15030
15599
  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)`;
15031
15600
  }
@@ -15059,7 +15628,7 @@ function taskFromManifest(opts, manifest, setDir) {
15059
15628
  const adapterSlugs = Object.keys(manifest.propAdapter);
15060
15629
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
15061
15630
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
15062
- const registry = Object.values(TASKS).find((t) => path43.resolve(t.set) === path43.resolve(setDir));
15631
+ const registry = Object.values(TASKS).find((t) => path45.resolve(t.set) === path45.resolve(setDir));
15063
15632
  const authored = (() => {
15064
15633
  if (registry !== void 0) return void 0;
15065
15634
  try {
@@ -15119,19 +15688,19 @@ function verdictCaveatsFor(input) {
15119
15688
  async function runVerify(opts) {
15120
15689
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
15121
15690
  let recordingSetDrift;
15122
- const setOverride = opts.set !== void 0 ? path43.resolve(callerCwd, opts.set) : void 0;
15123
- opts = { ...opts, bundleDir: path43.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
15124
- if (!existsSync34(opts.bundleDir)) {
15691
+ const setOverride = opts.set !== void 0 ? path45.resolve(callerCwd, opts.set) : void 0;
15692
+ opts = { ...opts, bundleDir: path45.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
15693
+ if (!existsSync35(opts.bundleDir)) {
15125
15694
  fail(opts, ExitCode.InputValidation, {
15126
15695
  error: `bundle directory not found: ${opts.bundleDir}`,
15127
15696
  code: "bundle-missing",
15128
15697
  remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
15129
15698
  });
15130
15699
  }
15131
- const manifestPath2 = path43.join(opts.bundleDir, "component.json");
15700
+ const manifestPath2 = path45.join(opts.bundleDir, "component.json");
15132
15701
  let manifest;
15133
- if (existsSync34(manifestPath2)) {
15134
- const { manifest: parsed, issues } = readBundleManifest(readFileSync29(manifestPath2, "utf8"));
15702
+ if (existsSync35(manifestPath2)) {
15703
+ const { manifest: parsed, issues } = readBundleManifest(readFileSync32(manifestPath2, "utf8"));
15135
15704
  if (issues.length > 0) {
15136
15705
  fail(opts, ExitCode.InputValidation, {
15137
15706
  error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -15162,21 +15731,21 @@ async function runVerify(opts) {
15162
15731
  task = registry;
15163
15732
  } else if (manifest !== void 0) {
15164
15733
  const resolveSetDir = (p) => {
15165
- if (path43.isAbsolute(p)) return p;
15166
- const fromRepo = path43.resolve(REPO_ROOT, p);
15167
- if (existsSync34(fromRepo)) return fromRepo;
15168
- return path43.resolve(callerCwd, p);
15734
+ if (path45.isAbsolute(p)) return p;
15735
+ const fromRepo = path45.resolve(REPO_ROOT, p);
15736
+ if (existsSync35(fromRepo)) return fromRepo;
15737
+ return path45.resolve(callerCwd, p);
15169
15738
  };
15170
15739
  const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
15171
- if (!existsSync34(path43.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path43.resolve(t.set) === path43.resolve(setDir))) {
15740
+ if (!existsSync35(path45.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path45.resolve(t.set) === path45.resolve(setDir))) {
15172
15741
  fail(opts, ExitCode.RecordingIncomplete, {
15173
15742
  error: `recording set not found or unmanifested: ${setDir}`,
15174
15743
  code: "recording-set-missing",
15175
15744
  remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
15176
15745
  });
15177
15746
  }
15178
- const registry = Object.values(TASKS).find((t) => path43.resolve(t.set) === path43.resolve(setDir));
15179
- if (registry !== void 0 && !existsSync34(path43.join(setDir, "recording-set.json"))) {
15747
+ const registry = Object.values(TASKS).find((t) => path45.resolve(t.set) === path45.resolve(setDir));
15748
+ if (registry !== void 0 && !existsSync35(path45.join(setDir, "recording-set.json"))) {
15180
15749
  const recordedSlugs = registry.configs.map((c) => c.rep);
15181
15750
  const adapterSlugs = Object.keys(manifest.propAdapter);
15182
15751
  unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
@@ -15208,9 +15777,9 @@ async function runVerify(opts) {
15208
15777
  recordingSetDrift = { stamped: manifest.provenance.recordingSet.hash, current: hash };
15209
15778
  }
15210
15779
  for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
15211
- const p = path43.join(opts.bundleDir, name);
15212
- if (!existsSync34(p)) continue;
15213
- const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync29(p)));
15780
+ const p = path45.join(opts.bundleDir, name);
15781
+ if (!existsSync35(p)) continue;
15782
+ const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync32(p)));
15214
15783
  if (issues.length > 0) {
15215
15784
  fail(opts, ExitCode.InputValidation, {
15216
15785
  error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -15248,7 +15817,7 @@ async function runVerify(opts) {
15248
15817
  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)`);
15249
15818
  }
15250
15819
  const missing = task.configs.filter(
15251
- (c) => !existsSync34(path43.join(task.set, c.rep, "get_screenshot.json")) || !existsSync34(path43.join(task.set, c.rep, "get_metadata.json"))
15820
+ (c) => !existsSync35(path45.join(task.set, c.rep, "get_screenshot.json")) || !existsSync35(path45.join(task.set, c.rep, "get_metadata.json"))
15252
15821
  );
15253
15822
  if (missing.length > 0) {
15254
15823
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -15258,7 +15827,8 @@ async function runVerify(opts) {
15258
15827
  });
15259
15828
  }
15260
15829
  const bar = BARS2[opts.bar];
15261
- const evidenceDir = path43.join(opts.bundleDir, "verify-evidence");
15830
+ const evidenceDir = path45.join(opts.bundleDir, "verify-evidence");
15831
+ rmSync5(evidenceDir, { recursive: true, force: true });
15262
15832
  const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
15263
15833
  const quality = await checkBundleQuality(
15264
15834
  opts.bundleDir,
@@ -15275,7 +15845,7 @@ async function runVerify(opts) {
15275
15845
  // ASKED, never "follows every convention".
15276
15846
  loadCodebaseProfile(opts, opts.profile) ?? void 0
15277
15847
  );
15278
- const bundleCss = ["tokens.css", "styles.css"].map((f) => path43.join(opts.bundleDir, f)).filter((f) => existsSync34(f)).map((f) => readFileSync29(f, "utf8")).join("\n");
15848
+ const bundleCss = ["tokens.css", "styles.css"].map((f) => path45.join(opts.bundleDir, f)).filter((f) => existsSync35(f)).map((f) => readFileSync32(f, "utf8")).join("\n");
15279
15849
  const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss, { ...authorityConfigs !== void 0 ? { authority: authorityConfigs } : {} });
15280
15850
  const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
15281
15851
  const framing = checkAdapterFraming(
@@ -15291,10 +15861,10 @@ async function runVerify(opts) {
15291
15861
  warn(opts, `compositions extension REJECTED (${crossComposition.malformed}) \u2014 the cross-bundle backstop did NOT run over it; repair the manifest entry and re-verify. This is an instrument failure, not a clean bill.`);
15292
15862
  }
15293
15863
  const verifyCallerCwd = process.env["INIT_CWD"] ?? process.cwd();
15294
- const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ? path43.resolve(verifyCallerCwd, opts.library) : verifyCallerCwd]) : { pins: [], issues: [] };
15864
+ const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ? path45.resolve(verifyCallerCwd, opts.library) : verifyCallerCwd]) : { pins: [], issues: [] };
15295
15865
  const staticComposedChecks = composedChecks(opts.bundleDir, task.entry, verifyPins.pins);
15296
15866
  const renderExpectations = verifyPins.pins.map((pin) => ({
15297
- modulePath: path43.join(composedModuleDir(pin.partnerName), pin.entryComponent),
15867
+ modulePath: path45.join(composedModuleDir(pin.partnerName), pin.entryComponent),
15298
15868
  component: pin.entryComponent,
15299
15869
  stampName: `${pin.pairKey}:${pin.entryComponent}`,
15300
15870
  hostReps: [...new Set(pin.instances.map((i) => i.hostRep))],
@@ -15352,8 +15922,10 @@ async function runVerify(opts) {
15352
15922
  }
15353
15923
  })();
15354
15924
  const compositionBlock = compositionReport({ availability, structural, crops, regions: regionsOut, composedPairs: composedPairs.size });
15925
+ const scoredFiles = digestScoredFiles(opts.bundleDir, manifest !== void 0 ? manifest.entry : task.entry);
15355
15926
  const report = {
15356
15927
  bundle: opts.bundleDir,
15928
+ scoredFiles,
15357
15929
  ...opts.task !== void 0 ? { task: opts.task } : {},
15358
15930
  ...manifest !== void 0 ? { bundleManifest: { name: manifest.name, bundleVersion: manifest.bundleVersion, recordingSetHash: manifest.provenance.recordingSet.hash } } : {},
15359
15931
  targetBar: opts.bar,
@@ -15418,7 +15990,7 @@ async function runVerify(opts) {
15418
15990
  } : {},
15419
15991
  configs: statuses,
15420
15992
  behaviors,
15421
- evidence: { dir: evidenceDir, files: "per config: <rep>-render.png, <rep>-ref.png, <rep>-diff.png" },
15993
+ evidence: { dir: evidenceDir, ...evidenceArtifacts(evidenceDir, statuses.map((s) => s.rep)) },
15422
15994
  composition: compositionBlock,
15423
15995
  verdict: ok ? "verified" : "verification-failed",
15424
15996
  // Run-23 R5: the one-word verdict printed beside "coverage
@@ -15579,7 +16151,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
15579
16151
  }
15580
16152
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
15581
16153
  `);
15582
- const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path43.join(opts.bundleDir, f)).filter((f) => existsSync34(f)).map((f) => readFileSync29(f, "utf8")).join("\n")));
16154
+ const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path45.join(opts.bundleDir, f)).filter((f) => existsSync35(f)).map((f) => readFileSync32(f, "utf8")).join("\n")));
15583
16155
  if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
15584
16156
  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)
15585
16157
  `);
@@ -15612,7 +16184,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
15612
16184
  if (certBlockedByAbsentInk.length > 0) {
15613
16185
  warn(
15614
16186
  opts,
15615
- `${certBlockedByAbsentInk.length} config(s) demoted by absent-ink clusters (${certBlockedByAbsentInk.join(", ")}) \u2014 certification never ships a missing or invisible feature; each cluster is named in the report above. Fix the component and re-verify, or verify at --bar pass for the disclosed result.`
16187
+ `${certBlockedByAbsentInk.length} config(s) demoted by absent-ink clusters (${certBlockedByAbsentInk.join(", ")}) \u2014 certification never ships a missing or invisible feature; each cluster is named in the report above. Fix the component and re-verify, or verify at --bar pass for the disclosed result. BEFORE changing code, check WHERE the clusters sit: open the -absent-*-ref.png crops in the evidence dir. If the missing ink lies at the extreme EDGES of the reference rather than inside the component, it is probably not yours \u2014 Figma pads an export to contain outer effects (a shadow, a glow), and the pad can capture the editor's own component-set chrome, which is purple #8a38f5 and which no correct implementation can ever paint. Measured 2026-08-19: a shadowed modal was demoted 0/3 with EVERY absent cluster made of chrome, while the same kit's unshadowed button, exported with no pad, certified 24/24 \u2014 and re-recording with contentsOnly: true took the same code to 3/3. That is a recording artifact, not a defect in your component.`
15616
16188
  );
15617
16189
  process.exitCode = ExitCode.VerificationFailed;
15618
16190
  }
@@ -15631,8 +16203,27 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
15631
16203
  );
15632
16204
  process.exitCode = ExitCode.VerificationFailed;
15633
16205
  }
16206
+ persistReport(opts, report, evidenceDir);
16207
+ }
16208
+ function persistReport(opts, report, evidenceDir) {
16209
+ if (!existsSync35(evidenceDir)) return;
16210
+ const rulerExit = process.exitCode === void 0 ? 0 : Number(process.exitCode);
16211
+ const withExit = {
16212
+ ...report,
16213
+ rulerExit,
16214
+ ...rulerExit !== 0 ? { rulerRefusal: EXIT_REFUSALS[rulerExit] ?? `ruler exited ${rulerExit}` } : {}
16215
+ };
16216
+ try {
16217
+ writeFileSync14(
16218
+ path45.join(evidenceDir, VERIFY_REPORT_FILENAME),
16219
+ `${JSON.stringify(verifyReportForTransport(withExit, path45.basename(opts.bundleDir)), null, 2)}
16220
+ `
16221
+ );
16222
+ } catch (e) {
16223
+ warn(opts, `could not write ${VERIFY_REPORT_FILENAME} (${e instanceof Error ? e.message : String(e)}) \u2014 the verdict above stands, but this run left no persisted report beside its evidence.`);
16224
+ }
15634
16225
  }
15635
- var BARS2, NO_INTERACTIVE_POSES, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES, COMPOSE_ON_GENERATE_ARMED, NO_OVERLAY_DECLARED;
16226
+ var BARS2, NO_INTERACTIVE_POSES, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES, COMPOSE_ON_GENERATE_ARMED, NO_OVERLAY_DECLARED, EXIT_REFUSALS;
15636
16227
  var init_verify = __esm({
15637
16228
  "packages/cli/src/commands/verify.ts"() {
15638
16229
  "use strict";
@@ -15662,6 +16253,10 @@ var init_verify = __esm({
15662
16253
  UNSTAMPED_ROLES = "unstamped";
15663
16254
  COMPOSE_ON_GENERATE_ARMED = false;
15664
16255
  NO_OVERLAY_DECLARED = "no overlay declared";
16256
+ EXIT_REFUSALS = {
16257
+ [ExitCode.VerificationFailed]: "the ruler refused this run: one or more checks fell below the requested bar",
16258
+ [ExitCode.FontsUnproven]: "the ruler refused this run: required font faces were not provisioned, so scores measure a substitute"
16259
+ };
15665
16260
  }
15666
16261
  });
15667
16262
 
@@ -15671,11 +16266,11 @@ __export(engine_exports, {
15671
16266
  runEngineBrief: () => runEngineBrief,
15672
16267
  runEngineScore: () => runEngineScore
15673
16268
  });
15674
- import { appendFileSync, existsSync as existsSync35, mkdirSync as mkdirSync9, readFileSync as readFileSync30, writeFileSync as writeFileSync13 } from "node:fs";
15675
- import path44 from "node:path";
16269
+ import { appendFileSync, existsSync as existsSync36, mkdirSync as mkdirSync10, readFileSync as readFileSync33, writeFileSync as writeFileSync15 } from "node:fs";
16270
+ import path46 from "node:path";
15676
16271
  function resolveEngineTask(opts, callerCwd) {
15677
- const asPath = path44.resolve(callerCwd, opts.taskOrSet);
15678
- const isSet = existsSync35(path44.join(asPath, "recording-set.json"));
16272
+ const asPath = path46.resolve(callerCwd, opts.taskOrSet);
16273
+ const isSet = existsSync36(path46.join(asPath, "recording-set.json"));
15679
16274
  const registry = TASKS[opts.taskOrSet];
15680
16275
  if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
15681
16276
  if (isSet) {
@@ -15684,7 +16279,7 @@ function resolveEngineTask(opts, callerCwd) {
15684
16279
  for (const d of authored.disclosures) warn(opts, d);
15685
16280
  return {
15686
16281
  task: authored.task,
15687
- name: path44.basename(asPath),
16282
+ name: path46.basename(asPath),
15688
16283
  ref: asPath,
15689
16284
  disclosures: authored.disclosures,
15690
16285
  interactionEvidence: authored.api.interactionEvidence,
@@ -15712,13 +16307,13 @@ function runEngineBrief(opts) {
15712
16307
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
15713
16308
  const { task, name, ref, disclosures } = resolveEngineTask(opts, callerCwd);
15714
16309
  const bar = BARS3[opts.bar];
15715
- if (existsSync35(path44.join(task.set, "recording-set.json"))) {
16310
+ if (existsSync36(path46.join(task.set, "recording-set.json"))) {
15716
16311
  try {
15717
- const { open } = compositionPairsFor(path44.resolve(task.set), [opts.library !== void 0 ? path44.resolve(callerCwd, opts.library) : callerCwd]);
16312
+ const { open } = compositionPairsFor(path46.resolve(task.set), [opts.library !== void 0 ? path46.resolve(callerCwd, opts.library) : callerCwd]);
15718
16313
  if (open.length > 0) {
15719
16314
  const componentNames = [...new Set(open.map((p) => p.displayName))];
15720
16315
  disclosures.push(
15721
- `COMPOSITION PAIRS UNDECIDED: this recording references ${componentNames.length} other recorded component(s) (${open.map((p) => `${p.displayName} [pair-key ${p.key}]: ${p.instances.length} instance(s)`).join("; ")}) and no human has decided the pairing. This brief treats the component as SELF-CONTAINED \u2014 you will re-implement the partner's pixels locally. If composition is wanted: generate the partner bundle first, have a human run \`${tendrilCommand(`compose --set ${path44.resolve(task.set)}`)}\` in their terminal, and re-emit this brief. Proceeding as-is is valid; it just is not composition.`
16316
+ `COMPOSITION PAIRS UNDECIDED: this recording references ${componentNames.length} other recorded component(s) (${open.map((p) => `${p.displayName} [pair-key ${p.key}]: ${p.instances.length} instance(s)`).join("; ")}) and no human has decided the pairing. This brief treats the component as SELF-CONTAINED \u2014 you will re-implement the partner's pixels locally. If composition is wanted: generate the partner bundle first, have a human run \`${tendrilCommand(`compose --set ${path46.resolve(task.set)}`)}\` in their terminal, and re-emit this brief. Proceeding as-is is valid; it just is not composition.`
15722
16317
  );
15723
16318
  }
15724
16319
  } catch (err) {
@@ -15735,9 +16330,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
15735
16330
  const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock + motionBriefSection(task.set) + conventions;
15736
16331
  const segments = buildSegments(task, "files");
15737
16332
  let notRecorded;
15738
- const manifestPath2 = path44.join(task.set, "recording-set.json");
15739
- if (existsSync35(manifestPath2)) {
15740
- notRecorded = JSON.parse(readFileSync30(manifestPath2, "utf8")).notRecorded;
16333
+ const manifestPath2 = path46.join(task.set, "recording-set.json");
16334
+ if (existsSync36(manifestPath2)) {
16335
+ notRecorded = JSON.parse(readFileSync33(manifestPath2, "utf8")).notRecorded;
15741
16336
  }
15742
16337
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
15743
16338
 
@@ -15745,7 +16340,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
15745
16340
  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.
15746
16341
  ${notRecorded}` : "";
15747
16342
  let fontProvisioning;
15748
- if (existsSync35(manifestPath2)) {
16343
+ if (existsSync36(manifestPath2)) {
15749
16344
  const missingFams = unprovisionedFamilies(task.set);
15750
16345
  const unprovided = unprovisionedFaces(task.set);
15751
16346
  const weightOnly = missingFams.length === 0;
@@ -15767,7 +16362,7 @@ ${notRecorded}` : "";
15767
16362
  };
15768
16363
  }
15769
16364
  }
15770
- const pinsResult = composedPins(task.set, [opts.library !== void 0 ? path44.resolve(callerCwd, opts.library) : callerCwd]);
16365
+ const pinsResult = composedPins(task.set, [opts.library !== void 0 ? path46.resolve(callerCwd, opts.library) : callerCwd]);
15771
16366
  const jsxProp = ([k, v]) => typeof v === "string" ? `${k}=${JSON.stringify(v)}` : `${k}={${JSON.stringify(v)}}`;
15772
16367
  const composedBlock = pinsResult.pins.length === 0 && pinsResult.issues.length === 0 ? "" : (pinsResult.pins.length > 0 ? `
15773
16368
 
@@ -15803,10 +16398,10 @@ ${pinsResult.issues.map((i) => `- ${i}`).join("\n")}` : "");
15803
16398
 
15804
16399
  === TASK PAYLOAD (recorded truth, verbatim) ===
15805
16400
  ${segments}`;
15806
- const payloadFile = path44.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
15807
- const candidateDirSuggestion = path44.resolve(callerCwd, `tendril-out/${name}-candidate`);
15808
- mkdirSync9(path44.dirname(payloadFile), { recursive: true });
15809
- writeFileSync13(payloadFile, payload);
16401
+ const payloadFile = path46.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
16402
+ const candidateDirSuggestion = path46.resolve(callerCwd, `tendril-out/${name}-candidate`);
16403
+ mkdirSync10(path46.dirname(payloadFile), { recursive: true });
16404
+ writeFileSync15(payloadFile, payload);
15810
16405
  emitData(
15811
16406
  opts,
15812
16407
  {
@@ -15852,7 +16447,7 @@ ${segments}`;
15852
16447
  // command must search the same bundle roots the pins came
15853
16448
  // from, or the oracle and the brief describe different worlds.
15854
16449
  `Run \`${tendrilCommand(
15855
- `engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(path44.resolve(callerCwd, opts.library))}` : ""} --json`
16450
+ `engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(path46.resolve(callerCwd, opts.library))}` : ""} --json`
15856
16451
  )}\` \u2014 the CLI is the sole judge, and it refuses to score an undeclared model. If you wrote the bundle elsewhere, pass that directory instead.`,
15857
16452
  "Apply the returned feedback and re-score. Stop when all checks pass. Two consecutive non-improving scores mean INSPECT the verify-evidence diffs before deciding \u2014 stop only when inspection yields no fix hypothesis (a measured run was byte-identical twice, then went 27/27 after reading the diffs).",
15858
16453
  "Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
@@ -15867,8 +16462,8 @@ ${segments}`;
15867
16462
  );
15868
16463
  }
15869
16464
  function appendScoreHistory(candidateDir, entry) {
15870
- const file = path44.join(candidateDir, "score-history.jsonl");
15871
- const starts = existsSync35(file) ? readFileSync30(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
16465
+ const file = path46.join(candidateDir, "score-history.jsonl");
16466
+ const starts = existsSync36(file) ? readFileSync33(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
15872
16467
  const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
15873
16468
  appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
15874
16469
  `);
@@ -15876,9 +16471,9 @@ function appendScoreHistory(candidateDir, entry) {
15876
16471
  async function runEngineScore(opts) {
15877
16472
  requireEntitlement(opts);
15878
16473
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
15879
- const candidateDir = path44.resolve(callerCwd, opts.candidateDir);
16474
+ const candidateDir = path46.resolve(callerCwd, opts.candidateDir);
15880
16475
  const { task, name, apiPin, interactionEvidence, unmappedInteractionEvidence: interactionEvidenceUnmapped, satisfiabilityDemoted, expertContract } = resolveEngineTask(opts, callerCwd);
15881
- if (!existsSync35(candidateDir)) {
16476
+ if (!existsSync36(candidateDir)) {
15882
16477
  fail(opts, ExitCode.InputValidation, {
15883
16478
  error: `candidate directory not found: ${candidateDir}`,
15884
16479
  code: "candidate-missing",
@@ -15903,10 +16498,10 @@ async function runEngineScore(opts) {
15903
16498
  for (const g of missingWeights(task.set)) {
15904
16499
  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)`);
15905
16500
  }
15906
- if (opts.rebind !== true && existsSync35(path44.join(candidateDir, "component.json"))) {
16501
+ if (opts.rebind !== true && existsSync36(path46.join(candidateDir, "component.json"))) {
15907
16502
  const prior = (() => {
15908
16503
  try {
15909
- const read = readBundleManifest(readFileSync30(path44.join(candidateDir, "component.json"), "utf8"));
16504
+ const read = readBundleManifest(readFileSync33(path46.join(candidateDir, "component.json"), "utf8"));
15910
16505
  return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
15911
16506
  } catch {
15912
16507
  return { unreadable: true };
@@ -15928,12 +16523,12 @@ async function runEngineScore(opts) {
15928
16523
  }
15929
16524
  }
15930
16525
  const bar = BARS3[opts.bar];
15931
- const evidenceDir = path44.join(candidateDir, "verify-evidence");
16526
+ const evidenceDir = path46.join(candidateDir, "verify-evidence");
15932
16527
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
15933
16528
  const parity = await checkHoverParity(task, candidateDir, task.configs);
15934
16529
  const occlusionRows = await checkSiblingOcclusion(task, candidateDir, candidateCss(candidateDir), { authority: task.configs });
15935
16530
  const occlusionFailures = occlusionRows.filter((o) => !o.pass);
15936
- const scorePins = composedPins(task.set, [opts.library !== void 0 ? path44.resolve(callerCwd, opts.library) : callerCwd]);
16531
+ const scorePins = composedPins(task.set, [opts.library !== void 0 ? path46.resolve(callerCwd, opts.library) : callerCwd]);
15937
16532
  const composition = composedChecks(candidateDir, task.entry, scorePins.pins);
15938
16533
  const behaviors = [...await checkBehaviors(task, candidateDir), ...parity, ...composition];
15939
16534
  const parityCoverage = parity.length > 0 ? `${parity.filter((p) => p.pass).length}/${parity.length} hover-forced configs` : "not applicable (no hover-forced configs in this set)";
@@ -16151,11 +16746,11 @@ var codeconnect_exports = {};
16151
16746
  __export(codeconnect_exports, {
16152
16747
  runCodeConnect: () => runCodeConnect
16153
16748
  });
16154
- import { existsSync as existsSync36, readFileSync as readFileSync31, writeFileSync as writeFileSync14 } from "node:fs";
16155
- import path45 from "node:path";
16749
+ import { existsSync as existsSync37, readFileSync as readFileSync34, writeFileSync as writeFileSync16 } from "node:fs";
16750
+ import path47 from "node:path";
16156
16751
  function runCodeConnect(opts) {
16157
16752
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
16158
- const bundleDir = path45.resolve(callerCwd, opts.bundleDir);
16753
+ const bundleDir = path47.resolve(callerCwd, opts.bundleDir);
16159
16754
  let url;
16160
16755
  try {
16161
16756
  url = new URL(opts.figmaUrl);
@@ -16171,7 +16766,7 @@ function runCodeConnect(opts) {
16171
16766
  }
16172
16767
  let manifest;
16173
16768
  try {
16174
- const read = readBundleManifest(readFileSync31(path45.join(bundleDir, "component.json"), "utf8"));
16769
+ const read = readBundleManifest(readFileSync34(path47.join(bundleDir, "component.json"), "utf8"));
16175
16770
  if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
16176
16771
  manifest = read.manifest;
16177
16772
  } catch (err) {
@@ -16181,8 +16776,8 @@ function runCodeConnect(opts) {
16181
16776
  remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
16182
16777
  });
16183
16778
  }
16184
- const setDir = path45.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
16185
- if (!existsSync36(path45.join(setDir, "recording-set.json"))) {
16779
+ const setDir = path47.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
16780
+ if (!existsSync37(path47.join(setDir, "recording-set.json"))) {
16186
16781
  fail(opts, ExitCode.InputValidation, {
16187
16782
  error: `recording set not found at ${setDir}`,
16188
16783
  code: "codeconnect-no-set",
@@ -16203,10 +16798,10 @@ function runCodeConnect(opts) {
16203
16798
  const component = api.component;
16204
16799
  const recManifest = loadManifest(setDir);
16205
16800
  const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
16206
- const meta = path45.join(setDir, r.slug, "get_metadata.json");
16207
- if (!existsSync36(meta)) return void 0;
16801
+ const meta = path47.join(setDir, r.slug, "get_metadata.json");
16802
+ if (!existsSync37(meta)) return void 0;
16208
16803
  try {
16209
- return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync31(meta, "utf8"))))?.[1];
16804
+ return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync34(meta, "utf8"))))?.[1];
16210
16805
  } catch {
16211
16806
  return void 0;
16212
16807
  }
@@ -16271,7 +16866,7 @@ function runCodeConnect(opts) {
16271
16866
  axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
16272
16867
  fragmentVars.push(varName);
16273
16868
  }
16274
- const entryRel = path45.relative(callerCwd, path45.join(bundleDir, manifest.entry));
16869
+ const entryRel = path47.relative(callerCwd, path47.join(bundleDir, manifest.entry));
16275
16870
  const trust = manifest.trustStatement.split("\n")[0] ?? "";
16276
16871
  const lines = [
16277
16872
  `// url=${opts.figmaUrl}`,
@@ -16295,8 +16890,8 @@ function runCodeConnect(opts) {
16295
16890
  `}`,
16296
16891
  ``
16297
16892
  ].join("\n");
16298
- const outFile = path45.resolve(callerCwd, opts.out ?? path45.join(bundleDir, `${component}.figma.ts`));
16299
- writeFileSync14(outFile, lines);
16893
+ const outFile = path47.resolve(callerCwd, opts.out ?? path47.join(bundleDir, `${component}.figma.ts`));
16894
+ writeFileSync16(outFile, lines);
16300
16895
  emitData(
16301
16896
  opts,
16302
16897
  {
@@ -16334,18 +16929,18 @@ var init_codeconnect = __esm({
16334
16929
  });
16335
16930
 
16336
16931
  // packages/mcp/src/server.ts
16337
- import { createHash as createHash8 } from "node:crypto";
16338
- import { existsSync as existsSync37, mkdtempSync as mkdtempSync3, readFileSync as readFileSync32, readdirSync as readdirSync14, writeFileSync as writeFileSync15 } from "node:fs";
16339
- import os7 from "node:os";
16340
- import path46 from "node:path";
16932
+ import { createHash as createHash9 } from "node:crypto";
16933
+ import { existsSync as existsSync38, mkdtempSync as mkdtempSync3, readFileSync as readFileSync35, readdirSync as readdirSync15, writeFileSync as writeFileSync17 } from "node:fs";
16934
+ import os8 from "node:os";
16935
+ import path48 from "node:path";
16341
16936
  import { fileURLToPath as fileURLToPath6 } from "node:url";
16342
16937
  import { z as z14 } from "zod";
16343
16938
  function sourceHash() {
16344
- const dir = path46.dirname(fileURLToPath6(import.meta.url));
16345
- const h = createHash8("sha256");
16346
- for (const f of readdirSync14(dir).filter((n) => n.endsWith(".ts")).sort()) {
16939
+ const dir = path48.dirname(fileURLToPath6(import.meta.url));
16940
+ const h = createHash9("sha256");
16941
+ for (const f of readdirSync15(dir).filter((n) => n.endsWith(".ts")).sort()) {
16347
16942
  h.update(f);
16348
- h.update(readFileSync32(path46.join(dir, f)));
16943
+ h.update(readFileSync35(path48.join(dir, f)));
16349
16944
  }
16350
16945
  return h.digest("hex").slice(0, 16);
16351
16946
  }
@@ -16353,10 +16948,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
16353
16948
  var init_server = __esm({
16354
16949
  "packages/mcp/src/server.ts"() {
16355
16950
  "use strict";
16356
- REPO_ROOT3 = path46.resolve(path46.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
16357
- CLI_BIN = path46.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
16358
- BUNDLED_CLI = path46.join(path46.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
16359
- CLI_SPAWN = existsSync37(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
16951
+ REPO_ROOT3 = path48.resolve(path48.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
16952
+ CLI_BIN = path48.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
16953
+ BUNDLED_CLI = path48.join(path48.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
16954
+ CLI_SPAWN = existsSync38(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
16360
16955
  str = (d) => z14.string().describe(d);
16361
16956
  optStr = (d) => z14.string().optional().describe(d);
16362
16957
  TOOLS = [
@@ -16387,13 +16982,13 @@ var init_server = __esm({
16387
16982
  const single = i["metadata"];
16388
16983
  const parts = i["metadataParts"];
16389
16984
  if (single !== void 0 || parts !== void 0) {
16390
- const tmp = path46.join(mkdtempSync3(path46.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
16985
+ const tmp = path48.join(mkdtempSync3(path48.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
16391
16986
  if (single !== void 0) {
16392
- writeFileSync15(tmp, single);
16987
+ writeFileSync17(tmp, single);
16393
16988
  argvOut.push("--metadata-raw-file", tmp);
16394
16989
  } else {
16395
16990
  if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
16396
- writeFileSync15(tmp, JSON.stringify(parts));
16991
+ writeFileSync17(tmp, JSON.stringify(parts));
16397
16992
  argvOut.push("--metadata-raw-parts-file", tmp);
16398
16993
  }
16399
16994
  }
@@ -16425,10 +17020,24 @@ var init_server = __esm({
16425
17020
  {
16426
17021
  name: "tendril_doctor",
16427
17022
  annotations: { readOnlyHint: true },
16428
- description: "Machine readiness + version status in one shot: installed vs latest version (with publish date and update remediation), browser identity, font-cache state, Figma desktop MCP reachability. Use to self-diagnose before recording/scoring, or whenever versions are in question. Exit 1 = something not ready; the report says exactly what and how to fix it.",
17023
+ description: "Machine readiness + version status in one shot: installed vs latest version (with publish date and update remediation), browser identity, font-cache state, Figma desktop MCP reachability, and whether this machine holds a LIVE portal session (checked against the portal itself; informational \u2014 publishing needs it, verification never does; when absent, offer tendril_login). Use to self-diagnose before recording/scoring, or whenever versions are in question. Exit 1 = something not ready; the report says exactly what and how to fix it.",
16429
17024
  schema: z14.object({}),
16430
17025
  argv: () => ["doctor"]
16431
17026
  },
17027
+ {
17028
+ name: "tendril_login",
17029
+ description: "Connect this machine to the user's Tendril portal account WITHOUT the user touching a terminal \u2014 phase one of the browser-approve sign-in. Starts the handshake, opens their browser to the approve page, and returns a verification link plus a short code. RELAY BOTH to the user verbatim: they click Approve on the page, and they must check the page shows EXACTLY this code (that check is the phishing defence \u2014 an attacker can send someone else's approve link). Then call tendril_login_wait to finish. Offer this whenever tendril_doctor reports no portal session and the user wants to publish or share; publishing needs it, verification never does.",
17030
+ schema: z14.object({
17031
+ portal: optStr("portal origin override for self-hosted portals (defaults to https://app.trytendril.com)")
17032
+ }),
17033
+ argv: (i) => ["login", "--device-start", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
17034
+ },
17035
+ {
17036
+ name: "tendril_login_wait",
17037
+ description: "Phase two of the browser-approve sign-in: waits (up to ten minutes, with live progress) for the user to click Approve on the page tendril_login opened, then stores the session on this machine. Call it right after relaying the link and code. A denial, a lapse, and success each come back as their own sentence; confirm the result to the user, and on success they are ready to publish.",
17038
+ schema: z14.object({}),
17039
+ argv: () => ["login", "--device-wait"]
17040
+ },
16432
17041
  {
16433
17042
  name: "tendril_record_next",
16434
17043
  annotations: { readOnlyHint: true },
@@ -16449,7 +17058,7 @@ var init_server = __esm({
16449
17058
  },
16450
17059
  {
16451
17060
  name: "tendril_record_ingest_rep",
16452
- description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
17061
+ description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot with contentsOnly=true and maxDimension=4096 \u2014 isolation keeps the editor's component-set chrome out of the padded export, where it would otherwise score as unpaintable reference ink), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
16453
17062
  schema: z14.object({
16454
17063
  setDir: str("recording set directory"),
16455
17064
  rep: str("planned rep slug"),
@@ -16470,14 +17079,14 @@ var init_server = __esm({
16470
17079
  const bridge = (label, single, parts) => {
16471
17080
  if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
16472
17081
  if (single === void 0 && parts === void 0) return;
16473
- const tmp = path46.join(mkdtempSync3(path46.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
17082
+ const tmp = path48.join(mkdtempSync3(path48.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
16474
17083
  if (single !== void 0) {
16475
- writeFileSync15(tmp, single);
17084
+ writeFileSync17(tmp, single);
16476
17085
  argvOut.push(`--${label}-file`, tmp);
16477
17086
  } else {
16478
17087
  const blocks = parts;
16479
17088
  if (blocks.length === 0) throw new Error(`\`${label}Parts\` must be a non-empty array of block texts`);
16480
- writeFileSync15(tmp, JSON.stringify(blocks));
17089
+ writeFileSync17(tmp, JSON.stringify(blocks));
16481
17090
  argvOut.push(`--${label}-parts-file`, tmp);
16482
17091
  }
16483
17092
  };
@@ -16518,12 +17127,12 @@ var init_server = __esm({
16518
17127
  const file = i["file"];
16519
17128
  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)");
16520
17129
  if (file !== void 0) return [...base, "--file", file];
16521
- const tmp = path46.join(mkdtempSync3(path46.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
17130
+ const tmp = path48.join(mkdtempSync3(path48.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
16522
17131
  if (text !== void 0) {
16523
- writeFileSync15(tmp, text);
17132
+ writeFileSync17(tmp, text);
16524
17133
  return [...base, "--file", tmp, "--raw"];
16525
17134
  }
16526
- writeFileSync15(tmp, JSON.stringify(texts));
17135
+ writeFileSync17(tmp, JSON.stringify(texts));
16527
17136
  return [...base, "--file", tmp, "--raw-parts"];
16528
17137
  }
16529
17138
  },
@@ -16680,13 +17289,13 @@ __export(permissions_exports, {
16680
17289
  runPermissions: () => runPermissions,
16681
17290
  writeSelection: () => writeSelection
16682
17291
  });
16683
- import { existsSync as existsSync38, mkdirSync as mkdirSync10, readFileSync as readFileSync33, writeFileSync as writeFileSync16 } from "node:fs";
16684
- import os8 from "node:os";
16685
- import path47 from "node:path";
17292
+ import { existsSync as existsSync39, mkdirSync as mkdirSync11, readFileSync as readFileSync36, writeFileSync as writeFileSync18 } from "node:fs";
17293
+ import os9 from "node:os";
17294
+ import path49 from "node:path";
16686
17295
  function mergeAllowlist(file, entries, denyEntries = []) {
16687
17296
  let settings = {};
16688
- if (existsSync38(file) && readFileSync33(file, "utf8").trim() !== "") {
16689
- settings = JSON.parse(readFileSync33(file, "utf8"));
17297
+ if (existsSync39(file) && readFileSync36(file, "utf8").trim() !== "") {
17298
+ settings = JSON.parse(readFileSync36(file, "utf8"));
16690
17299
  if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
16691
17300
  }
16692
17301
  const permissions = settings["permissions"] ??= {};
@@ -16706,8 +17315,8 @@ function mergeAllowlist(file, entries, denyEntries = []) {
16706
17315
  }
16707
17316
  if (added.length > 0 || denyAdded.length > 0) {
16708
17317
  allow.push(...added);
16709
- mkdirSync10(path47.dirname(file), { recursive: true });
16710
- writeFileSync16(file, `${JSON.stringify(settings, null, 2)}
17318
+ mkdirSync11(path49.dirname(file), { recursive: true });
17319
+ writeFileSync18(file, `${JSON.stringify(settings, null, 2)}
16711
17320
  `);
16712
17321
  }
16713
17322
  return { added, alreadyPresent, denyAdded };
@@ -16771,7 +17380,7 @@ async function runPermissions(flags) {
16771
17380
  }
16772
17381
  if (flags.write) {
16773
17382
  const base = process.env["INIT_CWD"] ?? process.cwd();
16774
- const file = flags.user ? path47.join(os8.homedir(), ".claude", "settings.json") : path47.join(base, ".claude", "settings.local.json");
17383
+ const file = flags.user ? path49.join(os9.homedir(), ".claude", "settings.json") : path49.join(base, ".claude", "settings.local.json");
16775
17384
  const { entries, denyEntries } = writeSelection(result, flags.user === true);
16776
17385
  if (flags.dryRun) {
16777
17386
  emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
@@ -16920,24 +17529,51 @@ __export(inspect_exports, {
16920
17529
  INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
16921
17530
  runInspect: () => runInspect
16922
17531
  });
16923
- import { existsSync as existsSync39, readFileSync as readFileSync34, writeFileSync as writeFileSync17 } from "node:fs";
16924
- import path48 from "node:path";
17532
+ import { existsSync as existsSync40, readFileSync as readFileSync37, writeFileSync as writeFileSync19 } from "node:fs";
17533
+ import path50 from "node:path";
17534
+ function readVerifyReport(evidenceDir) {
17535
+ const p = path50.join(evidenceDir, VERIFY_REPORT_FILENAME);
17536
+ if (!existsSync40(p)) return void 0;
17537
+ try {
17538
+ return JSON.parse(readFileSync37(p, "utf8"));
17539
+ } catch {
17540
+ return void 0;
17541
+ }
17542
+ }
17543
+ function verdictBlock(report) {
17544
+ if (report === void 0) {
17545
+ return `<div class="verdict none"><h2>No verdict on file</h2><p>This evidence carries no <code>${VERIFY_REPORT_FILENAME}</code>, so these images are unlabelled pixels \u2014 nothing here says what passed. Re-run <code>tendril verify</code> to write one.</p></div>`;
17546
+ }
17547
+ const buckets = /* @__PURE__ */ new Map();
17548
+ for (const c of report.configs) buckets.set(c.status, (buckets.get(c.status) ?? 0) + 1);
17549
+ const split = [...buckets.entries()].map(([k, n]) => `<span class="tier t-${esc(k)}">${n} ${esc(k)}</span>`).join("");
17550
+ const failed = report.behaviors.filter((b) => !b.pass);
17551
+ return `<div class="verdict ${report.verdict === "verified" ? "ok" : "bad"}"><h2>${esc(report.verdict)} <small>at bar \u201C${esc(report.targetBar)}\u201D</small></h2><p class="tiers">${split}<span class="tier">${report.behaviors.length - failed.length}/${report.behaviors.length} behaviors</span></p>` + (failed.length > 0 ? `<ul class="fails">${failed.map((b) => `<li><code>${esc(b.id)}</code>${b.detail !== void 0 ? ` \u2014 ${esc(b.detail)}` : ""}</li>`).join("")}</ul>` : "") + (report.verdictCaveats.length > 0 ? `<h3>What was never asked</h3><ul class="caveats">${report.verdictCaveats.map((c) => `<li>${esc(c)}</li>`).join("")}</ul>` : `<p class="caveats">No caveats: every question this bar asks was answered.</p>`) + `</div>`;
17552
+ }
17553
+ function scoreLine(report, rep) {
17554
+ const c = report?.configs.find((x) => x.rep === rep);
17555
+ if (c === void 0) return "";
17556
+ const ex = c.exact !== void 0 ? ` <span class="exact">measured ${c.exact.similarity.toFixed(6)} / ${c.exact.inkRecall.toFixed(6)}</span>` : "";
17557
+ const why = Array.isArray(c.demotedBy) && c.demotedBy.length > 0 ? `<span class="demoted">demoted: ${esc(c.demotedBy.join("; "))}</span>` : "";
17558
+ const err = typeof c.error === "string" ? `<span class="demoted">${esc(c.error)}</span>` : "";
17559
+ return `<p class="scores"><span class="tier t-${esc(c.status)}">${esc(c.status)}</span> sim ${c.similarity} \xB7 ink ${c.inkRecall}${ex} ${why} ${err}</p>`;
17560
+ }
16925
17561
  async function runInspect(opts) {
16926
17562
  if (opts.describe) {
16927
17563
  printDescription(INSPECT_DESCRIPTION);
16928
17564
  return;
16929
17565
  }
16930
- const bundleDir = path48.resolve(opts.bundleDir);
16931
- const evidenceDir = path48.join(bundleDir, "verify-evidence");
16932
- const manifestPath2 = path48.join(bundleDir, "component.json");
16933
- if (!existsSync39(evidenceDir) || !existsSync39(manifestPath2)) {
17566
+ const bundleDir = path50.resolve(opts.bundleDir);
17567
+ const evidenceDir = path50.join(bundleDir, "verify-evidence");
17568
+ const manifestPath2 = path50.join(bundleDir, "component.json");
17569
+ if (!existsSync40(evidenceDir) || !existsSync40(manifestPath2)) {
16934
17570
  fail(opts, ExitCode.InputValidation, {
16935
- error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync39(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
17571
+ error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync40(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
16936
17572
  code: "no-evidence",
16937
17573
  remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
16938
17574
  });
16939
17575
  }
16940
- const { manifest } = readBundleManifest(readFileSync34(manifestPath2, "utf8"));
17576
+ const { manifest } = readBundleManifest(readFileSync37(manifestPath2, "utf8"));
16941
17577
  if (manifest === void 0) {
16942
17578
  fail(opts, ExitCode.InputValidation, {
16943
17579
  error: "component.json did not parse as a bundle manifest",
@@ -16945,8 +17581,9 @@ async function runInspect(opts) {
16945
17581
  remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
16946
17582
  });
16947
17583
  }
16948
- const setDir = path48.resolve(opts.set ?? manifest.provenance.recordingSet.path);
16949
- const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync39(path48.join(evidenceDir, `${rep}-ref.png`)) && existsSync39(path48.join(evidenceDir, `${rep}-render.png`)));
17584
+ const setDir = path50.resolve(opts.set ?? manifest.provenance.recordingSet.path);
17585
+ const report = readVerifyReport(evidenceDir);
17586
+ const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync40(path50.join(evidenceDir, `${rep}-ref.png`)) && existsSync40(path50.join(evidenceDir, `${rep}-render.png`)));
16950
17587
  if (reps.length === 0) {
16951
17588
  fail(opts, ExitCode.InputValidation, {
16952
17589
  error: "verify-evidence holds no ref/render pairs for this bundle's configs",
@@ -16957,15 +17594,15 @@ async function runInspect(opts) {
16957
17594
  let crops = 0;
16958
17595
  const sections = [];
16959
17596
  for (const rep of reps) {
16960
- const ref = new Uint8Array(readFileSync34(path48.join(evidenceDir, `${rep}-ref.png`)));
16961
- const render = new Uint8Array(readFileSync34(path48.join(evidenceDir, `${rep}-render.png`)));
17597
+ const ref = new Uint8Array(readFileSync37(path50.join(evidenceDir, `${rep}-ref.png`)));
17598
+ const render = new Uint8Array(readFileSync37(path50.join(evidenceDir, `${rep}-render.png`)));
16962
17599
  const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
16963
17600
  const cells = [];
16964
17601
  for (const [i, n] of nodes.entries()) {
16965
17602
  const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
16966
17603
  try {
16967
- writeFileSync17(path48.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
16968
- writeFileSync17(path48.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
17604
+ writeFileSync19(path50.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
17605
+ writeFileSync19(path50.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
16969
17606
  } catch {
16970
17607
  continue;
16971
17608
  }
@@ -16975,11 +17612,15 @@ async function runInspect(opts) {
16975
17612
  );
16976
17613
  }
16977
17614
  sections.push(
16978
- `<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>`
17615
+ `<section><h2>${esc(rep)}</h2>` + scoreLine(report, rep) + `<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>`
16979
17616
  );
16980
17617
  }
16981
- const sheet = path48.join(evidenceDir, "inspect.html");
16982
- writeFileSync17(
17618
+ for (const c of report?.configs ?? []) {
17619
+ if (reps.includes(c.rep)) continue;
17620
+ sections.push(`<section class="missing"><h2>${esc(c.rep)}</h2>${scoreLine(report, c.rep)}<p class="none">No evidence images for this config \u2014 it was scored, but nothing was captured to look at.</p></section>`);
17621
+ }
17622
+ const sheet = path50.join(evidenceDir, "inspect.html");
17623
+ writeFileSync19(
16983
17624
  sheet,
16984
17625
  `<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
16985
17626
  body{font:14px/1.45 system-ui,sans-serif;margin:24px;background:#fff;color:#111}
@@ -16988,11 +17629,23 @@ h1{font-size:20px} h2{font-size:16px;border-top:1px solid #ddd;padding-top:16px}
16988
17629
  .full img{max-width:400px;border:1px solid #ccc} .pair img{border:1px solid #ccc;image-rendering:pixelated}
16989
17630
  figure{margin:0 0 16px} figcaption{margin-bottom:4px} em{display:block;color:#666;font-style:normal;font-size:12px}
16990
17631
  .grid{display:flex;flex-wrap:wrap;gap:20px;margin-top:12px} .none{color:#666}
16991
- </style><h1>${esc(manifest.name)} \u2014 detail sheet (small recorded nodes, recorded vs rendered)</h1>
16992
- <p>Every crop is a recorded node small enough that global metrics weight it as a rounding error.
17632
+ .verdict{border:1px solid #ddd;border-left-width:5px;padding:12px 16px;margin:16px 0;background:#fafafa}
17633
+ .verdict.ok{border-left-color:#1a7f37} .verdict.bad{border-left-color:#b35900} .verdict.none{border-left-color:#999}
17634
+ .verdict h2{border:0;padding:0;margin:0 0 8px} .verdict h3{font-size:13px;margin:12px 0 4px;color:#444}
17635
+ .verdict small{font-weight:400;color:#666}
17636
+ .tiers{margin:0;display:flex;gap:8px;flex-wrap:wrap}
17637
+ .tier{display:inline-block;padding:1px 8px;border:1px solid #ccc;border-radius:10px;font-size:12px;background:#fff}
17638
+ .t-certified{border-color:#1a7f37;color:#1a7f37} .t-pass{border-color:#8a6d00;color:#8a6d00} .t-fail{border-color:#b3261e;color:#b3261e}
17639
+ .caveats,.fails{margin:4px 0 0;padding-left:20px;color:#444} .caveats li,.fails li{margin:2px 0}
17640
+ .scores{margin:4px 0 10px;color:#333} .exact{color:#666;font-size:12px} .demoted{color:#b3261e;font-size:12px;margin-left:8px}
17641
+ .missing{opacity:.85} .legend{white-space:pre-wrap;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;background:#fafafa;border:1px solid #ddd;padding:12px;overflow-x:auto}
17642
+ </style><h1>${esc(manifest.name)} \u2014 verdict and evidence</h1>
17643
+ ${verdictBlock(report)}
17644
+ <p>Below, every crop is a recorded node small enough that global metrics weight it as a rounding error.
16993
17645
  Scan the pairs: anything present on the left and missing/invisible on the right is a defect,
16994
17646
  whatever the scores said. Verdicts come from <code>tendril verify</code> \u2014 this sheet only shows.</p>
16995
17647
  ${sections.join("\n")}
17648
+ <section><h2>Diff colours</h2><div class="legend">${esc(DIFF_LEGEND_TEXT)}</div></section>
16996
17649
  `
16997
17650
  );
16998
17651
  emitData(opts, { sheet, configs: reps.length, crops }, () => {
@@ -17032,6 +17685,694 @@ var init_inspect = __esm({
17032
17685
  }
17033
17686
  });
17034
17687
 
17688
+ // packages/cli/src/commands/login.ts
17689
+ var login_exports = {};
17690
+ __export(login_exports, {
17691
+ DEFAULT_PORTAL_ORIGIN: () => DEFAULT_PORTAL_ORIGIN,
17692
+ runLogin: () => runLogin,
17693
+ runLogout: () => runLogout
17694
+ });
17695
+ import { spawn } from "node:child_process";
17696
+ import { existsSync as existsSync41, mkdirSync as mkdirSync12, readFileSync as readFileSync38, rmSync as rmSync6, writeFileSync as writeFileSync20 } from "node:fs";
17697
+ import path51 from "node:path";
17698
+ import { isCancel as isCancel3, password as password2 } from "@clack/prompts";
17699
+ async function runLogin(opts, deps) {
17700
+ const origin = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? DEFAULT_PORTAL_ORIGIN).replace(/\/+$/, "");
17701
+ if (!isSecureOrigin(origin)) {
17702
+ fail(opts, ExitCode.InputValidation, {
17703
+ error: `${origin} is not https, so a session token sent there would travel in the clear`,
17704
+ code: "portal-not-https",
17705
+ remediation: "Use the https:// address of your portal. Only 127.0.0.1 and localhost are exempt, for local development."
17706
+ });
17707
+ }
17708
+ if (opts.deviceStart === true) {
17709
+ await deviceStartPhase(opts, origin, deps ?? realDeps());
17710
+ return;
17711
+ }
17712
+ if (opts.deviceWait === true) {
17713
+ await deviceWaitPhase(opts, deps ?? realDeps());
17714
+ return;
17715
+ }
17716
+ if (process.stdin.isTTY === true && opts.paste !== true) {
17717
+ await deviceLogin(opts, origin, deps ?? realDeps());
17718
+ return;
17719
+ }
17720
+ await pasteLogin(opts, origin);
17721
+ }
17722
+ async function deviceLogin(opts, origin, deps) {
17723
+ const started = await startHandshake(opts, origin, deps);
17724
+ process.stderr.write(`
17725
+ Open this link to approve the sign-in:
17726
+
17727
+ ${started.verificationUrl}
17728
+
17729
+ `);
17730
+ process.stderr.write(` The page must show this code: ${started.userCode}
17731
+ `);
17732
+ process.stderr.write(` Approve it only if this terminal is yours.
17733
+
17734
+ `);
17735
+ deps.openBrowser(started.verificationUrl);
17736
+ process.stderr.write(` Waiting for the browser (about ten minutes before this code lapses) `);
17737
+ const outcome = await waitForDecision(opts, origin, deps, started, () => process.stderr.write("."));
17738
+ process.stderr.write("\n");
17739
+ settleDecision(opts, origin, outcome);
17740
+ }
17741
+ async function waitForDecision(opts, origin, deps, started, onTick) {
17742
+ const interval = Math.max(1, started.intervalSeconds);
17743
+ const attempts = Math.ceil(660 / interval);
17744
+ for (let n = 0; n < attempts; n += 1) {
17745
+ await deps.sleep(interval);
17746
+ const state = await pollHandshake(opts, origin, deps, started.deviceCode);
17747
+ if (state.status === "pending") {
17748
+ onTick();
17749
+ continue;
17750
+ }
17751
+ if (state.status === "approved") return state;
17752
+ return { status: state.status };
17753
+ }
17754
+ return { status: "gave-up" };
17755
+ }
17756
+ function settleDecision(opts, origin, outcome) {
17757
+ switch (outcome.status) {
17758
+ case "approved":
17759
+ writeStoredSession({ origin, token: outcome.token });
17760
+ emitData(opts, { origin, storedAt: sessionPath(), via: "device" }, () => {
17761
+ process.stdout.write(`signed in to ${origin}
17762
+ `);
17763
+ process.stdout.write(` token stored at ${sessionPath()} (readable only by you)
17764
+ `);
17765
+ });
17766
+ return;
17767
+ case "denied":
17768
+ fail(opts, ExitCode.General, {
17769
+ error: "the sign-in was denied in the browser",
17770
+ code: "login-denied",
17771
+ remediation: `If that was not you saying no, run ${tendrilCommand("login")} again and approve the fresh code.`
17772
+ });
17773
+ break;
17774
+ case "expired":
17775
+ case "gave-up":
17776
+ fail(opts, ExitCode.General, {
17777
+ error: outcome.status === "expired" ? "the sign-in code lapsed before anyone approved it \u2014 they live ten minutes" : "gave up waiting for the browser approval",
17778
+ code: "login-expired",
17779
+ remediation: `Run ${tendrilCommand("login")} again for a fresh code.`
17780
+ });
17781
+ break;
17782
+ default:
17783
+ fail(opts, ExitCode.General, {
17784
+ error: "the portal no longer recognises this sign-in attempt",
17785
+ code: "login-unknown",
17786
+ remediation: `Run ${tendrilCommand("login")} again. If this repeats, the approval may be racing another terminal \u2014 approve only one at a time.`
17787
+ });
17788
+ }
17789
+ }
17790
+ function pendingLoginPath() {
17791
+ return path51.join(path51.dirname(sessionPath()), "pending-login.json");
17792
+ }
17793
+ async function deviceStartPhase(opts, origin, deps) {
17794
+ const started = await startHandshake(opts, origin, deps);
17795
+ const file = pendingLoginPath();
17796
+ mkdirSync12(path51.dirname(file), { recursive: true });
17797
+ writeFileSync20(file, `${JSON.stringify({ origin, ...started }, null, 2)}
17798
+ `, { mode: 384 });
17799
+ deps.openBrowser(started.verificationUrl);
17800
+ emitData(
17801
+ opts,
17802
+ {
17803
+ verificationUrl: started.verificationUrl,
17804
+ userCode: started.userCode,
17805
+ expiresAt: started.expiresAt,
17806
+ origin,
17807
+ next: "Show the user the link and the code \u2014 they approve in the browser, and must check the page shows this exact code. Then run login --device-wait (the tendril_login_wait tool) to finish."
17808
+ },
17809
+ () => {
17810
+ process.stdout.write(`open ${started.verificationUrl}
17811
+ `);
17812
+ process.stdout.write(` the page must show: ${started.userCode}
17813
+ `);
17814
+ process.stdout.write(` then: ${tendrilCommand("login --device-wait")}
17815
+ `);
17816
+ }
17817
+ );
17818
+ }
17819
+ async function deviceWaitPhase(opts, deps) {
17820
+ const file = pendingLoginPath();
17821
+ let pending;
17822
+ if (existsSync41(file)) {
17823
+ try {
17824
+ const parsed = JSON.parse(readFileSync38(file, "utf8"));
17825
+ if (typeof parsed.origin === "string" && typeof parsed.deviceCode === "string" && typeof parsed.intervalSeconds === "number") {
17826
+ pending = parsed;
17827
+ }
17828
+ } catch {
17829
+ }
17830
+ }
17831
+ if (pending === void 0) {
17832
+ fail(opts, ExitCode.InputValidation, {
17833
+ error: "there is no sign-in waiting to finish",
17834
+ code: "no-pending-login",
17835
+ remediation: `Start one first: ${tendrilCommand("login --device-start")} (the tendril_login tool).`
17836
+ });
17837
+ }
17838
+ const done = () => rmSync6(file, { force: true });
17839
+ const total = Math.ceil(660 / Math.max(1, pending.intervalSeconds));
17840
+ let ticks = 0;
17841
+ const outcome = await waitForDecision(opts, pending.origin, deps, pending, () => {
17842
+ ticks += 1;
17843
+ emitProgress(ticks, total, "waiting for the browser approval");
17844
+ });
17845
+ done();
17846
+ settleDecision(opts, pending.origin, outcome);
17847
+ }
17848
+ async function startHandshake(opts, origin, deps) {
17849
+ let response;
17850
+ try {
17851
+ response = await deps.fetch(`${origin}/api/device`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
17852
+ } catch (error) {
17853
+ return unreachable(opts, origin, error.message);
17854
+ }
17855
+ if (!response.ok) return unreachable(opts, origin, `the portal answered ${String(response.status)}`);
17856
+ const body = await response.json();
17857
+ if (typeof body.deviceCode !== "string" || typeof body.userCode !== "string" || typeof body.verificationUrl !== "string" || typeof body.intervalSeconds !== "number" || typeof body.expiresAt !== "string") {
17858
+ return unreachable(opts, origin, "the portal's answer was not a device handshake");
17859
+ }
17860
+ return body;
17861
+ }
17862
+ async function pollHandshake(opts, origin, deps, deviceCode) {
17863
+ let response;
17864
+ try {
17865
+ response = await deps.fetch(`${origin}/api/device/token`, {
17866
+ method: "POST",
17867
+ headers: { "content-type": "application/json" },
17868
+ body: JSON.stringify({ deviceCode })
17869
+ });
17870
+ } catch {
17871
+ return { status: "pending" };
17872
+ }
17873
+ if (!response.ok) return { status: "pending" };
17874
+ const body = await response.json();
17875
+ if (body.status === "approved" && typeof body.token === "string") return { status: "approved", token: body.token };
17876
+ if (body.status === "denied" || body.status === "expired" || body.status === "unknown") return { status: body.status };
17877
+ return { status: "pending" };
17878
+ }
17879
+ function unreachable(opts, origin, detail) {
17880
+ fail(opts, ExitCode.General, {
17881
+ error: `could not start a sign-in with ${origin}: ${detail}`,
17882
+ code: "portal-unreachable",
17883
+ remediation: "Check your connection and try again. If your portal is self-hosted, confirm the --to URL."
17884
+ });
17885
+ }
17886
+ function realDeps() {
17887
+ return {
17888
+ fetch: globalThis.fetch.bind(globalThis),
17889
+ sleep: (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1e3)),
17890
+ openBrowser: (url) => {
17891
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
17892
+ try {
17893
+ spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
17894
+ } catch {
17895
+ }
17896
+ }
17897
+ };
17898
+ }
17899
+ async function pasteLogin(opts, origin) {
17900
+ const token = await readToken(opts);
17901
+ if (token === void 0 || token.trim() === "") {
17902
+ fail(opts, ExitCode.InputValidation, {
17903
+ error: "no token was given",
17904
+ code: "no-token",
17905
+ remediation: `Paste the token when prompted, or pipe it: \`echo "<token>" | ${tendrilCommand(`login --to ${origin}`)}\`.`
17906
+ });
17907
+ }
17908
+ const trimmed = token.trim();
17909
+ if (!/^[A-Za-z0-9_-]{16,512}$/.test(trimmed)) {
17910
+ fail(opts, ExitCode.InputValidation, {
17911
+ error: "that does not look like a Tendril session token",
17912
+ code: "malformed-token",
17913
+ remediation: "Tokens are a single line of letters, digits, hyphens and underscores. Check for a stray space or a truncated paste."
17914
+ });
17915
+ }
17916
+ writeStoredSession({ origin, token: trimmed });
17917
+ emitData(opts, { origin, storedAt: sessionPath(), via: "paste" }, () => {
17918
+ process.stdout.write(`signed in to ${origin}
17919
+ `);
17920
+ process.stdout.write(` token stored at ${sessionPath()} (readable only by you)
17921
+ `);
17922
+ });
17923
+ }
17924
+ async function readToken(opts) {
17925
+ if (process.stdin.isTTY === true) {
17926
+ const entered = await password2({ message: "Paste your Tendril portal token (it will not be shown)" });
17927
+ if (isCancel3(entered)) {
17928
+ fail(opts, ExitCode.General, { error: "cancelled", code: "cancelled", remediation: "Run it again when you have the token." });
17929
+ }
17930
+ return entered;
17931
+ }
17932
+ const chunks = [];
17933
+ for await (const chunk of process.stdin) chunks.push(chunk);
17934
+ return Buffer.concat(chunks).toString("utf8").split("\n")[0];
17935
+ }
17936
+ function runLogout(opts) {
17937
+ const stored = readStoredSession();
17938
+ if (stored === void 0) {
17939
+ emitData(opts, { signedOut: true, wasSignedIn: false }, () => process.stdout.write("you were not signed in\n"));
17940
+ return;
17941
+ }
17942
+ clearStoredSession();
17943
+ emitData(opts, { signedOut: true, wasSignedIn: true, origin: stored.origin }, () => {
17944
+ process.stdout.write(`this machine has forgotten its session for ${stored.origin}
17945
+ `);
17946
+ process.stdout.write(" the session itself is still valid \u2014 sign out everywhere from the portal to end it\n");
17947
+ });
17948
+ }
17949
+ var DEFAULT_PORTAL_ORIGIN;
17950
+ var init_login = __esm({
17951
+ "packages/cli/src/commands/login.ts"() {
17952
+ "use strict";
17953
+ init_src3();
17954
+ init_invocation();
17955
+ init_output();
17956
+ init_publish_client();
17957
+ DEFAULT_PORTAL_ORIGIN = "https://app.trytendril.com";
17958
+ }
17959
+ });
17960
+
17961
+ // packages/cli/src/commands/share.ts
17962
+ var share_exports = {};
17963
+ __export(share_exports, {
17964
+ runShare: () => runShare
17965
+ });
17966
+ async function runShare(opts) {
17967
+ const origin = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "");
17968
+ if (origin === "") {
17969
+ fail(opts, ExitCode.InputValidation, {
17970
+ error: "no portal to share from",
17971
+ code: "no-portal-configured",
17972
+ remediation: `Pass \`--to <url>\` or set TENDRIL_PORTAL_URL.`
17973
+ });
17974
+ }
17975
+ if (!isSecureOrigin(origin)) {
17976
+ fail(opts, ExitCode.InputValidation, {
17977
+ error: `${origin} is not https, so a session token sent there would travel in the clear`,
17978
+ code: "portal-not-https",
17979
+ remediation: "Use the https:// address of your portal. Only 127.0.0.1 and localhost are exempt, for local development."
17980
+ });
17981
+ }
17982
+ const found = tokenFor(origin);
17983
+ if (!found.ok) {
17984
+ fail(opts, ExitCode.Auth, {
17985
+ error: found.reason === "origin-mismatch" ? `the session available here belongs to ${found.boundTo}, not ${origin}` : `no session for ${origin}`,
17986
+ code: found.reason === "origin-mismatch" ? "session-belongs-to-another-portal" : "not-signed-in",
17987
+ remediation: `Run \`${tendrilCommand(`login --to ${origin}`)}\`.`
17988
+ });
17989
+ }
17990
+ const client = new HttpPublishClient({ origin, token: found.token });
17991
+ if (opts.list === true) {
17992
+ const listed = await client.listShareLinks({ publicationId: opts.publicationId });
17993
+ if (!listed.ok) refuse(opts, listed);
17994
+ emitData(opts, listed.value, () => {
17995
+ if (listed.value.links.length === 0) {
17996
+ process.stdout.write("no links have been issued for this publication\n");
17997
+ return;
17998
+ }
17999
+ process.stdout.write(`${String(listed.value.links.length)} link(s) for this publication:
18000
+ `);
18001
+ for (const link of listed.value.links) {
18002
+ const ended = link.revoked_at !== null ? ` REVOKED ${link.revoked_at}` : link.expires_at !== null ? ` expires ${link.expires_at}` : " open-ended";
18003
+ process.stdout.write(` ${link.id}${ended}${link.recipient_email === null ? "" : ` \u2014 for ${link.recipient_email}`}
18004
+ `);
18005
+ }
18006
+ process.stdout.write(" (the links themselves are not stored \u2014 only their digests, so a lost link is a new link)\n");
18007
+ });
18008
+ return;
18009
+ }
18010
+ if (opts.revoke !== void 0) {
18011
+ const revoked = await client.revokeShareLink({ publicationId: opts.publicationId, shareLinkId: opts.revoke });
18012
+ if (!revoked.ok) refuse(opts, revoked);
18013
+ emitData(opts, revoked.value, () => {
18014
+ process.stdout.write(`revoked ${opts.revoke ?? ""}
18015
+ `);
18016
+ process.stdout.write(" anyone holding that link now gets told it was revoked, not that it never existed\n");
18017
+ });
18018
+ return;
18019
+ }
18020
+ const expiresAt = resolveExpiry(opts);
18021
+ const issued = await client.issueShareLink({
18022
+ publicationId: opts.publicationId,
18023
+ expiresAt,
18024
+ recipientEmail: opts.recipient ?? null
18025
+ });
18026
+ if (!issued.ok) refuse(opts, issued);
18027
+ emitData(opts, issued.value, () => {
18028
+ process.stdout.write(`${issued.value.url}
18029
+
18030
+ `);
18031
+ process.stdout.write(" Anyone with this link can read this one publication and its evidence.\n");
18032
+ process.stdout.write(" It is not tied to a person and needs no account.\n");
18033
+ process.stdout.write(
18034
+ issued.value.expiresAt === null ? " It does not expire. Revoke it with --revoke when you are done.\n" : ` It stops working at ${issued.value.expiresAt}.
18035
+ `
18036
+ );
18037
+ process.stdout.write(" This is the only time the link is shown \u2014 only its digest is stored.\n");
18038
+ });
18039
+ }
18040
+ function resolveExpiry(opts) {
18041
+ if (opts.expires === void 0) return null;
18042
+ const days = Number(opts.expires);
18043
+ if (Number.isInteger(days) && days > 0) {
18044
+ return new Date(Date.now() + days * 24 * 60 * 60 * 1e3).toISOString().replace(/\.\d{3}Z$/, (m) => m);
18045
+ }
18046
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(opts.expires)) return opts.expires;
18047
+ fail(opts, ExitCode.InputValidation, {
18048
+ error: `--expires takes a number of days (e.g. 7) or an ISO-8601 UTC timestamp with milliseconds \u2014 got ${JSON.stringify(opts.expires)}`,
18049
+ code: "bad-expiry",
18050
+ remediation: "Try `--expires 7` for a week, or omit it for a link that does not expire."
18051
+ });
18052
+ }
18053
+ function refuse(opts, sent) {
18054
+ fail(opts, sent.status === 401 ? ExitCode.Auth : ExitCode.General, {
18055
+ error: sent.refusal,
18056
+ code: "share-refused",
18057
+ remediation: sent.status === 404 ? "Check the publication id \u2014 only its owner can share it." : `Fix what is named above and run \`${tendrilCommand(`share ${opts.publicationId}`)}\` again.`
18058
+ });
18059
+ }
18060
+ var init_share = __esm({
18061
+ "packages/cli/src/commands/share.ts"() {
18062
+ "use strict";
18063
+ init_src3();
18064
+ init_invocation();
18065
+ init_output();
18066
+ init_publish_client();
18067
+ }
18068
+ });
18069
+
18070
+ // packages/cli/src/commands/publish.ts
18071
+ var publish_exports = {};
18072
+ __export(publish_exports, {
18073
+ runPublish: () => runPublish
18074
+ });
18075
+ import { existsSync as existsSync42, readFileSync as readFileSync39 } from "node:fs";
18076
+ import path52 from "node:path";
18077
+ async function runPublish(opts) {
18078
+ const bundleDir = path52.resolve(opts.bundleDir);
18079
+ const bundle = readBundle(opts, bundleDir);
18080
+ const report = bundle.report;
18081
+ if (typeof report["rulerExit"] !== "number" || !Number.isInteger(report["rulerExit"])) {
18082
+ fail(opts, ExitCode.InputValidation, {
18083
+ error: `${VERIFY_REPORT_FILENAME} does not state the ruler's exit code, and an absent exit code is not a passing one`,
18084
+ code: "report-has-no-exit-code",
18085
+ remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` with a current CLI \u2014 this report predates the field.`
18086
+ });
18087
+ }
18088
+ if (report["rulerExit"] !== 0) {
18089
+ const why = typeof report["rulerRefusal"] === "string" ? ` \u2014 ${report["rulerRefusal"]}` : "";
18090
+ fail(opts, ExitCode.VerificationFailed, {
18091
+ error: `the ruler refused this run (exit ${String(report["rulerExit"])}), so it has nothing to publish${why}`,
18092
+ code: "run-refused-by-ruler",
18093
+ remediation: `Fix what the report names, re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` until it passes, then publish. A declined verdict never becomes a page.`
18094
+ });
18095
+ }
18096
+ const undisclosed = undisclosedTrustFacts(report);
18097
+ if (undisclosed.length > 0) {
18098
+ fail(opts, ExitCode.InputValidation, {
18099
+ error: `this report carries a disclosure a published page cannot yet show, and a verdict shown without it is worse than no page: ${undisclosed.map((f) => `${f.pointer} \u2014 ${f.consequence}`).join("; ")}`,
18100
+ code: "report-carries-an-unrenderable-disclosure",
18101
+ remediation: "Resolve what the disclosure names \u2014 re-record so the set matches the bundle's stamp, or resolve the substituted font families \u2014 then re-verify and publish the clean run."
18102
+ });
18103
+ }
18104
+ if (scoredRecordingSetHash(report) === void 0) {
18105
+ fail(opts, ExitCode.InputValidation, {
18106
+ error: "this report does not name the recording set these scores were measured against",
18107
+ code: "report-names-no-recording-set",
18108
+ remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` with a current CLI \u2014 the set identity rides the report.`
18109
+ });
18110
+ }
18111
+ const componentName = (opts.name ?? bundle.manifest.name).trim();
18112
+ const surface = classifyBundleSurface(bundle.files, { entry: bundle.manifest.entry });
18113
+ if (surface.unknown.length > 0) {
18114
+ fail(opts, ExitCode.InputValidation, {
18115
+ error: `this bundle carries files a portal does not recognise, and publishing guesses at nothing: ${surface.unknown.join(", ")}`,
18116
+ code: "bundle-carries-unrecognised-files",
18117
+ remediation: `Remove them from ${opts.bundleDir}, or re-emit the bundle. Publishing them would ship something nobody reviewed; dropping them silently would ship evidence with a hole in it.`
18118
+ });
18119
+ }
18120
+ const sheetEntry = surface.published.find((p) => p.role === "inspect-sheet");
18121
+ if (sheetEntry !== void 0) {
18122
+ const missingCrops = missingInspectCrops(
18123
+ readFileSync39(path52.join(bundleDir, sheetEntry.path), "utf8"),
18124
+ surface.published.map((p) => p.path)
18125
+ );
18126
+ if (missingCrops.length > 0) {
18127
+ fail(opts, ExitCode.InputValidation, {
18128
+ error: `this bundle's inspect sheet references images it does not carry, and half a sheet reads as a broken component: ${missingCrops.join(", ")}`,
18129
+ code: "inspect-sheet-without-its-crops",
18130
+ remediation: `Re-run \`${tendrilCommand(`inspect ${opts.bundleDir}`)}\` to rebuild the sheet and its crops together, or delete the sheet and publish without it.`
18131
+ });
18132
+ }
18133
+ }
18134
+ if (surface.missingRequired.length > 0) {
18135
+ fail(opts, ExitCode.InputValidation, {
18136
+ error: `this bundle is missing what a publishable bundle cannot be without: ${surface.missingRequired.join(", ")}`,
18137
+ code: "bundle-missing-required-role",
18138
+ remediation: `Run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` \u2014 the evidence a page shows is written by that run. Required: ${REQUIRED_ROLES.join(", ")}.`
18139
+ });
18140
+ }
18141
+ const recorded = readScoredFiles(report);
18142
+ if (recorded === void 0) {
18143
+ fail(opts, ExitCode.InputValidation, {
18144
+ error: `${VERIFY_REPORT_FILENAME} does not record the files this run was scored beside, so nothing ties this bundle's bytes to its verdict`,
18145
+ code: "report-records-no-file-digests",
18146
+ remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` with a current CLI \u2014 this report predates the field.`
18147
+ });
18148
+ } else {
18149
+ const delta = compareScoredFiles(recorded, digestScoredFiles(bundleDir, bundle.manifest.entry));
18150
+ const disagreements = [
18151
+ ...delta.changed.map((p) => `${p} (bytes differ from the scored run)`),
18152
+ ...delta.missing.map((p) => `${p} (scored, no longer in the bundle)`),
18153
+ ...delta.unscored.map((p) => `${p} (in the bundle, never scored)`)
18154
+ ];
18155
+ if (disagreements.length > 0) {
18156
+ fail(opts, ExitCode.InputValidation, {
18157
+ error: `this bundle is not the one the ruler scored: ${disagreements.join("; ")}`,
18158
+ code: "bundle-disagrees-with-scored-files",
18159
+ remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` so the verdict describes these bytes. A page must never show code the ruler never saw.`
18160
+ });
18161
+ }
18162
+ }
18163
+ const figmaFile = bundle.manifest.provenance.recordingSet.figmaFile ?? null;
18164
+ if (opts.dryRun) {
18165
+ emitData(
18166
+ opts,
18167
+ {
18168
+ bundleDir,
18169
+ componentName,
18170
+ figmaFile,
18171
+ entry: bundle.manifest.entry,
18172
+ publishes: surface.published,
18173
+ excluded: surface.excluded,
18174
+ rulerVersion: report["environment"]?.["ruler"] ?? null,
18175
+ wouldPublish: true
18176
+ },
18177
+ () => {
18178
+ process.stdout.write(`${componentName} would publish ${String(surface.published.length)} files:
18179
+ `);
18180
+ for (const entry of surface.published) process.stdout.write(` ${entry.path} (${entry.role})
18181
+ `);
18182
+ for (const left of surface.excluded) process.stdout.write(` \u2014 leaving ${left.path} behind (${left.reason})
18183
+ `);
18184
+ }
18185
+ );
18186
+ return;
18187
+ }
18188
+ const origin = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "");
18189
+ const client = opts.client ?? httpClient(opts, origin);
18190
+ if (opts.confirmPublish === true && process.stdin.isTTY !== true) {
18191
+ fail(opts, ExitCode.ConfirmationRequired, {
18192
+ error: "--confirm-publish requires an interactive terminal \u2014 putting a component on the internet is a human-only decision",
18193
+ code: "publish-confirmation-not-interactive",
18194
+ remediation: `A human runs \`${tendrilCommand(`publish ${opts.bundleDir} --confirm-publish`)}\` in their own terminal. Agents: show this to your operator instead of confirming it.`
18195
+ });
18196
+ }
18197
+ if (opts.acceptTerms === true) {
18198
+ const accepted = await client.acceptTerms({ figmaFile });
18199
+ if (!accepted.ok) refuse2(opts, accepted, "accept-terms-refused");
18200
+ }
18201
+ const opened = await client.begin({
18202
+ componentName,
18203
+ figmaFile,
18204
+ entry: bundle.manifest.entry,
18205
+ files: bundle.files,
18206
+ report: bundle.reportText,
18207
+ confirmed: opts.confirmPublish === true
18208
+ });
18209
+ if (!opened.ok) {
18210
+ if (opened.needsConsent !== void 0) {
18211
+ fail(opts, ExitCode.ConfirmationRequired, {
18212
+ error: `publishing this design system needs an acceptance of the publishing terms (version ${opened.needsConsent.termsVersion}) first`,
18213
+ code: "publishing-terms-not-accepted",
18214
+ remediation: `Read the terms, then run \`${tendrilCommand(`publish ${opts.bundleDir} --accept-terms --confirm-publish`)}\`. The acceptance covers this design system only.`
18215
+ });
18216
+ }
18217
+ if (opened.needsConfirmation !== void 0) {
18218
+ fail(opts, ExitCode.ConfirmationRequired, {
18219
+ error: `publishing ${JSON.stringify(opened.needsConfirmation.componentName)} for the first time is a decision a person makes, not a build step`,
18220
+ code: "first-publish-unconfirmed",
18221
+ remediation: `A human runs \`${tendrilCommand(`publish ${opts.bundleDir} --confirm-publish`)}\` in their own terminal. Later publishes of this component need no flag.`
18222
+ });
18223
+ }
18224
+ refuse2(opts, opened, "publish-refused");
18225
+ }
18226
+ const uploaded = [];
18227
+ for (const object of opened.value.plan.objects) {
18228
+ const file = path52.join(bundleDir, object.relPath);
18229
+ if (!existsSync42(file)) {
18230
+ fail(opts, ExitCode.InputValidation, {
18231
+ error: `the portal expects ${object.relPath}, and it is not in ${opts.bundleDir}`,
18232
+ code: "planned-file-missing",
18233
+ remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` and publish again \u2014 a publication is refused rather than going live with a hole in its evidence.`
18234
+ });
18235
+ }
18236
+ const sent = await client.upload({
18237
+ publicationId: opened.value.publicationId,
18238
+ relPath: object.relPath,
18239
+ bytes: new Uint8Array(readFileSync39(file))
18240
+ });
18241
+ if (!sent.ok) refuse2(opts, sent, "upload-refused");
18242
+ uploaded.push({ relPath: object.relPath, deduplicated: sent.value.deduplicated });
18243
+ }
18244
+ const committed = await client.commit({ publicationId: opened.value.publicationId });
18245
+ if (!committed.ok) {
18246
+ if (committed.missing !== void 0 && committed.missing.length > 0) {
18247
+ fail(opts, ExitCode.General, {
18248
+ error: `this publication is missing objects the bundle declared, and a verdict beside missing evidence is worse than no page: ${committed.missing.join(", ")}`,
18249
+ code: "publication-incomplete",
18250
+ remediation: `Run \`${tendrilCommand(`publish ${opts.bundleDir}`)}\` again \u2014 it rejoins this same unfinished publication and re-sends only what is missing.`
18251
+ });
18252
+ }
18253
+ refuse2(opts, committed, "commit-refused");
18254
+ }
18255
+ emitData(
18256
+ opts,
18257
+ {
18258
+ publicationId: committed.value.publicationId,
18259
+ url: committed.value.url,
18260
+ componentName,
18261
+ figmaFile: opened.value.figmaFile,
18262
+ rulerVersion: opened.value.rulerVersion,
18263
+ resumed: opened.value.resumed === true,
18264
+ files: uploaded
18265
+ },
18266
+ () => {
18267
+ const reused = uploaded.filter((u) => u.deduplicated).length;
18268
+ if (opened.value.resumed === true) process.stdout.write(`resumed the unfinished publish of ${componentName}
18269
+ `);
18270
+ process.stdout.write(`published ${componentName} \u2014 ${String(uploaded.length)} files`);
18271
+ process.stdout.write(reused > 0 ? ` (${String(reused)} you already had)
18272
+ ` : "\n");
18273
+ process.stdout.write(` ${committed.value.url}
18274
+ `);
18275
+ process.stdout.write(` verdict as scored by ruler ${opened.value.rulerVersion}
18276
+ `);
18277
+ }
18278
+ );
18279
+ }
18280
+ function readBundle(opts, bundleDir) {
18281
+ const manifestPath2 = path52.join(bundleDir, "component.json");
18282
+ const reportPath = path52.join(bundleDir, EVIDENCE_DIR, VERIFY_REPORT_FILENAME);
18283
+ if (!existsSync42(manifestPath2)) {
18284
+ fail(opts, ExitCode.InputValidation, {
18285
+ error: `there is no component.json in ${opts.bundleDir}, so this is not a bundle`,
18286
+ code: "not-a-bundle",
18287
+ remediation: `Point publish at a directory a Tendril run produced. \`${tendrilCommand("generate --help")}\` shows how one is made.`
18288
+ });
18289
+ }
18290
+ if (!existsSync42(reportPath)) {
18291
+ fail(opts, ExitCode.InputValidation, {
18292
+ error: `this bundle has never been verified \u2014 there is no ${EVIDENCE_DIR}/${VERIFY_REPORT_FILENAME}`,
18293
+ code: "bundle-not-verified",
18294
+ remediation: `Run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` first. Verification is free and needs no account; publishing without it would put a page up with no verdict on it.`
18295
+ });
18296
+ }
18297
+ const { manifest } = readBundleManifest(readFileSync39(manifestPath2, "utf8"));
18298
+ if (manifest === void 0) {
18299
+ fail(opts, ExitCode.InputValidation, {
18300
+ error: "component.json did not parse as a bundle manifest",
18301
+ code: "no-mount-contract",
18302
+ remediation: `Re-emit the bundle \u2014 \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` rewrites component.json \u2014 then publish.`
18303
+ });
18304
+ }
18305
+ const reportText = readFileSync39(reportPath, "utf8");
18306
+ let report;
18307
+ try {
18308
+ report = JSON.parse(reportText);
18309
+ } catch (error) {
18310
+ fail(opts, ExitCode.InputValidation, {
18311
+ error: `${EVIDENCE_DIR}/${VERIFY_REPORT_FILENAME} is not parseable JSON: ${error.message}`,
18312
+ code: "report-unparseable",
18313
+ remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` \u2014 that file is written by the ruler and should never be edited by hand.`
18314
+ });
18315
+ }
18316
+ if (report === null || typeof report !== "object" || Array.isArray(report)) {
18317
+ fail(opts, ExitCode.InputValidation, {
18318
+ error: `${EVIDENCE_DIR}/${VERIFY_REPORT_FILENAME} parses, but a verify report is a JSON object and this is not one`,
18319
+ code: "report-not-an-object",
18320
+ remediation: `Re-run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\`.`
18321
+ });
18322
+ }
18323
+ return { manifest, report, reportText, files: bundleFiles(bundleDir) };
18324
+ }
18325
+ function httpClient(opts, origin) {
18326
+ if (origin === "") {
18327
+ fail(opts, ExitCode.InputValidation, {
18328
+ error: "no portal to publish to",
18329
+ code: "no-portal-configured",
18330
+ remediation: `Pass \`--to <url>\` or set TENDRIL_PORTAL_URL. \`${tendrilCommand(`publish ${opts.bundleDir} --dry-run`)}\` shows exactly what would be published without needing one.`
18331
+ });
18332
+ }
18333
+ if (!isSecureOrigin(origin)) {
18334
+ fail(opts, ExitCode.InputValidation, {
18335
+ error: `${origin} is not https, so a session token sent there would travel in the clear`,
18336
+ code: "portal-not-https",
18337
+ remediation: "Use the https:// address of your portal. Only 127.0.0.1 and localhost are exempt, for local development."
18338
+ });
18339
+ }
18340
+ const found = tokenFor(origin);
18341
+ if (!found.ok && found.reason === "origin-mismatch") {
18342
+ fail(opts, ExitCode.Auth, {
18343
+ error: `the session available here belongs to ${found.boundTo}, and this would publish to ${origin}`,
18344
+ code: "session-belongs-to-another-portal",
18345
+ remediation: found.from === "env" ? `TENDRIL_TOKEN is pinned to TENDRIL_PORTAL_URL (${found.boundTo}). Publish to that portal, or set both to ${origin} together \u2014 a token is a credential for one host.` : `Sign in to ${origin}. The stored session is for ${found.boundTo}, and sending it elsewhere would hand that host a live credential.`
18346
+ });
18347
+ }
18348
+ if (!found.ok) {
18349
+ fail(opts, ExitCode.Auth, {
18350
+ error: `no session for ${origin}`,
18351
+ code: "not-signed-in",
18352
+ remediation: `Run \`${tendrilCommand(`login --to ${origin}`)}\` and paste the token your portal operator gave you (or set TENDRIL_TOKEN and TENDRIL_PORTAL_URL together for this shell). Verification stays free and account-less; only publishing needs an account.`
18353
+ });
18354
+ }
18355
+ return new HttpPublishClient({ origin, token: found.token });
18356
+ }
18357
+ function refuse2(opts, sent, code) {
18358
+ const detail = sent.detail === void 0 || sent.detail.length === 0 ? "" : `: ${sent.detail.join(", ")}`;
18359
+ fail(opts, sent.status === 401 ? ExitCode.Auth : ExitCode.General, {
18360
+ error: `${sent.refusal}${detail}`,
18361
+ code,
18362
+ remediation: sent.status === 401 ? `That session is no longer valid. Run \`${tendrilCommand(`login --to ${(opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "")}`)}\` and paste a fresh token.` : sent.status >= 500 ? `The portal failed on its side. Quote the error id above to whoever runs it, then run \`${tendrilCommand(`publish ${opts.bundleDir}`)}\` again \u2014 a retry rejoins this same unfinished publication.` : `Fix what is named above and run \`${tendrilCommand(`publish ${opts.bundleDir}`)}\` again \u2014 it rejoins this same unfinished publication rather than starting a second one.`
18363
+ });
18364
+ }
18365
+ var init_publish = __esm({
18366
+ "packages/cli/src/commands/publish.ts"() {
18367
+ "use strict";
18368
+ init_src3();
18369
+ init_src4();
18370
+ init_invocation();
18371
+ init_output();
18372
+ init_publish_client();
18373
+ }
18374
+ });
18375
+
17035
18376
  // packages/cli/src/commands/generate-route.ts
17036
18377
  var generate_route_exports = {};
17037
18378
  __export(generate_route_exports, {
@@ -17055,18 +18396,18 @@ var generate_recorded_exports = {};
17055
18396
  __export(generate_recorded_exports, {
17056
18397
  runGenerateRecorded: () => runGenerateRecorded
17057
18398
  });
17058
- import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
17059
- import { existsSync as existsSync40, readFileSync as readFileSync35 } from "node:fs";
17060
- import path49 from "node:path";
18399
+ import { confirm as confirm3, isCancel as isCancel4 } from "@clack/prompts";
18400
+ import { existsSync as existsSync43, readFileSync as readFileSync40 } from "node:fs";
18401
+ import path53 from "node:path";
17061
18402
  async function runGenerateRecorded(opts) {
17062
18403
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
17063
- const outDirAbs = path49.resolve(callerCwd, opts.out);
17064
- const recordedAsPath = path49.resolve(callerCwd, opts.recorded);
18404
+ const outDirAbs = path53.resolve(callerCwd, opts.out);
18405
+ const recordedAsPath = path53.resolve(callerCwd, opts.recorded);
17065
18406
  let task;
17066
18407
  let taskName;
17067
18408
  let authoredApi;
17068
18409
  let composition;
17069
- const isSet = existsSync40(path49.join(recordedAsPath, "recording-set.json"));
18410
+ const isSet = existsSync43(path53.join(recordedAsPath, "recording-set.json"));
17070
18411
  const registry = TASKS[opts.recorded];
17071
18412
  if (registry !== void 0 && !isSet) {
17072
18413
  task = registry;
@@ -17075,7 +18416,7 @@ async function runGenerateRecorded(opts) {
17075
18416
  try {
17076
18417
  const authored = authorTaskFromSet(recordedAsPath);
17077
18418
  task = authored.task;
17078
- taskName = path49.basename(recordedAsPath);
18419
+ taskName = path53.basename(recordedAsPath);
17079
18420
  authoredApi = authored.api;
17080
18421
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
17081
18422
  if (roles.success) composition = roles.data;
@@ -17109,7 +18450,7 @@ async function runGenerateRecorded(opts) {
17109
18450
  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)`);
17110
18451
  }
17111
18452
  const missing = task.configs.filter(
17112
- (c) => !existsSync40(path49.join(task.set, c.rep, "get_screenshot.json")) || !existsSync40(path49.join(task.set, c.rep, "get_metadata.json")) || !existsSync40(path49.join(task.set, c.rep, "get_design_context.json"))
18453
+ (c) => !existsSync43(path53.join(task.set, c.rep, "get_screenshot.json")) || !existsSync43(path53.join(task.set, c.rep, "get_metadata.json")) || !existsSync43(path53.join(task.set, c.rep, "get_design_context.json"))
17113
18454
  );
17114
18455
  if (missing.length > 0) {
17115
18456
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -17179,8 +18520,8 @@ async function runGenerateRecorded(opts) {
17179
18520
  ` : `${line}
17180
18521
  `);
17181
18522
  if (opts.dryRun) {
17182
- emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path49.join(outDirAbs, taskName) }, () => {
17183
- process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path49.join(outDirAbs, taskName)})
18523
+ emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path53.join(outDirAbs, taskName) }, () => {
18524
+ process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path53.join(outDirAbs, taskName)})
17184
18525
  `);
17185
18526
  });
17186
18527
  return;
@@ -17195,7 +18536,7 @@ async function runGenerateRecorded(opts) {
17195
18536
  });
17196
18537
  }
17197
18538
  const accepted = await confirm3({ message: `Proceed? (cap $${opts.capUsd.toFixed(2)}, ${opts.maxIterations} iterations max)` });
17198
- if (isCancel3(accepted) || accepted !== true) {
18539
+ if (isCancel4(accepted) || accepted !== true) {
17199
18540
  fail(opts, ExitCode.ConfirmationRequired, {
17200
18541
  error: "Run was not confirmed.",
17201
18542
  code: "CONFIRMATION_DECLINED",
@@ -17203,10 +18544,10 @@ async function runGenerateRecorded(opts) {
17203
18544
  });
17204
18545
  }
17205
18546
  }
17206
- const bundleDir = path49.join(outDirAbs, taskName);
17207
- if (existsSync40(path49.join(bundleDir, "component.json"))) {
18547
+ const bundleDir = path53.join(outDirAbs, taskName);
18548
+ if (existsSync43(path53.join(bundleDir, "component.json"))) {
17208
18549
  try {
17209
- const prior = readBundleManifest(readFileSync35(path49.join(bundleDir, "component.json"), "utf8")).manifest;
18550
+ const prior = readBundleManifest(readFileSync40(path53.join(bundleDir, "component.json"), "utf8")).manifest;
17210
18551
  if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
17211
18552
  fail(opts, ExitCode.InputValidation, {
17212
18553
  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`,
@@ -17373,7 +18714,7 @@ init_invocation();
17373
18714
  init_output();
17374
18715
  import { intro, isCancel, outro, password } from "@clack/prompts";
17375
18716
  import fs from "node:fs";
17376
- import path27 from "node:path";
18717
+ import path29 from "node:path";
17377
18718
  var INIT_DESCRIPTION = {
17378
18719
  name: "init",
17379
18720
  summary: "Configure the OpenRouter credential in .env, and optionally a Figma token (idempotent).",
@@ -17415,7 +18756,7 @@ async function runInit(flags) {
17415
18756
  printDescription(INIT_DESCRIPTION);
17416
18757
  return;
17417
18758
  }
17418
- const envPath = path27.resolve(process.cwd(), ".env");
18759
+ const envPath = path29.resolve(process.cwd(), ".env");
17419
18760
  const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
17420
18761
  let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
17421
18762
  let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
@@ -17436,7 +18777,7 @@ async function runInit(flags) {
17436
18777
  if (openrouterKey) next.set(ENV_KEYS.openrouter, openrouterKey);
17437
18778
  if (figmaToken) next.set(ENV_KEYS.figma, figmaToken);
17438
18779
  const changed = [...next].some(([key, value]) => existing.get(key) !== value);
17439
- const gitignorePath = path27.resolve(process.cwd(), ".gitignore");
18780
+ const gitignorePath = path29.resolve(process.cwd(), ".gitignore");
17440
18781
  const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
17441
18782
  const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
17442
18783
  if (flags.dryRun) {
@@ -17492,14 +18833,14 @@ init_invocation();
17492
18833
  init_output();
17493
18834
  init_entitlement();
17494
18835
  import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
17495
- import { readFileSync as readFileSync18, readdirSync as readdirSync8, existsSync as existsSync22 } from "node:fs";
18836
+ import { readFileSync as readFileSync21, readdirSync as readdirSync9, existsSync as existsSync23 } from "node:fs";
17496
18837
 
17497
18838
  // packages/cli/src/pipeline.ts
17498
18839
  init_src2();
17499
18840
  init_src5();
17500
18841
  init_src4();
17501
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
17502
- import path28 from "node:path";
18842
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
18843
+ import path30 from "node:path";
17503
18844
 
17504
18845
  // packages/cli/src/assets-module.ts
17505
18846
  init_src();
@@ -17835,8 +19176,8 @@ async function runGenerationPipeline(input) {
17835
19176
  });
17836
19177
  const written = [];
17837
19178
  if (!input.dryRun) {
17838
- const dir = path28.resolve(input.outDir, semantics.componentName);
17839
- mkdirSync5(dir, { recursive: true });
19179
+ const dir = path30.resolve(input.outDir, semantics.componentName);
19180
+ mkdirSync6(dir, { recursive: true });
17840
19181
  const files = {
17841
19182
  // Bundle-local tokens: THE emission the component's CSS resolves
17842
19183
  // against — the same artifact the verify harness injects. A preview
@@ -17859,14 +19200,14 @@ async function runGenerationPipeline(input) {
17859
19200
  `
17860
19201
  };
17861
19202
  for (const [name, content] of Object.entries(files)) {
17862
- const filePath = path28.join(dir, name);
17863
- writeFileSync8(filePath, content);
19203
+ const filePath = path30.join(dir, name);
19204
+ writeFileSync9(filePath, content);
17864
19205
  written.push(filePath);
17865
19206
  }
17866
19207
  for (const artifact of emitTokenArtifacts(input.mapping)) {
17867
- const filePath = path28.resolve(input.outDir, artifact.path);
17868
- mkdirSync5(path28.dirname(filePath), { recursive: true });
17869
- writeFileSync8(filePath, artifact.content);
19208
+ const filePath = path30.resolve(input.outDir, artifact.path);
19209
+ mkdirSync6(path30.dirname(filePath), { recursive: true });
19210
+ writeFileSync9(filePath, artifact.content);
17870
19211
  written.push(filePath);
17871
19212
  }
17872
19213
  }
@@ -17924,7 +19265,7 @@ var GENERATE_DESCRIPTION = {
17924
19265
  function resolveProvidedSource(flags, contextFile) {
17925
19266
  let raw;
17926
19267
  try {
17927
- raw = readFileSync18(contextFile, "utf8");
19268
+ raw = readFileSync21(contextFile, "utf8");
17928
19269
  } catch {
17929
19270
  fail(flags, ExitCode.InputValidation, {
17930
19271
  error: `Cannot read context file "${contextFile}".`,
@@ -18044,11 +19385,11 @@ token mapping (${mapping.flat.length} variables):
18044
19385
  let initialCode;
18045
19386
  let initialSemantics;
18046
19387
  try {
18047
- if (existsSync22(flags.out)) {
18048
- for (const entry of readdirSync8(flags.out)) {
19388
+ if (existsSync23(flags.out)) {
19389
+ for (const entry of readdirSync9(flags.out)) {
18049
19390
  const cjPath = `${flags.out}/${entry}/component.json`;
18050
- if (!existsSync22(cjPath)) continue;
18051
- const cj = JSON.parse(readFileSync18(cjPath, "utf8"));
19391
+ if (!existsSync23(cjPath)) continue;
19392
+ const cj = JSON.parse(readFileSync21(cjPath, "utf8"));
18052
19393
  if (cj.name !== void 0 && Array.isArray(cj.props)) {
18053
19394
  previousApi = JSON.stringify({
18054
19395
  componentName: cj.name,
@@ -18056,14 +19397,14 @@ token mapping (${mapping.flat.length} variables):
18056
19397
  });
18057
19398
  const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
18058
19399
  const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
18059
- if (flags.refine && existsSync22(tsxPath) && existsSync22(cssPath)) {
19400
+ if (flags.refine && existsSync23(tsxPath) && existsSync23(cssPath)) {
18060
19401
  initialCode = {
18061
- tsx: readFileSync18(tsxPath, "utf8"),
18062
- css: readFileSync18(cssPath, "utf8")
19402
+ tsx: readFileSync21(tsxPath, "utf8"),
19403
+ css: readFileSync21(cssPath, "utf8")
18063
19404
  };
18064
19405
  const semPath = `${flags.out}/${entry}/semantics.json`;
18065
- if (existsSync22(semPath)) {
18066
- initialSemantics = JSON.parse(readFileSync18(semPath, "utf8"));
19406
+ if (existsSync23(semPath)) {
19407
+ initialSemantics = JSON.parse(readFileSync21(semPath, "utf8"));
18067
19408
  }
18068
19409
  }
18069
19410
  break;
@@ -18458,6 +19799,50 @@ function buildProgram() {
18458
19799
  ...local["maxArea"] !== void 0 ? { maxArea: Number(local["maxArea"]) } : {}
18459
19800
  });
18460
19801
  });
19802
+ program.command("login").description("Sign this machine in to the Tendril portal (approve in your browser), so `publish` and `share` can reach it. Verification never needs this.").option("--to <url>", "the portal to sign in to (defaults to the hosted portal; or set TENDRIL_PORTAL_URL)").option("--paste", "paste a token minted on the portal's Connect page instead of approving in the browser").option("--device-start", "agents: start the browser approval and return the link and code immediately").option("--device-wait", "agents: wait for the browser approval started by --device-start").action(async (_o, cmd) => {
19803
+ const flags = globalFlags(cmd.parent);
19804
+ const local = cmd.opts();
19805
+ const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_login(), login_exports));
19806
+ await runLogin2({
19807
+ ...flags,
19808
+ ...local["to"] !== void 0 ? { to: local["to"] } : {},
19809
+ ...local["paste"] !== void 0 ? { paste: local["paste"] } : {},
19810
+ ...local["deviceStart"] !== void 0 ? { deviceStart: local["deviceStart"] } : {},
19811
+ ...local["deviceWait"] !== void 0 ? { deviceWait: local["deviceWait"] } : {}
19812
+ });
19813
+ });
19814
+ program.command("logout").description("Forget this machine's stored portal session. Does not end the session itself.").action(async (_o, cmd) => {
19815
+ const flags = globalFlags(cmd.parent);
19816
+ const { runLogout: runLogout2 } = await Promise.resolve().then(() => (init_login(), login_exports));
19817
+ runLogout2(flags);
19818
+ });
19819
+ program.command("share").description("Create a link that opens one publication's verdict and evidence for someone with no account. Anyone holding the link can read it.").argument("<publicationId>", "the publication to share, as `publish` printed it").option("--to <url>", "the portal (or set TENDRIL_PORTAL_URL)").option("--expires <days|timestamp>", "a number of days, or an ISO-8601 UTC timestamp; omit for a link that does not expire").option("--recipient <email>", "recorded so you can see who a link was for; never used to authenticate").option("--list", "show the links outstanding for this publication").option("--revoke <shareLinkId>", "stop a link working").action(async (publicationId, _o, cmd) => {
19820
+ const flags = globalFlags(cmd.parent);
19821
+ const local = cmd.opts();
19822
+ const { runShare: runShare2 } = await Promise.resolve().then(() => (init_share(), share_exports));
19823
+ await runShare2({
19824
+ ...flags,
19825
+ publicationId,
19826
+ ...local["to"] !== void 0 ? { to: local["to"] } : {},
19827
+ ...local["expires"] !== void 0 ? { expires: String(local["expires"]) } : {},
19828
+ ...local["recipient"] !== void 0 ? { recipient: local["recipient"] } : {},
19829
+ ...local["list"] !== void 0 ? { list: local["list"] } : {},
19830
+ ...local["revoke"] !== void 0 ? { revoke: local["revoke"] } : {}
19831
+ });
19832
+ });
19833
+ program.command("publish").description("Put a verified bundle and its evidence on a page, at a URL you can send to someone. Refuses a run the ruler declined.").argument("<bundleDir>", "bundle directory that has already been verified").option("--to <url>", "the portal to publish to (or set TENDRIL_PORTAL_URL)").option("--name <name>", "library label for this component (default: the bundle manifest's own name)").option("--accept-terms", "accept the current publishing terms for this design system as part of this run").option("--confirm-publish", "you, a person, saying this component may be published \u2014 required the first time, and refused without an interactive terminal").action(async (bundleDir, _o, cmd) => {
19834
+ const flags = globalFlags(cmd.parent);
19835
+ const local = cmd.opts();
19836
+ const { runPublish: runPublish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
19837
+ await runPublish2({
19838
+ ...flags,
19839
+ bundleDir,
19840
+ ...local["to"] !== void 0 ? { to: local["to"] } : {},
19841
+ ...local["name"] !== void 0 ? { name: local["name"] } : {},
19842
+ ...local["acceptTerms"] !== void 0 ? { acceptTerms: local["acceptTerms"] } : {},
19843
+ ...local["confirmPublish"] !== void 0 ? { confirmPublish: local["confirmPublish"] } : {}
19844
+ });
19845
+ });
18461
19846
  program.command("verify").description("Recompute verification for a generated bundle against its recording set (per-config status, behaviors, honest exit codes).").argument("<bundleDir>", "bundle directory to verify").option("--task <name>", "legacy task adapter for bundles WITHOUT component.json (bundle v1 carries its own prop manifest)").option("--set <dir>", "recording set directory (overrides the bundle's provenance path)").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--library <dir>", "workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: current directory)").option("--profile <file>", "a `tendril profile` artifact; reports whether the bundle followed your codebase conventions (never gates the verdict)").action(async (bundleDir, _opts, cmd) => {
18462
19847
  const flags = globalFlags(cmd);
18463
19848
  const local = cmd.opts();