@tendrilapp/cli 0.1.6 → 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.
@@ -158,7 +158,8 @@ var TOOLS = [
158
158
  candidateDir: str("directory containing the proposed bundle files"),
159
159
  bar: optStr("pass (default) or cert"),
160
160
  host: optStr("your host identity (e.g. claude-code, cursor, codex) \u2014 recorded as self-reported provenance"),
161
- 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")
162
163
  }),
163
164
  argv: (i) => [
164
165
  "engine",
@@ -168,7 +169,26 @@ var TOOLS = [
168
169
  ...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
169
170
  ...i["host"] !== void 0 ? ["--host", i["host"]] : [],
170
171
  "--model",
171
- 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"]] : []
172
192
  ]
173
193
  },
174
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 });
@@ -1071,6 +1101,10 @@ var init_session = __esm({
1071
1101
  * domains: the API must cover the lattice even where only a subset
1072
1102
  * is recorded. */
1073
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(),
1074
1108
  roles: z4.unknown().optional()
1075
1109
  });
1076
1110
  manifestPath = (setDir) => path.join(setDir, "recording-set.json");
@@ -1178,8 +1212,8 @@ var init_src = __esm({
1178
1212
  function variableNameToPath(name) {
1179
1213
  return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
1180
1214
  }
1181
- function tokenPathToCssVar(path33) {
1182
- return `--${path33.join("-")}`;
1215
+ function tokenPathToCssVar(path34) {
1216
+ return `--${path34.join("-")}`;
1183
1217
  }
1184
1218
  function toDtcgToken(variable, defaultMode) {
1185
1219
  const modes = Object.keys(variable.valuesByMode);
@@ -1223,11 +1257,11 @@ function toDtcgToken(variable, defaultMode) {
1223
1257
  }
1224
1258
  function mapVariablesToDtcg(variables, defaultMode = "light") {
1225
1259
  const entries = variables.map((variable) => {
1226
- const path33 = variableNameToPath(variable.name);
1227
- if (path33.length === 0) {
1260
+ const path34 = variableNameToPath(variable.name);
1261
+ if (path34.length === 0) {
1228
1262
  throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
1229
1263
  }
1230
- return { variable, path: path33 };
1264
+ return { variable, path: path34 };
1231
1265
  });
1232
1266
  const groupPrefixes = /* @__PURE__ */ new Set();
1233
1267
  for (const e of entries) {
@@ -1248,21 +1282,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
1248
1282
  }
1249
1283
  const tokens = {};
1250
1284
  const flat = [];
1251
- for (const { variable, path: path33 } of entries) {
1285
+ for (const { variable, path: path34 } of entries) {
1252
1286
  const token = toDtcgToken(variable, defaultMode);
1253
1287
  let group = tokens;
1254
- for (const segment of path33.slice(0, -1)) {
1288
+ for (const segment of path34.slice(0, -1)) {
1255
1289
  const existing = group[segment];
1256
1290
  group = existing ?? (group[segment] = {});
1257
1291
  }
1258
- const leaf = path33[path33.length - 1];
1292
+ const leaf = path34[path34.length - 1];
1259
1293
  if (group[leaf] !== void 0) {
1260
- 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})`);
1261
1295
  }
1262
1296
  group[leaf] = token;
1263
1297
  flat.push({
1264
- path: path33.join("."),
1265
- cssVar: tokenPathToCssVar(path33),
1298
+ path: path34.join("."),
1299
+ cssVar: tokenPathToCssVar(path34),
1266
1300
  type: token.$type,
1267
1301
  value: token.$value
1268
1302
  });
@@ -1451,9 +1485,9 @@ function boundId(value) {
1451
1485
  return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
1452
1486
  }
1453
1487
  function resolveBinding(ctx, id) {
1454
- const path33 = ctx.pathById.get(id);
1455
- if (path33 === void 0) ctx.unresolved.add(id);
1456
- return path33;
1488
+ const path34 = ctx.pathById.get(id);
1489
+ if (path34 === void 0) ctx.unresolved.add(id);
1490
+ return path34;
1457
1491
  }
1458
1492
  function parseVariantProps(name) {
1459
1493
  if (!name.includes("=")) return void 0;
@@ -1488,8 +1522,8 @@ function walk(ctx, raw) {
1488
1522
  if (!isObject(paint) || paint["visible"] === false) continue;
1489
1523
  const id = boundId(paint);
1490
1524
  if (id !== void 0) {
1491
- const path33 = resolveBinding(ctx, id);
1492
- if (path33 !== void 0) tokens.add(path33);
1525
+ const path34 = resolveBinding(ctx, id);
1526
+ if (path34 !== void 0) tokens.add(path34);
1493
1527
  } else if (typeof paint["color"] === "string") {
1494
1528
  ctx.hardcoded.push({ node: name, property, value: paint["color"] });
1495
1529
  }
@@ -1497,8 +1531,8 @@ function walk(ctx, raw) {
1497
1531
  }
1498
1532
  const radiusId = boundId(raw["cornerRadius"]);
1499
1533
  if (radiusId !== void 0) {
1500
- const path33 = resolveBinding(ctx, radiusId);
1501
- if (path33 !== void 0) tokens.add(path33);
1534
+ const path34 = resolveBinding(ctx, radiusId);
1535
+ if (path34 !== void 0) tokens.add(path34);
1502
1536
  } else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
1503
1537
  ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
1504
1538
  }
@@ -1508,10 +1542,10 @@ function walk(ctx, raw) {
1508
1542
  layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
1509
1543
  const gapId = boundId(raw["itemSpacing"]);
1510
1544
  if (gapId !== void 0) {
1511
- const path33 = resolveBinding(ctx, gapId);
1512
- if (path33 !== void 0) {
1513
- layout.gap = path33;
1514
- tokens.add(path33);
1545
+ const path34 = resolveBinding(ctx, gapId);
1546
+ if (path34 !== void 0) {
1547
+ layout.gap = path34;
1548
+ tokens.add(path34);
1515
1549
  }
1516
1550
  } else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
1517
1551
  ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
@@ -1520,10 +1554,10 @@ function walk(ctx, raw) {
1520
1554
  for (const field of PADDING_FIELDS) {
1521
1555
  const id = boundId(raw[field]);
1522
1556
  if (id !== void 0) {
1523
- const path33 = resolveBinding(ctx, id);
1524
- if (path33 !== void 0) {
1525
- paddingPaths.push(path33);
1526
- tokens.add(path33);
1557
+ const path34 = resolveBinding(ctx, id);
1558
+ if (path34 !== void 0) {
1559
+ paddingPaths.push(path34);
1560
+ tokens.add(path34);
1527
1561
  }
1528
1562
  } else if (typeof raw[field] === "number" && raw[field] !== 0) {
1529
1563
  ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
@@ -6087,10 +6121,10 @@ function buildTrustStatement(input) {
6087
6121
  const unrecorded = input.latticeConfigs === null ? null : Math.max(0, input.latticeConfigs - input.scored);
6088
6122
  const interaction = input.interactionChecks === 0 ? "interaction behaviors NONE VERIFIED (0 checks)" : `interaction behaviors ${input.interactionPassed}/${input.interactionChecks}`;
6089
6123
  const prelude = input.preludeChecks === 0 ? "" : `, page hygiene ${input.preludePassed}/${input.preludeChecks}`;
6090
- 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.`;
6091
6125
  }
6092
6126
  function cssProvenanceComment(input) {
6093
- 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`;
6094
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\`. */`;
6095
6129
  }
6096
6130
  function hashRecordingSet(relPaths, readFile, sha256) {
@@ -6243,8 +6277,10 @@ function symbolsFromMetadataEnvelope(file, sourceFrame) {
6243
6277
  const next = node.type !== "COMPONENT" && node.name !== "" ? node.name : ancestor;
6244
6278
  for (const child of node.children) walk2(child, next);
6245
6279
  };
6246
- walk2(parseMetadataStructure(text), void 0);
6247
- 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 };
6248
6284
  }
6249
6285
  function instanceLeads(text) {
6250
6286
  const seen = /* @__PURE__ */ new Map();
@@ -6269,10 +6305,13 @@ function runRecordPlan(opts) {
6269
6305
  defaults[spec.slice(0, eq)] = spec.slice(eq + 1);
6270
6306
  }
6271
6307
  let symbols = [];
6308
+ let metadataTruncated = false;
6272
6309
  for (const spec of opts.metadataFiles) {
6273
6310
  const [file, frame] = spec.split("@");
6274
6311
  try {
6275
- 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;
6276
6315
  } catch (err) {
6277
6316
  fail(opts, ExitCode.InputValidation, {
6278
6317
  error: `could not read metadata envelope ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -6337,7 +6376,10 @@ function runRecordPlan(opts) {
6337
6376
  }
6338
6377
  }
6339
6378
  }
6340
- 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 } : {} });
6341
6383
  const defaultReports = reportAxisDefaults(symbols, Object.keys(defaults).length > 0 ? defaults : void 0);
6342
6384
  const toConfirm = resumed ? [] : defaultReports.filter((r) => (r.rule === "non-interaction" || r.rule === "frequency") && r.domain.length > 1);
6343
6385
  const callLow = manifest.reps.length * 3;
@@ -6347,6 +6389,15 @@ function runRecordPlan(opts) {
6347
6389
  {
6348
6390
  resumed,
6349
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 } : {},
6350
6401
  notRecorded: manifest.notRecorded ?? null,
6351
6402
  figmaCallEstimate: { reps: manifest.reps.length, calls: `~${callLow}\u2013${callHigh}` },
6352
6403
  ...toConfirm.length > 0 ? {
@@ -6362,8 +6413,18 @@ function runRecordPlan(opts) {
6362
6413
  },
6363
6414
  () => {
6364
6415
  if (resumed) {
6365
- 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})
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
6366
6426
  `);
6427
+ }
6367
6428
  return;
6368
6429
  }
6369
6430
  for (const r of plan.reps) process.stdout.write(`planned ${r.slug.padEnd(24)} ${r.nodeId} (${r.tier})
@@ -6372,8 +6433,8 @@ function runRecordPlan(opts) {
6372
6433
  `);
6373
6434
  process.stdout.write(`estimated recording cost: ~${callLow}\u2013${callHigh} Figma calls for ${manifest.reps.length} reps
6374
6435
  `);
6375
- for (const q of toConfirm) {
6376
- 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
6377
6438
  `);
6378
6439
  }
6379
6440
  }
@@ -7141,10 +7202,7 @@ function authorComponentApi(opts) {
7141
7202
  }
7142
7203
  const props = [];
7143
7204
  const forcedStates = [];
7144
- const propNameFor = (axis) => {
7145
- const name = camel(axis);
7146
- return RESERVED_PROPS.has(name.toLowerCase()) ? camel(`${opts.component} ${axis}`) : name;
7147
- };
7205
+ const propNameFor = (axis) => axisPropName(opts.component, axis);
7148
7206
  for (const key of axisKeys) {
7149
7207
  const domain = domains.get(key);
7150
7208
  const def = defaults.get(key);
@@ -7412,6 +7470,7 @@ PAINT & PLATFORM TRAPS (each measured on a paid run; every one passed typecheck,
7412
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.
7413
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.
7414
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).
7415
7474
 
7416
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.
7417
7476
 
@@ -7426,7 +7485,7 @@ ${PRELUDE_CONTRACT}
7426
7485
  ${opts.colorScheme === void 0 ? "" : `
7427
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.`}`;
7428
7487
  }
7429
- 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;
7430
7489
  var init_brief = __esm({
7431
7490
  "packages/generate/src/brief.ts"() {
7432
7491
  "use strict";
@@ -7450,6 +7509,10 @@ var init_brief = __esm({
7450
7509
  return c === "" ? c : c[0].toUpperCase() + c.slice(1);
7451
7510
  };
7452
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
+ };
7453
7516
  isStateAxis = (axis) => kebab3(axis) === "state";
7454
7517
  symbolName = (metaText) => {
7455
7518
  const raw = /name="([^"]*)"/.exec(metaText)?.[1];
@@ -8063,7 +8126,18 @@ function taskFromManifest(opts, manifest, setDir) {
8063
8126
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
8064
8127
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
8065
8128
  const registry = Object.values(TASKS).find((t) => path30.resolve(t.set) === path30.resolve(setDir));
8066
- 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
+ })();
8067
8141
  const behaviorSource = registry ?? authored.task;
8068
8142
  const task = {
8069
8143
  set: setDir,
@@ -8245,6 +8319,23 @@ async function runVerify(opts) {
8245
8319
  scoredConfigs: statuses.length,
8246
8320
  certified,
8247
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
+ })(),
8248
8339
  // Prelude checks are page-level style hygiene and say nothing
8249
8340
  // about whether the component WORKS. Reporting one merged
8250
8341
  // "behaviors 6/6" made a component with zero interaction coverage
@@ -8349,6 +8440,18 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
8349
8440
  `
8350
8441
  );
8351
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
+ }
8352
8455
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
8353
8456
  `);
8354
8457
  process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
@@ -8427,6 +8530,7 @@ function runEngineBrief(opts) {
8427
8530
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
8428
8531
 
8429
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.
8430
8534
  ${notRecorded}` : "";
8431
8535
  let fontProvisioning;
8432
8536
  if (existsSync25(manifestPath2)) {
@@ -8512,6 +8616,30 @@ async function runEngineScore(opts) {
8512
8616
  remediation: fontsUnprovenRemediation(task.set)
8513
8617
  });
8514
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
+ }
8515
8643
  const bar = BARS3[opts.bar];
8516
8644
  const evidenceDir = path31.join(candidateDir, "verify-evidence");
8517
8645
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
@@ -8584,6 +8712,7 @@ var init_engine2 = __esm({
8584
8712
  "use strict";
8585
8713
  init_src3();
8586
8714
  init_src7();
8715
+ init_src6();
8587
8716
  init_environment();
8588
8717
  init_font_guidance();
8589
8718
  init_src4();
@@ -8596,6 +8725,182 @@ var init_engine2 = __esm({
8596
8725
  }
8597
8726
  });
8598
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
+
8599
8904
  // packages/cli/src/commands/generate-route.ts
8600
8905
  var generate_route_exports = {};
8601
8906
  __export(generate_route_exports, {
@@ -8620,17 +8925,17 @@ __export(generate_recorded_exports, {
8620
8925
  runGenerateRecorded: () => runGenerateRecorded
8621
8926
  });
8622
8927
  import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
8623
- import { existsSync as existsSync26 } from "node:fs";
8624
- import path32 from "node:path";
8928
+ import { existsSync as existsSync27, readFileSync as readFileSync24 } from "node:fs";
8929
+ import path33 from "node:path";
8625
8930
  async function runGenerateRecorded(opts) {
8626
8931
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
8627
- const outDirAbs = path32.resolve(callerCwd, opts.out);
8628
- const recordedAsPath = path32.resolve(callerCwd, opts.recorded);
8932
+ const outDirAbs = path33.resolve(callerCwd, opts.out);
8933
+ const recordedAsPath = path33.resolve(callerCwd, opts.recorded);
8629
8934
  let task;
8630
8935
  let taskName;
8631
8936
  let authoredApi;
8632
8937
  let composition;
8633
- const isSet = existsSync26(path32.join(recordedAsPath, "recording-set.json"));
8938
+ const isSet = existsSync27(path33.join(recordedAsPath, "recording-set.json"));
8634
8939
  const registry = TASKS[opts.recorded];
8635
8940
  if (registry !== void 0 && !isSet) {
8636
8941
  task = registry;
@@ -8639,7 +8944,7 @@ async function runGenerateRecorded(opts) {
8639
8944
  try {
8640
8945
  const authored = authorTaskFromSet(recordedAsPath);
8641
8946
  task = authored.task;
8642
- taskName = path32.basename(recordedAsPath);
8947
+ taskName = path33.basename(recordedAsPath);
8643
8948
  authoredApi = authored.api;
8644
8949
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
8645
8950
  if (roles.success) composition = roles.data;
@@ -8666,7 +8971,7 @@ async function runGenerateRecorded(opts) {
8666
8971
  });
8667
8972
  }
8668
8973
  const missing = task.configs.filter(
8669
- (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"))
8670
8975
  );
8671
8976
  if (missing.length > 0) {
8672
8977
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -8736,8 +9041,8 @@ async function runGenerateRecorded(opts) {
8736
9041
  ` : `${line}
8737
9042
  `);
8738
9043
  if (opts.dryRun) {
8739
- emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path32.join(outDirAbs, taskName) }, () => {
8740
- 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)})
8741
9046
  `);
8742
9047
  });
8743
9048
  return;
@@ -8760,7 +9065,21 @@ async function runGenerateRecorded(opts) {
8760
9065
  });
8761
9066
  }
8762
9067
  }
8763
- 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
+ }
8764
9083
  const result = await runEngineLoop({
8765
9084
  engine,
8766
9085
  segments,
@@ -9858,7 +10177,7 @@ function buildProgram() {
9858
10177
  await runDoctor({ ...flags, mcpUrl: local["mcpUrl"] });
9859
10178
  });
9860
10179
  const record = program.command("record").description("Agent-driven recording protocol: plan the queue, get instructions, ingest verbatim envelopes, resume from disk.");
9861
- 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) => {
9862
10181
  const flags = globalFlags(cmd.parent.parent);
9863
10182
  const local = cmd.opts();
9864
10183
  const { runRecordPlan: runRecordPlan2 } = await Promise.resolve().then(() => (init_record(), record_exports));
@@ -9867,6 +10186,7 @@ function buildProgram() {
9867
10186
  setDir: local["set"],
9868
10187
  component: local["component"],
9869
10188
  metadataFiles: local["metadata"],
10189
+ sample: local["sample"],
9870
10190
  ...local["default"] !== void 0 ? { defaultSpecs: local["default"] } : {},
9871
10191
  ...local["componentSet"] !== void 0 ? { componentSet: local["componentSet"] } : {}
9872
10192
  });
@@ -9957,7 +10277,7 @@ function buildProgram() {
9957
10277
  ...local["model"] !== void 0 ? { model: local["model"] } : {}
9958
10278
  });
9959
10279
  });
9960
- 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) => {
9961
10281
  const flags = globalFlags(cmd.parent.parent);
9962
10282
  const local = cmd.opts();
9963
10283
  const { runEngineScore: runEngineScore2 } = await Promise.resolve().then(() => (init_engine2(), engine_exports));
@@ -9967,7 +10287,20 @@ function buildProgram() {
9967
10287
  candidateDir,
9968
10288
  bar: local["bar"] === "cert" ? "cert" : "pass",
9969
10289
  ...local["host"] !== void 0 ? { host: local["host"] } : {},
9970
- 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"] } : {}
9971
10304
  });
9972
10305
  });
9973
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.6",
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",