@tendrilapp/cli 0.1.5 → 0.1.7

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.
@@ -66,7 +66,7 @@ var TOOLS = [
66
66
  },
67
67
  {
68
68
  name: "tendril_record_ingest",
69
- description: "Ingest a VERBATIM Figma tool-response for a planned rep. PREFER `text`: paste the tool response text exactly as received \u2014 no file to write, no wrapper to build; the CLI constructs the envelope from the same bytes. The response includes `next` (the following instruction \u2014 no separate record_next call) and, for get_design_context, `assets`: the emission's SVG/PNG assets are AUTO-FETCHED server-side; only listed failures need manual record_fetch/record_asset handling.",
69
+ description: "Ingest a VERBATIM Figma tool-response for a planned rep. PREFER `text` (single block) or `texts` (response split into multiple output blocks \u2014 pass each block verbatim, in order; NEVER hand-join them): no file to write, no wrapper to build; the CLI constructs the envelope from the same bytes. The response includes `next` (the following instruction \u2014 no separate record_next call) and, for get_design_context, `assets`: the emission's SVG/PNG assets are AUTO-FETCHED server-side; only listed failures need manual record_fetch/record_asset handling.",
70
70
  schema: z.object({
71
71
  setDir: str("recording set directory"),
72
72
  rep: str("planned rep slug, or __set__ for the set-level get_variable_defs"),
@@ -76,22 +76,26 @@ var TOOLS = [
76
76
  // exit 0). The sink in session.ts now contains the path too — this
77
77
  // is the second layer, and it makes the tool self-documenting.
78
78
  tool: z.enum(["get_design_context", "get_metadata", "get_screenshot", "get_variable_defs", "get_metadata_interior"]),
79
- text: optStr("the tool response text VERBATIM \u2014 exactly as returned, unmodified (preferred; text tools only \u2014 screenshots go through record_fetch)"),
80
- file: optStr("path to a saved envelope JSON (alternative to text)")
79
+ text: optStr("the tool response text VERBATIM \u2014 exactly as returned, unmodified (single-block responses; text tools only \u2014 screenshots go through record_fetch)"),
80
+ texts: z.array(z.string()).optional().describe("when the response arrived as MULTIPLE output blocks: every block, in order, each verbatim \u2014 never hand-join blocks yourself"),
81
+ file: optStr("path to a saved envelope JSON (alternative to text/texts)")
81
82
  }),
82
83
  // The text rides a temp file, never argv: Windows caps a command
83
84
  // line at ~32 KB and design-context envelopes routinely exceed it.
84
85
  argv: (i) => {
85
86
  const base = ["record", "ingest", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"]];
86
87
  const text = i["text"];
88
+ const texts = i["texts"];
87
89
  const file = i["file"];
88
- if (text === void 0 === (file === void 0)) throw new Error("pass exactly one of `text` (the verbatim response text) or `file` (a saved envelope)");
90
+ 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)");
91
+ if (file !== void 0) return [...base, "--file", file];
92
+ const tmp = path.join(mkdtempSync(path.join(os.tmpdir(), "tendril-envelope-")), "response.txt");
89
93
  if (text !== void 0) {
90
- const tmp = path.join(mkdtempSync(path.join(os.tmpdir(), "tendril-envelope-")), "response.txt");
91
94
  writeFileSync(tmp, text);
92
95
  return [...base, "--file", tmp, "--raw"];
93
96
  }
94
- return [...base, "--file", file];
97
+ writeFileSync(tmp, JSON.stringify(texts));
98
+ return [...base, "--file", tmp, "--raw-parts"];
95
99
  }
96
100
  },
97
101
  {
@@ -154,7 +158,8 @@ var TOOLS = [
154
158
  candidateDir: str("directory containing the proposed bundle files"),
155
159
  bar: optStr("pass (default) or cert"),
156
160
  host: optStr("your host identity (e.g. claude-code, cursor, codex) \u2014 recorded as self-reported provenance"),
157
- model: str("the model that proposed the candidate \u2014 REQUIRED; recorded as self-reported provenance, and the CLI refuses to score without it")
161
+ model: str("the model that proposed the candidate \u2014 REQUIRED; recorded as self-reported provenance, and the CLI refuses to score without it"),
162
+ rebind: z.boolean().optional().describe("explicitly re-bind an already-bound bundle to a DIFFERENT recording set \u2014 scoring refuses this otherwise, because rebinding silently rewrites the bundle's verification identity; only pass after telling the user")
158
163
  }),
159
164
  argv: (i) => [
160
165
  "engine",
@@ -164,7 +169,26 @@ var TOOLS = [
164
169
  ...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
165
170
  ...i["host"] !== void 0 ? ["--host", i["host"]] : [],
166
171
  "--model",
167
- i["model"]
172
+ i["model"],
173
+ ...i["rebind"] === true ? ["--rebind"] : []
174
+ ]
175
+ },
176
+ {
177
+ name: "tendril_codeconnect",
178
+ description: "Emit a Figma Code Connect template (.figma.ts) for a certified bundle \u2014 every Figma variant value mapped to its verified prop fragment from recorded truth, stamped with the bundle's trust statement. EXTRA VALUE step after verify passes: offer it to the user. Publishing is the USER'S action (their Figma token, Organization/Enterprise plan) \u2014 via npx @figma/code-connect connect publish, or the Figma MCP's own add_code_connect_map/send_code_connect_mappings tools if available in this session.",
179
+ schema: z.object({
180
+ bundleDir: str("bundle directory (carries component.json)"),
181
+ figmaUrl: str("figma.com /design/ URL of the COMPONENT SET, with node-id (ask the user to Copy link to selection if you don't have it)"),
182
+ set: optStr("recording set override (default: the bundle's provenance path)"),
183
+ out: optStr("output file path (default: <bundle>/<Component>.figma.ts)")
184
+ }),
185
+ argv: (i) => [
186
+ "codeconnect",
187
+ i["bundleDir"],
188
+ "--figma-url",
189
+ i["figmaUrl"],
190
+ ...i["set"] !== void 0 ? ["--set", i["set"]] : [],
191
+ ...i["out"] !== void 0 ? ["--out", i["out"]] : []
168
192
  ]
169
193
  },
170
194
  {
package/dist/tendril.js CHANGED
@@ -414,26 +414,32 @@ function toNode(tag, attrs) {
414
414
  if (attrs["hidden"] === "true") node.hidden = true;
415
415
  return node;
416
416
  }
417
- function parseMetadataStructure(response) {
417
+ function parseMetadataForest(response) {
418
418
  const stack = [];
419
- let root;
419
+ const roots = [];
420
420
  for (const m of response.matchAll(TAG_RE)) {
421
421
  const [, closing, tag, rawAttrs, selfClosing] = m;
422
422
  if (closing === "/") {
423
423
  const done = stack.pop();
424
- if (done !== void 0 && stack.length === 0 && root === void 0) root = done;
424
+ if (done !== void 0 && stack.length === 0) roots.push(done);
425
425
  continue;
426
426
  }
427
427
  const node = toNode(tag.toLowerCase(), parseAttrs(rawAttrs));
428
428
  const parent = stack[stack.length - 1];
429
429
  if (parent !== void 0) parent.children.push(node);
430
430
  if (selfClosing === "/") {
431
- if (stack.length === 0 && root === void 0) root = node;
431
+ if (stack.length === 0) roots.push(node);
432
432
  continue;
433
433
  }
434
434
  stack.push(node);
435
435
  }
436
- if (root === void 0) root = stack[0];
436
+ const truncated = stack.length > 0;
437
+ if (truncated && stack[0] !== void 0) roots.push(stack[0]);
438
+ return { roots, truncated };
439
+ }
440
+ function parseMetadataStructure(response) {
441
+ const { roots } = parseMetadataForest(response);
442
+ const root = roots[0];
437
443
  if (root === void 0) {
438
444
  throw new Error("get_metadata response contains no parseable structure");
439
445
  }
@@ -878,7 +884,7 @@ function planQueue(symbols, opts = {}) {
878
884
  if (axes === void 0) {
879
885
  reps.push({ slug: slugify(kebab2(sym.name) || sym.nodeId.replace(":", "-")), nodeId: sym.nodeId, ...sym.sourceFrame !== void 0 ? { sourceFrame: sym.sourceFrame } : {}, tier: "singleton" });
880
886
  } else {
881
- reps.push({ slug: slugify(kebab2(Object.values(axes).join("-"))), nodeId: sym.nodeId, ...sym.sourceFrame !== void 0 ? { sourceFrame: sym.sourceFrame } : {}, tier: "anchor", axes });
887
+ reps.push({ slug: slugify(kebab2(Object.values(axes).join("-")) || sym.nodeId.replace(":", "-")), nodeId: sym.nodeId, ...sym.sourceFrame !== void 0 ? { sourceFrame: sym.sourceFrame } : {}, tier: "anchor", axes });
882
888
  }
883
889
  }
884
890
  continue;
@@ -898,11 +904,13 @@ function planQueue(symbols, opts = {}) {
898
904
  reps.push({ slug: slugify("anchor"), nodeId: sym.nodeId, ...frame, tier: "anchor", axes });
899
905
  } else if (diffs.length === 1) {
900
906
  const k = diffs[0];
901
- reps.push({ slug: slugify(kebab2(`${k}-${axes[k]}`)), nodeId: sym.nodeId, ...frame, tier: "one-factor", axes });
907
+ reps.push({ slug: slugify(kebab2(`${k}-${axes[k]}`) || `pose-${sym.nodeId.replace(":", "-")}`), nodeId: sym.nodeId, ...frame, tier: "one-factor", axes });
902
908
  } else if (diffs.length === 2 && diffs.some((k) => crossAxes.includes(k.toLowerCase()))) {
903
- reps.push({ slug: slugify(kebab2(`cross-${diffs.map((k) => axes[k]).join("-")}`)), nodeId: sym.nodeId, ...frame, tier: "cross", axes });
909
+ reps.push({ slug: slugify(kebab2(`cross-${diffs.map((k) => axes[k]).join("-")}`) || `cross-${sym.nodeId.replace(":", "-")}`), nodeId: sym.nodeId, ...frame, tier: "cross", axes });
910
+ } else if (opts.sample !== true) {
911
+ reps.push({ slug: slugify(kebab2(`cross-${diffs.map((k) => `${k}-${axes[k]}`).join("-")}`) || `cross-${sym.nodeId.replace(":", "-")}`), nodeId: sym.nodeId, ...frame, tier: "cross", axes });
904
912
  } else {
905
- notRecorded.push({ nodeId: sym.nodeId, name: sym.name, reason: `${diffs.length}-factor pose outside the queue rule (diffs: ${diffs.join(", ")})` });
913
+ notRecorded.push({ nodeId: sym.nodeId, name: sym.name, reason: `${diffs.length}-factor pose outside the SAMPLED queue rule (diffs: ${diffs.join(", ")}) \u2014 sampling is blind to multi-axis interactions; re-plan without --sample for the full matrix` });
906
914
  }
907
915
  }
908
916
  }
@@ -940,6 +948,27 @@ function planSet(setDir, component, symbols, opts = {}) {
940
948
  const wantsNewDefaults = opts.defaults !== void 0 && JSON.stringify(opts.defaults) !== JSON.stringify(existing.defaults ?? {});
941
949
  const anythingRecorded = existing.reps.some((r) => RECORD_TOOLS.some((t) => existsSync(path.join(setDir, r.slug, `${t}.json`))));
942
950
  if (!wantsNewDefaults || anythingRecorded) {
951
+ if (opts.sample !== true) {
952
+ const known = new Set(existing.reps.map((r) => r.nodeId));
953
+ const fullPlan = planQueue(symbols, { ...opts, sample: false });
954
+ const usedSlugs = new Set(existing.reps.map((r) => r.slug));
955
+ const missing = fullPlan.reps.filter((r) => !known.has(r.nodeId));
956
+ if (missing.length > 0) {
957
+ const appended = missing.map((r) => {
958
+ let slug = r.slug;
959
+ let n = 2;
960
+ while (usedSlugs.has(slug)) slug = `${r.slug}-${n++}`;
961
+ usedSlugs.add(slug);
962
+ return { slug, nodeId: r.nodeId, ...r.sourceFrame !== void 0 ? { sourceFrame: r.sourceFrame } : {} };
963
+ });
964
+ existing.reps.push(...appended);
965
+ existing.planMode = "full";
966
+ delete existing.notRecorded;
967
+ writeFileSync(manifestPath(setDir), `${JSON.stringify(existing, null, 1)}
968
+ `);
969
+ return { manifest: existing, plan: { reps: [], notRecorded: [] }, resumed: true, toppedUp: appended.map((a) => ({ slug: a.slug, nodeId: a.nodeId })) };
970
+ }
971
+ }
943
972
  return { manifest: existing, plan: { reps: [], notRecorded: [] }, resumed: true };
944
973
  }
945
974
  }
@@ -955,6 +984,7 @@ function planSet(setDir, component, symbols, opts = {}) {
955
984
  return variants.length > 0 ? { latticeNames: variants } : {};
956
985
  })(),
957
986
  reps: plan.reps.map((r) => ({ slug: r.slug, nodeId: r.nodeId, ...r.sourceFrame !== void 0 ? { sourceFrame: r.sourceFrame } : {} })),
987
+ planMode: opts.sample === true ? "sample" : "full",
958
988
  ...plan.notRecorded.length > 0 ? { notRecorded: plan.notRecorded.map((n) => `${n.name} (${n.nodeId}): ${n.reason}`).join("; ") } : {}
959
989
  };
