@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.
- package/dist/tendril-mcp.js +22 -2
- package/dist/tendril.js +396 -63
- package/package.json +1 -1
package/dist/tendril-mcp.js
CHANGED
|
@@ -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
|
|
417
|
+
function parseMetadataForest(response) {
|
|
418
418
|
const stack = [];
|
|
419
|
-
|
|
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
|
|
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
|
|
431
|
+
if (stack.length === 0) roots.push(node);
|
|
432
432
|
continue;
|
|
433
433
|
}
|
|
434
434
|
stack.push(node);
|
|
435
435
|
}
|
|
436
|
-
|
|
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(
|
|
1182
|
-
return `--${
|
|
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
|
|
1227
|
-
if (
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
1292
|
+
const leaf = path34[path34.length - 1];
|
|
1259
1293
|
if (group[leaf] !== void 0) {
|
|
1260
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
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:
|
|
1265
|
-
cssVar: tokenPathToCssVar(
|
|
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
|
|
1455
|
-
if (
|
|
1456
|
-
return
|
|
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
|
|
1492
|
-
if (
|
|
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
|
|
1501
|
-
if (
|
|
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
|
|
1512
|
-
if (
|
|
1513
|
-
layout.gap =
|
|
1514
|
-
tokens.add(
|
|
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
|
|
1524
|
-
if (
|
|
1525
|
-
paddingPaths.push(
|
|
1526
|
-
tokens.add(
|
|
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
|
-
|
|
6247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6376
|
-
process.stdout.write(`CONFIRM ${
|
|
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 =
|
|
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
|
|
8624
|
-
import
|
|
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 =
|
|
8628
|
-
const recordedAsPath =
|
|
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 =
|
|
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 =
|
|
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) => !
|
|
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:
|
|
8740
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
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 =
|
|
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) => {
|