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