960
990
  mkdirSync(setDir, { recursive: true });
@@ -1008,6 +1038,14 @@ function ingestEnvelope(setDir, slug, tool, payload) {
1008
1038
  `);
1009
1039
  return { overwrote };
1010
1040
  }
1041
+ function envelopeTextContent(env) {
1042
+ const parts = env?.content ?? [];
1043
+ return parts.map((c) => c.text ?? "").filter((t) => t !== "").join("\n");
1044
+ }
1045
+ function envelopeFirstTextPart(env) {
1046
+ const parts = env?.content ?? [];
1047
+ return parts.find((c) => typeof c.text === "string" && c.text !== "")?.text ?? "";
1048
+ }
1011
1049
  function assetUrlsFromEnvelopeText(text) {
1012
1050
  const byUrl = /* @__PURE__ */ new Map();
1013
1051
  for (const m of text.matchAll(/const\s+\w+\s*=\s*"(https?:\/\/[^"]+\/assets?\/([a-z0-9-]+)\.(svg|png))"/gi)) {
@@ -1063,6 +1101,10 @@ var init_session = __esm({
1063
1101
  * domains: the API must cover the lattice even where only a subset
1064
1102
  * is recorded. */
1065
1103
  latticeNames: z4.array(z4.string()).optional(),
1104
+ /** Which planning mode produced this queue. Absent = planned before
1105
+ * the full-matrix default (i.e. sampled) — resume uses this to
1106
+ * top-up rather than silently perpetuating a sampled queue. */
1107
+ planMode: z4.enum(["full", "sample"]).optional(),
1066
1108
  roles: z4.unknown().optional()
1067
1109
  });
1068
1110
  manifestPath = (setDir) => path.join(setDir, "recording-set.json");
@@ -1170,8 +1212,8 @@ var init_src = __esm({
1170
1212
  function variableNameToPath(name) {
1171
1213
  return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
1172
1214
  }
1173
- function tokenPathToCssVar(path33) {
1174
- return `--${path33.join("-")}`;
1215
+ function tokenPathToCssVar(path34) {
1216
+ return `--${path34.join("-")}`;
1175
1217
  }
1176
1218
  function toDtcgToken(variable, defaultMode) {
1177
1219
  const modes = Object.keys(variable.valuesByMode);
@@ -1215,11 +1257,11 @@ function toDtcgToken(variable, defaultMode) {
1215
1257
  }
1216
1258
  function mapVariablesToDtcg(variables, defaultMode = "light") {
1217
1259
  const entries = variables.map((variable) => {
1218
- const path33 = variableNameToPath(variable.name);
1219
- if (path33.length === 0) {
1260
+ const path34 = variableNameToPath(variable.name);
1261
+ if (path34.length === 0) {
1220
1262
  throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
1221
1263
  }
1222
- return { variable, path: path33 };
1264
+ return { variable, path: path34 };
1223
1265
  });
1224
1266
  const groupPrefixes = /* @__PURE__ */ new Set();
1225
1267
  for (const e of entries) {
@@ -1240,21 +1282,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
1240
1282
  }
1241
1283
  const tokens = {};
1242
1284
  const flat = [];
1243
- for (const { variable, path: path33 } of entries) {
1285
+ for (const { variable, path: path34 } of entries) {
1244
1286
  const token = toDtcgToken(variable, defaultMode);
1245
1287
  let group = tokens;
1246
- for (const segment of path33.slice(0, -1)) {
1288
+ for (const segment of path34.slice(0, -1)) {
1247
1289
  const existing = group[segment];
1248
1290
  group = existing ?? (group[segment] = {});
1249
1291
  }
1250
- const leaf = path33[path33.length - 1];
1292
+ const leaf = path34[path34.length - 1];
1251
1293
  if (group[leaf] !== void 0) {
1252
- throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path33.join(".")}" (variable ${variable.id})`);
1294
+ throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path34.join(".")}" (variable ${variable.id})`);
1253
1295
  }
1254
1296
  group[leaf] = token;
1255
1297
  flat.push({
1256
- path: path33.join("."),
1257
- cssVar: tokenPathToCssVar(path33),
1298
+ path: path34.join("."),
1299
+ cssVar: tokenPathToCssVar(path34),
1258
1300
  type: token.$type,
1259
1301
  value: token.$value
1260
1302
  });
@@ -1443,9 +1485,9 @@ function boundId(value) {
1443
1485
  return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
1444
1486
  }
1445
1487
  function resolveBinding(ctx, id) {
1446
- const path33 = ctx.pathById.get(id);
1447
- if (path33 === void 0) ctx.unresolved.add(id);
1448
- return path33;
1488
+ const path34 = ctx.pathById.get(id);
1489
+ if (path34 === void 0) ctx.unresolved.add(id);
1490
+ return path34;
1449
1491
  }
1450
1492
  function parseVariantProps(name) {
1451
1493
  if (!name.includes("=")) return void 0;
@@ -1480,8 +1522,8 @@ function walk(ctx, raw) {
1480
1522
  if (!isObject(paint) || paint["visible"] === false) continue;
1481
1523
  const id = boundId(paint);
1482
1524
  if (id !== void 0) {
1483
- const path33 = resolveBinding(ctx, id);
1484
- if (path33 !== void 0) tokens.add(path33);
1525
+ const path34 = resolveBinding(ctx, id);
1526
+ if (path34 !== void 0) tokens.add(path34);
1485
1527
  } else if (typeof paint["color"] === "string") {
1486
1528
  ctx.hardcoded.push({ node: name, property, value: paint["color"] });
1487
1529
  }
@@ -1489,8 +1531,8 @@ function walk(ctx, raw) {
1489
1531
  }
1490
1532
  const radiusId = boundId(raw["cornerRadius"]);
1491
1533
  if (radiusId !== void 0) {
1492
- const path33 = resolveBinding(ctx, radiusId);
1493
- if (path33 !== void 0) tokens.add(path33);
1534
+ const path34 = resolveBinding(ctx, radiusId);
1535
+ if (path34 !== void 0) tokens.add(path34);
1494
1536
  } else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
1495
1537
  ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
1496
1538
  }
@@ -1500,10 +1542,10 @@ function walk(ctx, raw) {
1500
1542
  layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
1501
1543
  const gapId = boundId(raw["itemSpacing"]);
1502
1544
  if (gapId !== void 0) {
1503
- const path33 = resolveBinding(ctx, gapId);
1504
- if (path33 !== void 0) {
1505
- layout.gap = path33;
1506
- tokens.add(path33);
1545
+ const path34 = resolveBinding(ctx, gapId);
1546
+ if (path34 !== void 0) {
1547
+ layout.gap = path34;
1548
+ tokens.add(path34);
1507
1549
  }
1508
1550
  } else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
1509
1551
  ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
@@ -1512,10 +1554,10 @@ function walk(ctx, raw) {
1512
1554
  for (const field of PADDING_FIELDS) {
1513
1555
  const id = boundId(raw[field]);
1514
1556
  if (id !== void 0) {
1515
- const path33 = resolveBinding(ctx, id);
1516
- if (path33 !== void 0) {
1517
- paddingPaths.push(path33);
1518
- tokens.add(path33);
1557
+ const path34 = resolveBinding(ctx, id);
1558
+ if (path34 !== void 0) {
1559
+ paddingPaths.push(path34);
1560
+ tokens.add(path34);
1519
1561
  }
1520
1562
  } else if (typeof raw[field] === "number" && raw[field] !== 0) {
1521
1563
  ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
@@ -4309,7 +4351,7 @@ var init_prelude = __esm({
4309
4351
  - Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (a missing font weight must fall back visibly, never fake-bold); color-scheme MATCHING YOUR RECORDING (light for a light capture, dark for a dark one) and direction: ltr \u2014 pin them, so the render cannot follow the viewer OS preference and drift from the capture it is graded against; isolation: isolate (own stacking context; overlay z-indexes never fight the host).
4310
4352
  - Interactive controls (buttons, options): touch-action: manipulation and user-select: none. Text inputs stay selectable (never user-select: none on them).
4311
4353
  - Scrollable popovers/menus: overscroll-behavior: contain.
4312
- - Focus rings on :focus-visible, never bare :focus (keyboard shows the ring; mouse click must not leave one).
4354
+ - Focus indicators bind to :focus-visible, never bare :focus. If the recording contains a focus pose, style the indicator from that recorded truth. If NO focus pose is recorded, do NOT invent ring colors/widths/offsets \u2014 an invented ring is unrecorded pixels; keep the browser's default focus indicator (leave outline in place on :focus-visible) and state the gap in your report. Text inputs match :focus-visible even on mouse click BY SPEC \u2014 that is platform-correct accessibility, never a bug to suppress with JS modality tracking.
4313
4355
  - Every animation wrapped in @media (prefers-reduced-motion: no-preference) or disabled under reduce.`;
4314
4356
  }
4315
4357
  });
@@ -6079,10 +6121,10 @@ function buildTrustStatement(input) {
6079
6121
  const unrecorded = input.latticeConfigs === null ? null : Math.max(0, input.latticeConfigs - input.scored);
6080
6122
  const interaction = input.interactionChecks === 0 ? "interaction behaviors NONE VERIFIED (0 checks)" : `interaction behaviors ${input.interactionPassed}/${input.interactionChecks}`;
6081
6123
  const prelude = input.preludeChecks === 0 ? "" : `, page hygiene ${input.preludePassed}/${input.preludeChecks}`;
6082
- return `Verified against recorded truth: ${input.pass}/${input.scored} recorded configs at or above the pass bar (${input.certified} certified), ${interaction}${prelude}.` + (unrecorded === null ? "" : ` ${unrecorded} lattice configs are unrecorded and UNVERIFIED.`) + ` These numbers are claims: recompute them with \`tendril verify\` \u2014 certification authority lives in the CLI ruler, never in this file.`;
6124
+ return `Verified against recorded truth: ${input.pass}/${input.scored} recorded configs at or above the pass bar (${input.certified} certified), ${interaction}${prelude}.` + (unrecorded === null ? " Coverage denominator UNKNOWN (set predates lattice tracking): completeness is not established." : ` ${unrecorded} lattice configs are unrecorded and UNVERIFIED.`) + ` These numbers are claims: recompute them with \`tendril verify\` \u2014 certification authority lives in the CLI ruler, never in this file.`;
6083
6125
  }
6084
6126
  function cssProvenanceComment(input) {
6085
- const unrecorded = input.latticeConfigs === null ? "" : `; ${Math.max(0, input.latticeConfigs - input.scored)} lattice configs unverified`;
6127
+ const unrecorded = input.latticeConfigs === null ? "; coverage denominator unknown" : `; ${Math.max(0, input.latticeConfigs - input.scored)} lattice configs unverified`;
6086
6128
  return `/* tendril bundle v${BUNDLE_VERSION} \u2014 ${input.pass}/${input.scored} recorded configs \u2265 pass bar, ${input.certified} certified${unrecorded}. Non-authoritative claim AT STAMP TIME \u2014 any edit invalidates it; recompute with \`tendril verify\`. */`;
6087
6129
  }
6088
6130
  function hashRecordingSet(relPaths, readFile, sha256) {
@@ -6235,8 +6277,10 @@ function symbolsFromMetadataEnvelope(file, sourceFrame) {
6235
6277
  const next = node.type !== "COMPONENT" && node.name !== "" ? node.name : ancestor;
6236
6278
  for (const child of node.children) walk2(child, next);
6237
6279
  };
6238
- walk2(parseMetadataStructure(text), void 0);
6239
- return symbols;
6280
+ const forest = parseMetadataForest(text);
6281
+ for (const root of forest.roots) walk2(root, void 0);
6282
+ const seen = /* @__PURE__ */ new Set();
6283
+ return { symbols: symbols.filter((sym) => seen.has(sym.nodeId) ? false : (seen.add(sym.nodeId), true)), truncated: forest.truncated };
6240
6284
  }
6241
6285
  function instanceLeads(text) {
6242
6286
  const seen = /* @__PURE__ */ new Map();
@@ -6261,10 +6305,13 @@ function runRecordPlan(opts) {
6261
6305
  defaults[spec.slice(0, eq)] = spec.slice(eq + 1);
6262
6306
  }
6263
6307
  let symbols = [];
6308
+ let metadataTruncated = false;
6264
6309
  for (const spec of opts.metadataFiles) {
6265
6310
  const [file, frame] = spec.split("@");
6266
6311
  try {
6267
- symbols.push(...symbolsFromMetadataEnvelope(path24.resolve(file), frame));
6312
+ const parsed = symbolsFromMetadataEnvelope(path24.resolve(file), frame);
6313
+ symbols.push(...parsed.symbols);
6314
+ if (parsed.truncated) metadataTruncated = true;
6268
6315
  } catch (err) {
6269
6316
  fail(opts, ExitCode.InputValidation, {
6270
6317
  error: `could not read metadata envelope ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -6329,7 +6376,10 @@ function runRecordPlan(opts) {
6329
6376
  }
6330
6377
  }
6331
6378
  }
6332
- const { manifest, plan, resumed } = planSet(opts.setDir, opts.component, symbols, Object.keys(defaults).length > 0 ? { defaults } : {});
6379
+ if (metadataTruncated) {
6380
+ warn(opts, "get_metadata response appears TRUNCATED (unclosed structure) \u2014 the variant list below may be incomplete. Cross-check variantsFound against the variant count Figma shows for this component set; re-fetch the metadata if lower.");
6381
+ }
6382
+ const { manifest, plan, resumed, toppedUp } = planSet(opts.setDir, opts.component, symbols, { ...Object.keys(defaults).length > 0 ? { defaults } : {}, ...opts.sample === true ? { sample: true } : {} });
6333
6383
  const defaultReports = reportAxisDefaults(symbols, Object.keys(defaults).length > 0 ? defaults : void 0);
6334
6384
  const toConfirm = resumed ? [] : defaultReports.filter((r) => (r.rule === "non-interaction" || r.rule === "frequency") && r.domain.length > 1);
6335
6385
  const callLow = manifest.reps.length * 3;
@@ -6339,6 +6389,15 @@ function runRecordPlan(opts) {
6339
6389
  {
6340
6390
  resumed,
6341
6391
  reps: manifest.reps,
6392
+ // The honesty ledger: how many variants the metadata YIELDED vs
6393
+ // how many the queue records. An agent (or user) can hold
6394
+ // variantsFound against the count Figma's UI shows for the set —
6395
+ // the one check that catches parse/transfer losses this pipeline
6396
+ // cannot detect from the envelope alone.
6397
+ variantsFound: symbols.length,
6398
+ planMode: manifest.planMode ?? "sample",
6399
+ ...metadataTruncated ? { metadataTruncated: true } : {},
6400
+ ...toppedUp !== void 0 ? { toppedUp } : {},
6342
6401
  notRecorded: manifest.notRecorded ?? null,
6343
6402
  figmaCallEstimate: { reps: manifest.reps.length, calls: `~${callLow}\u2013${callHigh}` },
6344
6403
  ...toConfirm.length > 0 ? {
@@ -6354,8 +6413,18 @@ function runRecordPlan(opts) {
6354
6413
  },
6355
6414
  () => {
6356
6415
  if (resumed) {
6357
- process.stdout.write(`resumed existing plan (${manifest.reps.length} reps) \u2014 delete recording-set.json to re-plan
6416
+ if (opts.sample === true && (manifest.planMode ?? "sample") === "full") {
6417
+ process.stdout.write("NOTE: --sample has no effect on a resumed full-matrix set \u2014 delete recording-set.json to re-plan sampled (partial coverage is a deliberate choice)\n");
6418
+ }
6419
+ if (toppedUp !== void 0) {
6420
+ process.stdout.write(`resumed and TOPPED UP: ${toppedUp.length} lattice pose(s) this set was missing are now planned (full-matrix default reaches existing sets)
6421
+ `);
6422
+ for (const t of toppedUp) process.stdout.write(` added ${t.slug} (${t.nodeId})
6358
6423
  `);
6424
+ } else {
6425
+ process.stdout.write(`resumed existing plan (${manifest.reps.length} reps, ${manifest.planMode ?? "sample"} mode) \u2014 delete recording-set.json to re-plan
6426
+ `);
6427
+ }
6359
6428
  return;
6360
6429
  }
6361
6430
  for (const r of plan.reps) process.stdout.write(`planned ${r.slug.padEnd(24)} ${r.nodeId} (${r.tier})
@@ -6364,8 +6433,8 @@ function runRecordPlan(opts) {
6364
6433
  `);
6365
6434
  process.stdout.write(`estimated recording cost: ~${callLow}\u2013${callHigh} Figma calls for ${manifest.reps.length} reps
6366
6435
  `);
6367
- for (const q of toConfirm) {
6368
- process.stdout.write(`CONFIRM ${q.axis}: default resolved to "${q.value}" by heuristic (${q.rule}) \u2014 ask the user; change with --default before recording
6436
+ for (const q2 of toConfirm) {
6437
+ process.stdout.write(`CONFIRM ${q2.axis}: default resolved to "${q2.value}" by heuristic (${q2.rule}) \u2014 ask the user; change with --default before recording
6369
6438
  `);
6370
6439
  }
6371
6440
  }
@@ -6498,7 +6567,13 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
6498
6567
  async function runRecordIngest(opts) {
6499
6568
  let payload;
6500
6569
  try {
6501
- payload = opts.raw === true ? { content: [{ type: "text", text: readFileSync15(path24.resolve(opts.file), "utf8") }] } : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
6570
+ if (opts.rawParts === true) {
6571
+ const parts = JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
6572
+ if (!Array.isArray(parts) || parts.length === 0 || parts.some((p) => typeof p !== "string")) throw new Error("--raw-parts file must be a non-empty JSON array of strings");
6573
+ payload = { content: parts.map((text) => ({ type: "text", text })) };
6574
+ } else {
6575
+ payload = opts.raw === true ? { content: [{ type: "text", text: readFileSync15(path24.resolve(opts.file), "utf8") }] } : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
6576
+ }
6502
6577
  } catch (err) {
6503
6578
  fail(opts, ExitCode.InputValidation, {
6504
6579
  error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
@@ -6506,7 +6581,7 @@ async function runRecordIngest(opts) {
6506
6581
  remediation: "Pass the tool response saved verbatim: as JSON, or as raw text with --raw."
6507
6582
  });
6508
6583
  }
6509
- if (opts.raw === true && opts.tool === "get_screenshot") {
6584
+ if ((opts.raw === true || opts.rawParts === true) && opts.tool === "get_screenshot") {
6510
6585
  fail(opts, ExitCode.InputValidation, {
6511
6586
  error: "--raw is for text tool responses; screenshots are binary",
6512
6587
  code: "envelope-invalid",
@@ -7127,10 +7202,7 @@ function authorComponentApi(opts) {
7127
7202
  }
7128
7203
  const props = [];
7129
7204
  const forcedStates = [];
7130
- const propNameFor = (axis) => {
7131
- const name = camel(axis);
7132
- return RESERVED_PROPS.has(name.toLowerCase()) ? camel(`${opts.component} ${axis}`) : name;
7133
- };
7205
+ const propNameFor = (axis) => axisPropName(opts.component, axis);
7134
7206
  for (const key of axisKeys) {
7135
7207
  const domain = domains.get(key);
7136
7208
  const def = defaults.get(key);
@@ -7276,7 +7348,7 @@ function authorBehaviors(api) {
7276
7348
  return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
7277
7349
  }
7278
7350
  function envelopeText(file) {
7279
- return JSON.parse(readFileSync17(file, "utf8")).content[0]?.text ?? "";
7351
+ return envelopeFirstTextPart(JSON.parse(readFileSync17(file, "utf8")));
7280
7352
  }
7281
7353
  function recordedFontNeeds(setDir) {
7282
7354
  const byFamily = /* @__PURE__ */ new Map();
@@ -7398,6 +7470,7 @@ PAINT & PLATFORM TRAPS (each measured on a paid run; every one passed typecheck,
7398
7470
  - A native <dialog> carries user-agent padding: 1em. Override every side, or the root grows past its recorded box and every band inside shifts.
7399
7471
  - THE PAGE CANVAS IS NOT YOURS. Never paint the recording's page background into the component \u2014 no canvas-coloured plates across the root box, no square shadow spread carrying the canvas past the frame. The mount composites your render over the recorded canvas, so transparency wherever the recording shows canvas is both correct and scores correctly; the score report's canvasCoupling counts canvas pixels your render refuses to let the page repaint, and a component that paints the canvas is wrong on every real page.
7400
7472
  - TOKENS SCOPE TO YOUR ROOT CLASS, NEVER :root. Bundles compose on real pages: token names on :root collide across independently generated components and the last stylesheet loaded silently rewrites the others (measured: two colliding names flipped certified surfaces translucent). Declare every token under the component's root class.
7473
+ - WHEN TWO AXES BOTH CONTROL PAINT, THEY COMPOSE THROUGH CUSTOM PROPERTIES \u2014 one axis SETS variables, the other CONSUMES them. Direct paint rules on both axes have equal specificity, so source order silently drops one axis for exactly the crossed poses (measured: variant \xD7 tone \u2014 primary+critical rendered dark neutral instead of the recorded red; the sampled scorer never sees crossed poses, so nothing catches it but pixels in use).
7401
7474
 
7402
7475
  INTERACTION-READY BY DEFAULT: components are real controls, never static lookalikes \u2014 use the native element matching the archetype (button, input[type=radio|checkbox], select\u2026), real handlers, keyboard operability, and real state (checked/disabled/:hover/:focus-visible). Behavioral checks fail statues. (The forcing-hook rule is specified once, in the prescribed API below.) Your file runs in a bare browser bundle: it must be fully self-contained. IMPORT EVERY React API you use explicitly \u2014 e.g. import { useState, useRef, useEffect } from "react" \u2014 nothing is provided globally; a missing import crashes the mount and every config scores 0.
7403
7476
 
@@ -7412,7 +7485,7 @@ ${PRELUDE_CONTRACT}
7412
7485
  ${opts.colorScheme === void 0 ? "" : `
7413
7486
  RESOLVED FOR THIS RECORDING: it is ${opts.colorScheme}-mode truth, so pin color-scheme: ${opts.colorScheme} on the root. A conditional rule you have to resolve yourself is a rule you will get wrong \u2014 this is the answer, not the question.`}`;
7414
7487
  }
7415
- var PoseCompletenessError, kebab3, camel, pascal, RESERVED_PROPS, isStateAxis, symbolName, STYLE_WEIGHTS2, styleWeight;
7488
+ var PoseCompletenessError, kebab3, camel, pascal, RESERVED_PROPS, axisPropName, isStateAxis, symbolName, STYLE_WEIGHTS2, styleWeight;
7416
7489
  var init_brief = __esm({
7417
7490
  "packages/generate/src/brief.ts"() {
7418
7491
  "use strict";
@@ -7436,6 +7509,10 @@ var init_brief = __esm({
7436
7509
  return c === "" ? c : c[0].toUpperCase() + c.slice(1);
7437
7510
  };
7438
7511
  RESERVED_PROPS = /* @__PURE__ */ new Set(["style", "classname", "children", "key", "ref", "id"]);
7512
+ axisPropName = (component, axis) => {
7513
+ const name = camel(axis);
7514
+ return RESERVED_PROPS.has(name.toLowerCase()) ? camel(`${component} ${axis}`) : name;
7515
+ };
7439
7516
  isStateAxis = (axis) => kebab3(axis) === "state";
7440
7517
  symbolName = (metaText) => {
7441
7518
  const raw = /name="([^"]*)"/.exec(metaText)?.[1];
@@ -7467,7 +7544,8 @@ var init_brief = __esm({
7467
7544
  import { existsSync as existsSync21, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
7468
7545
  import path27 from "node:path";
7469
7546
  function repText(set, rep, tool) {
7470
- return JSON.parse(readFileSync18(path27.join(set, rep, `${tool}.json`), "utf8")).content[0]?.text ?? "";
7547
+ const env = JSON.parse(readFileSync18(path27.join(set, rep, `${tool}.json`), "utf8"));
7548
+ return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
7471
7549
  }
7472
7550
  function stripFigmaInstructions(emission) {
7473
7551
  const STYLE_FACTS = "These styles are contained in the design:";
@@ -7528,7 +7606,7 @@ function buildSegments(task, mode = "fenced") {
7528
7606
  const SET = task.set;
7529
7607
  let rawDefs = {};
7530
7608
  if (existsSync21(path27.join(SET, "get_variable_defs.json"))) {
7531
- const text = JSON.parse(readFileSync18(path27.join(SET, "get_variable_defs.json"), "utf8")).content[0]?.text ?? "{}";
7609
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync18(path27.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
7532
7610
  try {
7533
7611
  rawDefs = JSON.parse(text);
7534
7612
  } catch {
@@ -7537,7 +7615,7 @@ function buildSegments(task, mode = "fenced") {
7537
7615
  for (const cfg of task.configs) {
7538
7616
  const f = path27.join(SET, cfg.rep, "get_variable_defs.json");
7539
7617
  if (!existsSync21(f)) continue;
7540
- const text = JSON.parse(readFileSync18(f, "utf8")).content[0]?.text ?? "{}";
7618
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync18(f, "utf8"))) || "{}";
7541
7619
  try {
7542
7620
  for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
7543
7621
  } catch {
@@ -7546,7 +7624,7 @@ function buildSegments(task, mode = "fenced") {
7546
7624
  }
7547
7625
  const emissionTexts = task.configs.map((cfg) => {
7548
7626
  const f = path27.join(SET, cfg.rep, "get_design_context.json");
7549
- return existsSync21(f) ? JSON.parse(readFileSync18(f, "utf8")).content[0]?.text ?? "" : "";
7627
+ return existsSync21(f) ? envelopeFirstTextPart(JSON.parse(readFileSync18(f, "utf8"))) : "";
7550
7628
  });
7551
7629
  const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
7552
7630
  const defs = JSON.stringify(map, null, 1);
@@ -7596,6 +7674,7 @@ var cssIdent;
7596
7674
  var init_segments = __esm({
7597
7675
  "packages/generate/src/segments.ts"() {
7598
7676
  "use strict";
7677
+ init_src();
7599
7678
  cssIdent = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7600
7679
  }
7601
7680
  });
@@ -7720,7 +7799,7 @@ function countLatticeSymbols(setDir) {
7720
7799
  if (files.length === 0) return null;
7721
7800
  let count = 0;
7722
7801
  for (const f of files) {
7723
- const text = JSON.parse(readFileSync19(f, "utf8")).content[0]?.text ?? "";
7802
+ const text = envelopeTextContent(JSON.parse(readFileSync19(f, "utf8")));
7724
7803
  count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
7725
7804
  }
7726
7805
  return count > 0 ? count : null;
@@ -7836,6 +7915,7 @@ var init_bundle_emit = __esm({
7836
7915
  "packages/generate/src/bundle-emit.ts"() {
7837
7916
  "use strict";
7838
7917
  init_src6();
7918
+ init_src();
7839
7919
  init_src4();
7840
7920
  BARS = {
7841
7921
  pass: { sim: 0.95, ink: 0.95 },
@@ -8046,7 +8126,18 @@ function taskFromManifest(opts, manifest, setDir) {
8046
8126
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
8047
8127
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
8048
8128
  const registry = Object.values(TASKS).find((t) => path30.resolve(t.set) === path30.resolve(setDir));
8049
- const authored = registry === void 0 ? authorTaskFromSet(setDir) : void 0;
8129
+ const authored = (() => {
8130
+ if (registry !== void 0) return void 0;
8131
+ try {
8132
+ return authorTaskFromSet(setDir);
8133
+ } catch (err) {
8134
+ fail(opts, ExitCode.InputValidation, {
8135
+ error: `cannot author the verification task from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
8136
+ code: "AUTHORING_FAILED",
8137
+ remediation: "Fix the recording set (duplicate/colliding poses fail authoring by design \u2014 check `tendril record status` and the set's variant names)."
8138
+ });
8139
+ }
8140
+ })();
8050
8141
  const behaviorSource = registry ?? authored.task;
8051
8142
  const task = {
8052
8143
  set: setDir,
@@ -8228,6 +8319,23 @@ async function runVerify(opts) {
8228
8319
  scoredConfigs: statuses.length,
8229
8320
  certified,
8230
8321
  pass: statuses.filter((s) => s.status !== "fail").length,
8322
+ // LATTICE HONESTY (adversarial review, 2026-08-10): "verified"
8323
+ // must never quietly mean "the recorded subset matched". The
8324
+ // denominator is the set's own lattice; unrecorded poses are
8325
+ // named in the report and the human output, on the trust anchor
8326
+ // itself — not only in a long-gone plan output.
8327
+ ...(() => {
8328
+ try {
8329
+ const setManifest = loadManifest(task.set);
8330
+ const lattice = setManifest.latticeNames?.length;
8331
+ return {
8332
+ ...lattice !== void 0 ? { latticeConfigs: lattice, unrecordedConfigs: Math.max(0, lattice - statuses.length) } : { latticeConfigs: null },
8333
+ ...setManifest.notRecorded !== void 0 && setManifest.notRecorded !== "" ? { notRecorded: setManifest.notRecorded } : {}
8334
+ };
8335
+ } catch {
8336
+ return { latticeConfigs: null };
8337
+ }
8338
+ })(),
8231
8339
  // Prelude checks are page-level style hygiene and say nothing
8232
8340
  // about whether the component WORKS. Reporting one merged
8233
8341
  // "behaviors 6/6" made a component with zero interaction coverage
@@ -8332,6 +8440,18 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
8332
8440
  `
8333
8441
  );
8334
8442
  }
8443
+ {
8444
+ const cov = report.coverage;
8445
+ if (typeof cov.latticeConfigs === "number" && (cov.unrecordedConfigs ?? 0) > 0) {
8446
+ process.stdout.write(
8447
+ `INCOMPLETE ${cov.unrecordedConfigs} of ${cov.latticeConfigs} lattice poses were NEVER RECORDED \u2014 nothing verifies them; any implementation of those poses is inference, not verified truth. Re-plan the set (full matrix is the default) and record the missing poses.
8448
+ `
8449
+ );
8450
+ } else if (cov.latticeConfigs === null) {
8451
+ process.stdout.write(`COVERAGE denominator unknown (set predates lattice tracking) \u2014 scored configs are verified; completeness is not established
8452
+ `);
8453
+ }
8454
+ }
8335
8455
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
8336
8456
  `);
8337
8457
  process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
@@ -8410,6 +8530,7 @@ function runEngineBrief(opts) {
8410
8530
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
8411
8531
 
8412
8532
  === UNVERIFIED SURFACE (recorded-set disclosure \u2014 these poses were never recorded; nothing verifies them) ===
8533
+ 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.
8413
8534
  ${notRecorded}` : "";
8414
8535
  let fontProvisioning;
8415
8536
  if (existsSync25(manifestPath2)) {
@@ -8449,7 +8570,7 @@ ${segments}`;
8449
8570
  ...notRecorded !== void 0 && notRecorded !== "" ? { notRecorded } : {},
8450
8571
  ...fontProvisioning !== void 0 ? { fontProvisioning } : {},
8451
8572
  modelSelection: {
8452
- instruction: "The model declaration is MECHANICAL: `engine score` refuses to run without --model. When a user is present, ask BEFORE reading the payload, using the template below verbatim where your host renders option dialogs. The audience is non-technical: plain cost/quality language, no model knowledge assumed. In a non-interactive session pick the balanced tier, state the reason in your report, and declare it \u2014 a silent default is not possible.",
8573
+ instruction: "The model declaration is MECHANICAL: `engine score` refuses to run without --model. ONLY ask when the answer can take effect \u2014 i.e. you can delegate implementation to an agent running the chosen model; if you cannot delegate in this session, skip the question, build as yourself, and declare your own model honestly (a question whose answer changes nothing wastes the user's trust \u2014 measured, second Windows run). When you do ask: BEFORE reading the payload, using the template below verbatim where your host renders option dialogs. The audience is non-technical: plain cost/quality language, no model knowledge assumed. In a non-interactive session pick the balanced tier, state the reason in your report, and declare it \u2014 a silent default is not possible.",
8453
8574
  questionTemplate: {
8454
8575
  prompt: "Which model should build this component? Every choice is scored by the same independent measurement \u2014 a cheaper model may need more attempts, but it can never ship a lower-quality certified result.",
8455
8576
  options: [
@@ -8495,6 +8616,30 @@ async function runEngineScore(opts) {
8495
8616
  remediation: fontsUnprovenRemediation(task.set)
8496
8617
  });
8497
8618
  }
8619
+ if (opts.rebind !== true && existsSync25(path31.join(candidateDir, "component.json"))) {
8620
+ const prior = (() => {
8621
+ try {
8622
+ const read = readBundleManifest(readFileSync22(path31.join(candidateDir, "component.json"), "utf8"));
8623
+ return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
8624
+ } catch {
8625
+ return { unreadable: true };
8626
+ }
8627
+ })();
8628
+ if (prior.unreadable === true) {
8629
+ fail(opts, ExitCode.InputValidation, {
8630
+ error: "this bundle carries a component.json whose recording-set binding cannot be read \u2014 refusing to overwrite an unverifiable identity",
8631
+ code: "set-binding-unreadable",
8632
+ remediation: "Fix or remove the bundle's component.json, or pass --rebind to overwrite it deliberately."
8633
+ });
8634
+ }
8635
+ if (prior.hash !== recordingSetHash(task.set, task.configs)) {
8636
+ fail(opts, ExitCode.InputValidation, {
8637
+ error: `this bundle is bound to recording set "${prior.path}" (content ${prior.hash.slice(0, 12)}\u2026), which does not match "${opts.taskOrSet}" as it exists now \u2014 scoring would rewrite the bundle's verification identity and its evidence images`,
8638
+ code: "set-rebind-refused",
8639
+ remediation: "If this is the SAME set and it simply gained recordings since the last score, pass --rebind once to re-stamp. If it is a DIFFERENT set, score against the bundle's own set \u2014 rebinding voids its previous verification claims."
8640
+ });
8641
+ }
8642
+ }
8498
8643
  const bar = BARS3[opts.bar];
8499
8644
  const evidenceDir = path31.join(candidateDir, "verify-evidence");
8500
8645
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
@@ -8567,6 +8712,7 @@ var init_engine2 = __esm({
8567
8712
  "use strict";
8568
8713
  init_src3();
8569
8714
  init_src7();
8715
+ init_src6();
8570
8716
  init_environment();
8571
8717
  init_font_guidance();
8572
8718
  init_src4();
@@ -8579,6 +8725,182 @@ var init_engine2 = __esm({
8579
8725
  }
8580
8726
  });
8581
8727
 
8728
+ // packages/cli/src/commands/codeconnect.ts
8729
+ var codeconnect_exports = {};
8730
+ __export(codeconnect_exports, {
8731
+ runCodeConnect: () => runCodeConnect
8732
+ });
8733
+ import { existsSync as existsSync26, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
8734
+ import path32 from "node:path";
8735
+ function runCodeConnect(opts) {
8736
+ const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
8737
+ const bundleDir = path32.resolve(callerCwd, opts.bundleDir);
8738
+ let url;
8739
+ try {
8740
+ url = new URL(opts.figmaUrl);
8741
+ } catch {
8742
+ url = new URL("invalid://x");
8743
+ }
8744
+ if (/[\u0000-\u001f\u2028\u2029]/.test(opts.figmaUrl) || !(url.hostname === "figma.com" || url.hostname.endsWith(".figma.com")) || !/\/design\//.test(url.pathname) || url.searchParams.get("node-id") === null) {
8745
+ fail(opts, ExitCode.InputValidation, {
8746
+ error: `--figma-url must be a clean figma.com /design/ URL carrying the COMPONENT SET's node-id, got: ${JSON.stringify(opts.figmaUrl)}`,
8747
+ code: "codeconnect-bad-url",
8748
+ remediation: "In Figma, select the component set and copy its link (Copy link to selection)."
8749
+ });
8750
+ }
8751
+ let manifest;
8752
+ try {
8753
+ const read = readBundleManifest(readFileSync23(path32.join(bundleDir, "component.json"), "utf8"));
8754
+ if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
8755
+ manifest = read.manifest;
8756
+ } catch (err) {
8757
+ fail(opts, ExitCode.InputValidation, {
8758
+ error: `not a Tendril bundle (component.json unreadable): ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
8759
+ code: "codeconnect-no-bundle",
8760
+ remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
8761
+ });
8762
+ }
8763
+ const setDir = path32.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
8764
+ if (!existsSync26(path32.join(setDir, "recording-set.json"))) {
8765
+ fail(opts, ExitCode.InputValidation, {
8766
+ error: `recording set not found at ${setDir}`,
8767
+ code: "codeconnect-no-set",
8768
+ remediation: "Pass --set <recording-dir> (the bundle's provenance path did not resolve from this directory)."
8769
+ });
8770
+ }
8771
+ let authored;
8772
+ try {
8773
+ authored = authorTaskFromSet(setDir);
8774
+ } catch (err) {
8775
+ fail(opts, ExitCode.InputValidation, {
8776
+ error: `cannot author the API from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
8777
+ code: "codeconnect-authoring-failed",
8778
+ remediation: "The set must be a complete protocol recording (tendril record status)."
8779
+ });
8780
+ }
8781
+ const api = authored.api;
8782
+ const component = api.component;
8783
+ const recManifest = loadManifest(setDir);
8784
+ const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
8785
+ const meta = path32.join(setDir, r.slug, "get_metadata.json");
8786
+ if (!existsSync26(meta)) return void 0;
8787
+ try {
8788
+ return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync23(meta, "utf8"))))?.[1];
8789
+ } catch {
8790
+ return void 0;
8791
+ }
8792
+ }).filter((n) => n !== void 0);
8793
+ const axisDomains = mergeVariantAxes(poseNames) ?? {};
8794
+ if (Object.keys(axisDomains).length === 0 && api.props.length > 0) {
8795
+ fail(opts, ExitCode.InputValidation, {
8796
+ error: "no variant axes recoverable from the recording manifest (no latticeNames)",
8797
+ code: "codeconnect-no-axes",
8798
+ remediation: "Re-plan the set with a current Tendril (lattice names persist in the manifest), or record the set fresh."
8799
+ });
8800
+ }
8801
+ if (recordingSetHash(setDir, authored.task.configs) !== manifest.provenance.recordingSet.hash) {
8802
+ fail(opts, ExitCode.InputValidation, {
8803
+ error: "the recording set's content no longer matches the hash this bundle was scored against \u2014 its verification claims describe a different recording",
8804
+ code: "codeconnect-set-drift",
8805
+ remediation: "Re-run `tendril verify` (or engine score) against the current set, then re-emit."
8806
+ });
8807
+ }
8808
+ const boolProps = api.props.filter((p) => p.kind === "boolean");
8809
+ const pixelOnly = [];
8810
+ const axisLines = [];
8811
+ const fragmentVars = [];
8812
+ for (const [axis, values] of Object.entries(axisDomains)) {
8813
+ const varName = `frag${axisLines.length}`;
8814
+ const entries = [];
8815
+ const owner = api.props.find((p) => p.name === axisPropName(component, axis));
8816
+ const axisDefault = resolveAxisDefault(values, values, recManifest.defaults?.[axis]);
8817
+ for (const value of values) {
8818
+ const kv = kebab4(value);
8819
+ let fragment = null;
8820
+ if (owner?.kind === "union" && (owner.values ?? []).includes(kv)) {
8821
+ fragment = owner.default === kv ? "" : ` ${owner.name}="${kv}"`;
8822
+ } else if (owner?.kind === "boolean") {
8823
+ fragment = ["true", "on", "yes"].includes(kv) ? ` ${owner.name}` : "";
8824
+ } else if (api.forcedStates.includes(kv)) {
8825
+ fragment = ` data-tendril-state="${kv}"`;
8826
+ } else if (boolProps.some((p) => p.name === kv || kebab4(p.name) === kv)) {
8827
+ fragment = ` ${boolProps.find((p) => p.name === kv || kebab4(p.name) === kv).name}`;
8828
+ } else if (value === axisDefault) {
8829
+ fragment = "";
8830
+ } else if (api.unmappedInteractionEvidence.some((e) => kebab4(e) === kv || kebab4(e) === kebab4(`${axis} ${value}`))) {
8831
+ pixelOnly.push(`${axis}=${value}`);
8832
+ fragment = "";
8833
+ }
8834
+ if (fragment === null) {
8835
+ fail(opts, ExitCode.InputValidation, {
8836
+ error: `axis "${axis}" value "${value}" maps to nothing in the authored API \u2014 an unmapped value silently breaks the Dev Mode snippet`,
8837
+ code: "codeconnect-unmapped-value",
8838
+ remediation: "Record the missing pose (the full matrix is the default plan) so the authored API covers the full lattice, then re-emit."
8839
+ });
8840
+ }
8841
+ entries.push(`${q(value)}: ${q(fragment)}`);
8842
+ }
8843
+ axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
8844
+ fragmentVars.push(varName);
8845
+ }
8846
+ const entryRel = path32.relative(callerCwd, path32.join(bundleDir, manifest.entry));
8847
+ const trust = manifest.trustStatement.split("\n")[0] ?? "";
8848
+ const lines = [
8849
+ `// url=${opts.figmaUrl}`,
8850
+ `// source=${entryRel}`,
8851
+ `// component=${component}`,
8852
+ "// Generated by Tendril. Claims below are the bundle's self-reported verification \u2014 recompute them free and offline with: tendril verify",
8853
+ `// ${trust}`,
8854
+ ...pixelOnly.length > 0 ? [`// PIXEL-VERIFIED ONLY (no operable mapping): ${pixelOnly.join(", ")}`] : [],
8855
+ `import figma from 'figma'`,
8856
+ ``,
8857
+ `const instance = figma.selectedInstance`,
8858
+ ...axisLines,
8859
+ ``,
8860
+ `export default {`,
8861
+ ` example: figma.code\`<${component}${fragmentVars.map((v) => `\${${v}}`).join("")} />\`,`,
8862
+ ` imports: [${q(`import { ${component} } from "./${manifest.entry.replace(/\.tsx?$/, "")}" /* adjust to your project import path */`)}],`,
8863
+ ` metadata: { nestable: true },`,
8864
+ `}`,
8865
+ ``
8866
+ ].join("\n");
8867
+ const outFile = path32.resolve(callerCwd, opts.out ?? path32.join(bundleDir, `${component}.figma.ts`));
8868
+ writeFileSync12(outFile, lines);
8869
+ emitData(
8870
+ opts,
8871
+ {
8872
+ file: outFile,
8873
+ component,
8874
+ axes: Object.keys(axisDomains),
8875
+ ...pixelOnly.length > 0 ? { pixelOnly } : {},
8876
+ publish: {
8877
+ cli: `npx @figma/code-connect connect publish --file ${outFile} (requires a Figma Organization/Enterprise plan and a token with Code Connect Write scope)`,
8878
+ note: "Publishing is YOUR action with YOUR Figma access \u2014 Tendril never publishes to Figma."
8879
+ }
8880
+ },
8881
+ () => {
8882
+ process.stdout.write(`code connect template: ${outFile}
8883
+ `);
8884
+ if (pixelOnly.length > 0) warn(opts, `pixel-only poses mapped to the base pose in snippets: ${pixelOnly.join(", ")}`);
8885
+ process.stdout.write(`publish (your Figma token, Org/Enterprise plan): npx @figma/code-connect connect publish
8886
+ `);
8887
+ }
8888
+ );
8889
+ }
8890
+ var kebab4, q;
8891
+ var init_codeconnect = __esm({
8892
+ "packages/cli/src/commands/codeconnect.ts"() {
8893
+ "use strict";
8894
+ init_src3();
8895
+ init_src7();
8896
+ init_src();
8897
+ init_src6();
8898
+ init_output();
8899
+ kebab4 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
8900
+ q = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
8901
+ }
8902
+ });
8903
+
8582
8904
  // packages/cli/src/commands/generate-route.ts
8583
8905
  var generate_route_exports = {};
8584
8906
  __export(generate_route_exports, {
@@ -8603,17 +8925,17 @@ __export(generate_recorded_exports, {
8603
8925
  runGenerateRecorded: () => runGenerateRecorded
8604
8926
  });
8605
8927
  import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
8606
- import { existsSync as existsSync26 } from "node:fs";
8607
- import path32 from "node:path";
8928
+ import { existsSync as existsSync27, readFileSync as readFileSync24 } from "node:fs";
8929
+ import path33 from "node:path";
8608
8930
  async function runGenerateRecorded(opts) {
8609
8931
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
8610
- const outDirAbs = path32.resolve(callerCwd, opts.out);
8611
- const recordedAsPath = path32.resolve(callerCwd, opts.recorded);
8932
+ const outDirAbs = path33.resolve(callerCwd, opts.out);
8933
+ const recordedAsPath = path33.resolve(callerCwd, opts.recorded);
8612
8934
  let task;
8613
8935
  let taskName;
8614
8936
  let authoredApi;
8615
8937
  let composition;
8616
- const isSet = existsSync26(path32.join(recordedAsPath, "recording-set.json"));
8938
+ const isSet = existsSync27(path33.join(recordedAsPath, "recording-set.json"));
8617
8939
  const registry = TASKS[opts.recorded];
8618
8940
  if (registry !== void 0 && !isSet) {
8619
8941
  task = registry;
@@ -8622,7 +8944,7 @@ async function runGenerateRecorded(opts) {
8622
8944
  try {
8623
8945
  const authored = authorTaskFromSet(recordedAsPath);
8624
8946
  task = authored.task;
8625
- taskName = path32.basename(recordedAsPath);
8947
+ taskName = path33.basename(recordedAsPath);
8626
8948
  authoredApi = authored.api;
8627
8949
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
8628
8950
  if (roles.success) composition = roles.data;
@@ -8649,7 +8971,7 @@ async function runGenerateRecorded(opts) {
8649
8971
  });
8650
8972
  }
8651
8973
  const missing = task.configs.filter(
8652
- (c) => !existsSync26(path32.join(task.set, c.rep, "get_screenshot.json")) || !existsSync26(path32.join(task.set, c.rep, "get_metadata.json")) || !existsSync26(path32.join(task.set, c.rep, "get_design_context.json"))
8974
+ (c) => !existsSync27(path33.join(task.set, c.rep, "get_screenshot.json")) || !existsSync27(path33.join(task.set, c.rep, "get_metadata.json")) || !existsSync27(path33.join(task.set, c.rep, "get_design_context.json"))
8653
8975
  );
8654
8976
  if (missing.length > 0) {
8655
8977
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -8719,8 +9041,8 @@ async function runGenerateRecorded(opts) {
8719
9041
  ` : `${line}
8720
9042
  `);
8721
9043
  if (opts.dryRun) {
8722
- emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path32.join(outDirAbs, taskName) }, () => {
8723
- process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path32.join(outDirAbs, taskName)})
9044
+ emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path33.join(outDirAbs, taskName) }, () => {
9045
+ process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path33.join(outDirAbs, taskName)})
8724
9046
  `);
8725
9047
  });
8726
9048
  return;
@@ -8743,7 +9065,21 @@ async function runGenerateRecorded(opts) {
8743
9065
  });
8744
9066
  }
8745
9067
  }
8746
- const bundleDir = path32.join(outDirAbs, taskName);
9068
+ const bundleDir = path33.join(outDirAbs, taskName);
9069
+ if (existsSync27(path33.join(bundleDir, "component.json"))) {
9070
+ try {
9071
+ const prior = readBundleManifest(readFileSync24(path33.join(bundleDir, "component.json"), "utf8")).manifest;
9072
+ if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
9073
+ fail(opts, ExitCode.InputValidation, {
9074
+ 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`,
9075
+ code: "set-rebind-refused",
9076
+ remediation: "Point --out at a fresh directory, or delete the stale bundle deliberately."
9077
+ });
9078
+ }
9079
+ } catch (err) {
9080
+ if (err.code === void 0) throw err;
9081
+ }
9082
+ }
8747
9083
  const result = await runEngineLoop({
8748
9084
  engine,
8749
9085
  segments,
@@ -9841,7 +10177,7 @@ function buildProgram() {
9841
10177
  await runDoctor({ ...flags, mcpUrl: local["mcpUrl"] });
9842
10178
  });
9843
10179
  const record = program.command("record").description("Agent-driven recording protocol: plan the queue, get instructions, ingest verbatim envelopes, resume from disk.");
9844
- record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").requiredOption("--metadata <file...>", "verbatim get_metadata envelope(s), optionally <file>@<frameId>").option("--default <axis=value...>", "explicit axis default, e.g. --default State=Rest (repeatable; persisted to the manifest; may re-plan an unrecorded set)").option("--component-set <name>", "when the metadata holds several component sets, record only this one (name = the set label in the plan error)").action(async (_o, cmd) => {
10180
+ record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").requiredOption("--metadata <file...>", "verbatim get_metadata envelope(s), optionally <file>@<frameId>").option("--default <axis=value...>", "explicit axis default, e.g. --default State=Rest (repeatable; persisted to the manifest; may re-plan an unrecorded set)").option("--component-set <name>", "when the metadata holds several component sets, record only this one (name = the set label in the plan error)").option("--sample", "cost sampling: anchor + one-factor + conflict crosses only (the FULL variant matrix is the default; sampling is blind to multi-axis interactions)").action(async (_o, cmd) => {
9845
10181
  const flags = globalFlags(cmd.parent.parent);
9846
10182
  const local = cmd.opts();
9847
10183
  const { runRecordPlan: runRecordPlan2 } = await Promise.resolve().then(() => (init_record(), record_exports));
@@ -9850,6 +10186,7 @@ function buildProgram() {
9850
10186
  setDir: local["set"],
9851
10187
  component: local["component"],
9852
10188
  metadataFiles: local["metadata"],
10189
+ sample: local["sample"],
9853
10190
  ...local["default"] !== void 0 ? { defaultSpecs: local["default"] } : {},
9854
10191
  ...local["componentSet"] !== void 0 ? { componentSet: local["componentSet"] } : {}
9855
10192
  });
@@ -9859,11 +10196,11 @@ function buildProgram() {
9859
10196
  const { runRecordNext: runRecordNext2 } = await Promise.resolve().then(() => (init_record(), record_exports));
9860
10197
  runRecordNext2({ ...flags, setDir: cmd.opts()["set"] });
9861
10198
  });
9862
- record.command("ingest").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").requiredOption("--tool <tool>", "get_design_context | get_metadata | get_screenshot | get_variable_defs").requiredOption("--file <file>", "verbatim envelope JSON (or raw response text with --raw)").option("--raw", "the file holds the tool response TEXT verbatim; the CLI builds the envelope").action(async (_o, cmd) => {
10199
+ record.command("ingest").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").requiredOption("--tool <tool>", "get_design_context | get_metadata | get_screenshot | get_variable_defs").requiredOption("--file <file>", "verbatim envelope JSON (or raw response text with --raw)").option("--raw", "the file holds the tool response TEXT verbatim; the CLI builds the envelope").option("--raw-parts", "the file holds a JSON array of response block texts (multi-block transport); each becomes a content part verbatim").action(async (_o, cmd) => {
9863
10200
  const flags = globalFlags(cmd.parent.parent);
9864
10201
  const local = cmd.opts();
9865
10202
  const { runRecordIngest: runRecordIngest2 } = await Promise.resolve().then(() => (init_record(), record_exports));
9866
- await runRecordIngest2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], file: local["file"], raw: local["raw"] });
10203
+ await runRecordIngest2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], file: local["file"], raw: local["raw"], rawParts: local["rawParts"] });
9867
10204
  });
9868
10205
  record.command("fetch").description("Download a Figma asset URL straight to disk and ingest it \u2014 no shell, no model in the byte path.").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").requiredOption("--tool <tool>", "get_screenshot").requiredOption("--url <url>", "image_url from the Figma tool response, verbatim").action(async (_o, cmd) => {
9869
10206
  const flags = globalFlags(cmd.parent.parent);
@@ -9940,7 +10277,7 @@ function buildProgram() {
9940
10277
  ...local["model"] !== void 0 ? { model: local["model"] } : {}
9941
10278
  });
9942
10279
  });
9943
- engine.command("score").argument("<taskOrSet>", "reference task name or recording-set directory").argument("<candidateDir>", "directory containing the proposed bundle files").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--host <name>", "self-reported host identity (recorded in provenance, labeled self-reported)").requiredOption("--model <id>", "proposer model, self-reported \u2014 required; a score without a declared model is not accepted").action(async (taskOrSet, candidateDir, _o, cmd) => {
10280
+ engine.command("score").argument("<taskOrSet>", "reference task name or recording-set directory").argument("<candidateDir>", "directory containing the proposed bundle files").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--host <name>", "self-reported host identity (recorded in provenance, labeled self-reported)").requiredOption("--model <id>", "proposer model, self-reported \u2014 required; a score without a declared model is not accepted").option("--rebind", "explicitly re-bind an already-bound bundle to a different recording set (refused otherwise \u2014 rebinding rewrites the bundle's verification identity)").action(async (taskOrSet, candidateDir, _o, cmd) => {
9944
10281
  const flags = globalFlags(cmd.parent.parent);
9945
10282
  const local = cmd.opts();
9946
10283
  const { runEngineScore: runEngineScore2 } = await Promise.resolve().then(() => (init_engine2(), engine_exports));
@@ -9950,7 +10287,20 @@ function buildProgram() {
9950
10287
  candidateDir,
9951
10288
  bar: local["bar"] === "cert" ? "cert" : "pass",
9952
10289
  ...local["host"] !== void 0 ? { host: local["host"] } : {},
9953
- model: local["model"]
10290
+ model: local["model"],
10291
+ rebind: local["rebind"]
10292
+ });
10293
+ });
10294
+ program.command("codeconnect").description("Emit a Figma Code Connect template (.figma.ts) for a certified bundle \u2014 every variant\u2192prop mapping from recorded truth, stamped with the trust statement. Publishing stays yours (figma connect publish; Org/Enterprise plan).").argument("<bundleDir>", "bundle directory (must carry component.json)").requiredOption("--figma-url <url>", "figma.com /design/ URL of the COMPONENT SET (Copy link to selection)").option("--set <dir>", "recording set override (default: the bundle's provenance path)").option("--out <file>", "output file (default: <bundle>/<Component>.figma.ts)").action(async (bundleDir, _o, cmd) => {
10295
+ const flags = globalFlags(cmd.parent);
10296
+ const local = cmd.opts();
10297
+ const { runCodeConnect: runCodeConnect2 } = await Promise.resolve().then(() => (init_codeconnect(), codeconnect_exports));
10298
+ runCodeConnect2({
10299
+ ...flags,
10300
+ bundleDir,
10301
+ figmaUrl: local["figmaUrl"],
10302
+ ...local["set"] !== void 0 ? { set: local["set"] } : {},
10303
+ ...local["out"] !== void 0 ? { out: local["out"] } : {}
9954
10304
  });
9955
10305
  });
9956
10306
  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").action(async (bundleDir, _opts, cmd) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tendrilapp/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Figma design systems → verified React components. CLI ruler + MCP server.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",