figma-cache-toolchain 2.0.4 → 2.0.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/README.md +1 -5
- package/figma-cache/docs/README.md +10 -19
- package/figma-cache/docs/raw-json-extensions.schema.json +44 -0
- package/figma-cache/figma-cache.js +111 -0
- package/figma-cache/js/entry-files.js +199 -26
- package/figma-cache/js/flow-cli.js +51 -6
- package/figma-cache/js/raw-derivatives.js +85 -0
- package/figma-cache/js/related-cache-keys.js +56 -0
- package/figma-cache/js/ui-facts-normalizer.js +29 -18
- package/figma-cache/js/validate-cli.js +68 -0
- package/package.json +16 -3
- package/scripts/apply-auto-related-suggestions.cjs +180 -0
- package/scripts/archive-artifacts-from-batch.cjs +160 -0
- package/scripts/auto-link-related-from-batch.cjs +310 -0
- package/scripts/cross-project-e2e.js +163 -6
- package/scripts/forbidden-markup-check.cjs +300 -0
- package/scripts/generate-icon-insets-from-batch.cjs +194 -0
- package/scripts/generate-icon-insets.cjs +112 -0
- package/scripts/import-mcp-raw-evidence.cjs +141 -0
- package/scripts/merge-figma-geometry-metrics.cjs +81 -0
- package/scripts/ui-1to1-audit.js +33 -5
- package/scripts/ui-auto-acceptance.js +3 -3
- package/scripts/ui-preflight.js +1 -1
- package/scripts/ui-report-aggregate.js +3 -3
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Auto-link related cacheKeys into figma-cache/index.json flows using
|
|
6
|
+
* high-confidence heuristics based on raw.json.iconMetrics overlaps.
|
|
7
|
+
*
|
|
8
|
+
* Goal: reduce manual relatedCacheKeys maintenance and support "agent only mentions nodeId".
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/auto-link-related-from-batch.cjs --batch=./figma-e2e-batch.json
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --flow=<flowId> (default: env FIGMA_DEFAULT_FLOW || "auto-related")
|
|
15
|
+
* --min-shared=<n> (default: 3) min shared iconMetrics nodeIds
|
|
16
|
+
* --min-shared-instance=<n> (default: 2) min shared instance-path nodeIds (contain ';')
|
|
17
|
+
* --min-jaccard=<ratio> (default: 0.35)
|
|
18
|
+
* --suggest-min-shared=<n> (default: 1) suggestion threshold (won't auto-link)
|
|
19
|
+
* --suggest-min-shared-instance=<n> (default: 1)
|
|
20
|
+
* --suggest-min-jaccard=<ratio> (default: 0.12)
|
|
21
|
+
* --suggest-out=<path> (default: figma-cache/reports/runtime/auto-related-suggestions.json)
|
|
22
|
+
* --promote-min-jaccard=<ratio> (default: 0.55) if met (with shared instance), promote to auto-link
|
|
23
|
+
* --promote-min-shared-instance=<n> (default: 2)
|
|
24
|
+
* --dry-run don't write index.json
|
|
25
|
+
*
|
|
26
|
+
* Strict mode:
|
|
27
|
+
* FIGMA_UI_AUTOLINK_STRICT=1 will exit(3) if suggestions are present.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require("fs");
|
|
31
|
+
const path = require("path");
|
|
32
|
+
|
|
33
|
+
const ROOT = process.cwd();
|
|
34
|
+
const INDEX_ABS = path.join(ROOT, "figma-cache", "index.json");
|
|
35
|
+
const DEFAULT_SUGGEST_OUT = path.join(
|
|
36
|
+
ROOT,
|
|
37
|
+
"figma-cache",
|
|
38
|
+
"reports",
|
|
39
|
+
"runtime",
|
|
40
|
+
"auto-related-suggestions.json"
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
function safeReadJson(abs) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeJson(abs, value) {
|
|
52
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
53
|
+
fs.writeFileSync(abs, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeNodeId(input) {
|
|
57
|
+
const value = String(input || "").trim();
|
|
58
|
+
if (!value) return "";
|
|
59
|
+
return value.includes(":") ? value : value.replace(/-/g, ":");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeCacheKey(input) {
|
|
63
|
+
const value = String(input || "").trim();
|
|
64
|
+
if (!value) return "";
|
|
65
|
+
const parts = value.split("#");
|
|
66
|
+
if (parts.length !== 2) return value;
|
|
67
|
+
return `${parts[0]}#${normalizeNodeId(parts[1])}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function cacheKeyFromItem(item) {
|
|
71
|
+
const fileKey = String(item && item.fileKey ? item.fileKey : "").trim();
|
|
72
|
+
const nodeId = String(item && item.nodeId ? item.nodeId : "").trim();
|
|
73
|
+
if (!fileKey || !nodeId) return "";
|
|
74
|
+
return `${fileKey}#${normalizeNodeId(nodeId)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseArgs(argv) {
|
|
78
|
+
const out = {
|
|
79
|
+
batch: path.join(ROOT, "figma-e2e-batch.json"),
|
|
80
|
+
flowId: process.env.FIGMA_DEFAULT_FLOW || "auto-related",
|
|
81
|
+
minShared: 2,
|
|
82
|
+
minSharedInstance: 1,
|
|
83
|
+
minJaccard: 0.2,
|
|
84
|
+
suggestMinShared: 1,
|
|
85
|
+
suggestMinSharedInstance: 1,
|
|
86
|
+
suggestMinJaccard: 0.12,
|
|
87
|
+
suggestOut: process.env.FIGMA_UI_AUTOLINK_SUGGEST_OUT || DEFAULT_SUGGEST_OUT,
|
|
88
|
+
promoteMinJaccard: 0.55,
|
|
89
|
+
promoteMinSharedInstance: 2,
|
|
90
|
+
dryRun: false,
|
|
91
|
+
};
|
|
92
|
+
argv.slice(2).forEach((arg) => {
|
|
93
|
+
if (arg.startsWith("--batch=")) out.batch = arg.split("=").slice(1).join("=").trim();
|
|
94
|
+
if (arg.startsWith("--flow=")) out.flowId = arg.split("=").slice(1).join("=").trim();
|
|
95
|
+
if (arg.startsWith("--min-shared=")) out.minShared = Number(arg.split("=").slice(1).join("=").trim());
|
|
96
|
+
if (arg.startsWith("--min-shared-instance="))
|
|
97
|
+
out.minSharedInstance = Number(arg.split("=").slice(1).join("=").trim());
|
|
98
|
+
if (arg.startsWith("--min-jaccard=")) out.minJaccard = Number(arg.split("=").slice(1).join("=").trim());
|
|
99
|
+
if (arg.startsWith("--suggest-min-shared="))
|
|
100
|
+
out.suggestMinShared = Number(arg.split("=").slice(1).join("=").trim());
|
|
101
|
+
if (arg.startsWith("--suggest-min-shared-instance="))
|
|
102
|
+
out.suggestMinSharedInstance = Number(arg.split("=").slice(1).join("=").trim());
|
|
103
|
+
if (arg.startsWith("--suggest-min-jaccard="))
|
|
104
|
+
out.suggestMinJaccard = Number(arg.split("=").slice(1).join("=").trim());
|
|
105
|
+
if (arg.startsWith("--suggest-out=")) out.suggestOut = arg.split("=").slice(1).join("=").trim();
|
|
106
|
+
if (arg.startsWith("--promote-min-jaccard="))
|
|
107
|
+
out.promoteMinJaccard = Number(arg.split("=").slice(1).join("=").trim());
|
|
108
|
+
if (arg.startsWith("--promote-min-shared-instance="))
|
|
109
|
+
out.promoteMinSharedInstance = Number(arg.split("=").slice(1).join("=").trim());
|
|
110
|
+
if (arg === "--dry-run") out.dryRun = true;
|
|
111
|
+
});
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function setFromIconMetrics(raw) {
|
|
116
|
+
const list = raw && Array.isArray(raw.iconMetrics) ? raw.iconMetrics : [];
|
|
117
|
+
const all = new Set();
|
|
118
|
+
const instance = new Set();
|
|
119
|
+
list.forEach((m) => {
|
|
120
|
+
const id = m && m.nodeId ? String(m.nodeId).trim() : "";
|
|
121
|
+
if (!id) return;
|
|
122
|
+
all.add(id);
|
|
123
|
+
if (id.includes(";")) instance.add(id);
|
|
124
|
+
});
|
|
125
|
+
return { all, instance };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function intersectionSize(a, b) {
|
|
129
|
+
let c = 0;
|
|
130
|
+
a.forEach((x) => {
|
|
131
|
+
if (b.has(x)) c += 1;
|
|
132
|
+
});
|
|
133
|
+
return c;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function jaccard(a, b) {
|
|
137
|
+
const inter = intersectionSize(a, b);
|
|
138
|
+
const union = a.size + b.size - inter;
|
|
139
|
+
return union <= 0 ? 0 : inter / union;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ensureFlow(index, flowId) {
|
|
143
|
+
index.flows = index.flows || {};
|
|
144
|
+
if (!index.flows[flowId]) {
|
|
145
|
+
index.flows[flowId] = {
|
|
146
|
+
id: flowId,
|
|
147
|
+
title: flowId,
|
|
148
|
+
description: "Auto-linked by iconMetrics overlap heuristics",
|
|
149
|
+
createdAt: new Date().toISOString(),
|
|
150
|
+
updatedAt: new Date().toISOString(),
|
|
151
|
+
nodes: [],
|
|
152
|
+
edges: [],
|
|
153
|
+
assumptions: [],
|
|
154
|
+
openQuestions: [],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return index.flows[flowId];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function addNode(flow, cacheKey) {
|
|
161
|
+
flow.nodes = flow.nodes || [];
|
|
162
|
+
if (!flow.nodes.includes(cacheKey)) flow.nodes.push(cacheKey);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hasEdge(flow, from, to, type) {
|
|
166
|
+
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
167
|
+
return edges.some((e) => e && e.from === from && e.to === to && e.type === type);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function addEdge(flow, from, to, type, note) {
|
|
171
|
+
flow.edges = flow.edges || [];
|
|
172
|
+
if (hasEdge(flow, from, to, type)) return false;
|
|
173
|
+
flow.edges.push({
|
|
174
|
+
id: `${from}->${to}:${type}:${Date.now()}`,
|
|
175
|
+
from,
|
|
176
|
+
to,
|
|
177
|
+
type,
|
|
178
|
+
note: note || "",
|
|
179
|
+
createdAt: new Date().toISOString(),
|
|
180
|
+
});
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function main() {
|
|
185
|
+
const args = parseArgs(process.argv);
|
|
186
|
+
const batchAbs = path.isAbsolute(args.batch) ? args.batch : path.join(ROOT, args.batch);
|
|
187
|
+
const index = safeReadJson(INDEX_ABS);
|
|
188
|
+
if (!index || !index.items) {
|
|
189
|
+
console.error(`[auto-link-related-from-batch] missing/invalid index: ${INDEX_ABS}`);
|
|
190
|
+
process.exit(2);
|
|
191
|
+
}
|
|
192
|
+
if (!fs.existsSync(batchAbs)) {
|
|
193
|
+
console.error(`[auto-link-related-from-batch] batch not found: ${batchAbs}`);
|
|
194
|
+
process.exit(2);
|
|
195
|
+
}
|
|
196
|
+
const batch = JSON.parse(fs.readFileSync(batchAbs, "utf8"));
|
|
197
|
+
if (!Array.isArray(batch) || batch.length === 0) {
|
|
198
|
+
console.error("[auto-link-related-from-batch] batch must be a non-empty array");
|
|
199
|
+
process.exit(2);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const flow = ensureFlow(index, args.flowId);
|
|
203
|
+
let links = 0;
|
|
204
|
+
const suggestions = [];
|
|
205
|
+
|
|
206
|
+
batch.forEach((b) => {
|
|
207
|
+
const primary = normalizeCacheKey(String(b && (b.cacheKey || cacheKeyFromItem(b)) || ""));
|
|
208
|
+
if (!primary) return;
|
|
209
|
+
const primaryItem = index.items[primary];
|
|
210
|
+
if (!primaryItem || !primaryItem.paths || !primaryItem.paths.raw) return;
|
|
211
|
+
const primaryRawAbs = path.isAbsolute(primaryItem.paths.raw)
|
|
212
|
+
? primaryItem.paths.raw
|
|
213
|
+
: path.join(ROOT, primaryItem.paths.raw);
|
|
214
|
+
const primaryRaw = safeReadJson(primaryRawAbs);
|
|
215
|
+
const primarySet = setFromIconMetrics(primaryRaw);
|
|
216
|
+
if (primarySet.all.size === 0) return;
|
|
217
|
+
|
|
218
|
+
const fileKey = String(primaryItem.fileKey || "").trim();
|
|
219
|
+
const candidates = Object.keys(index.items)
|
|
220
|
+
.filter((k) => k !== primary && index.items[k] && index.items[k].fileKey === fileKey)
|
|
221
|
+
.map((k) => ({ key: k, item: index.items[k] }));
|
|
222
|
+
|
|
223
|
+
candidates.forEach((cand) => {
|
|
224
|
+
const rawPath = cand.item && cand.item.paths ? cand.item.paths.raw : "";
|
|
225
|
+
if (!rawPath) return;
|
|
226
|
+
const rawAbs = path.isAbsolute(rawPath) ? rawPath : path.join(ROOT, rawPath);
|
|
227
|
+
const raw = safeReadJson(rawAbs);
|
|
228
|
+
const set = setFromIconMetrics(raw);
|
|
229
|
+
if (set.all.size === 0) return;
|
|
230
|
+
|
|
231
|
+
const shared = intersectionSize(primarySet.all, set.all);
|
|
232
|
+
const sharedInstance = intersectionSize(primarySet.instance, set.instance);
|
|
233
|
+
const score = jaccard(primarySet.all, set.all);
|
|
234
|
+
|
|
235
|
+
const promote =
|
|
236
|
+
sharedInstance >= args.promoteMinSharedInstance &&
|
|
237
|
+
score >= args.promoteMinJaccard;
|
|
238
|
+
|
|
239
|
+
const ok =
|
|
240
|
+
promote ||
|
|
241
|
+
(shared >= args.minShared &&
|
|
242
|
+
sharedInstance >= args.minSharedInstance &&
|
|
243
|
+
score >= args.minJaccard);
|
|
244
|
+
if (!ok) {
|
|
245
|
+
const suggestOk =
|
|
246
|
+
shared >= args.suggestMinShared &&
|
|
247
|
+
sharedInstance >= args.suggestMinSharedInstance &&
|
|
248
|
+
score >= args.suggestMinJaccard;
|
|
249
|
+
if (suggestOk) {
|
|
250
|
+
suggestions.push({
|
|
251
|
+
from: primary,
|
|
252
|
+
to: cand.key,
|
|
253
|
+
fileKey,
|
|
254
|
+
shared,
|
|
255
|
+
sharedInstance,
|
|
256
|
+
jaccard: Number(score.toFixed(4)),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
addNode(flow, primary);
|
|
263
|
+
addNode(flow, cand.key);
|
|
264
|
+
const note = `auto: shared=${shared}, sharedInstance=${sharedInstance}, jaccard=${Number(
|
|
265
|
+
score.toFixed(3)
|
|
266
|
+
)}`;
|
|
267
|
+
if (addEdge(flow, primary, cand.key, "related_auto", note)) {
|
|
268
|
+
links += 1;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
flow.updatedAt = new Date().toISOString();
|
|
274
|
+
index.updatedAt = new Date().toISOString();
|
|
275
|
+
|
|
276
|
+
if (!args.dryRun) {
|
|
277
|
+
writeJson(INDEX_ABS, index);
|
|
278
|
+
}
|
|
279
|
+
const suggestOutAbs = path.isAbsolute(args.suggestOut) ? args.suggestOut : path.join(ROOT, args.suggestOut);
|
|
280
|
+
writeJson(suggestOutAbs, {
|
|
281
|
+
generatedAt: new Date().toISOString(),
|
|
282
|
+
flowId: args.flowId,
|
|
283
|
+
strict: process.env.FIGMA_UI_AUTOLINK_STRICT === "1",
|
|
284
|
+
thresholds: {
|
|
285
|
+
auto: { minShared: args.minShared, minSharedInstance: args.minSharedInstance, minJaccard: args.minJaccard },
|
|
286
|
+
promote: {
|
|
287
|
+
minSharedInstance: args.promoteMinSharedInstance,
|
|
288
|
+
minJaccard: args.promoteMinJaccard,
|
|
289
|
+
},
|
|
290
|
+
suggest: {
|
|
291
|
+
minShared: args.suggestMinShared,
|
|
292
|
+
minSharedInstance: args.suggestMinSharedInstance,
|
|
293
|
+
minJaccard: args.suggestMinJaccard,
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
links,
|
|
297
|
+
suggestions,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
console.log(
|
|
301
|
+
`[auto-link-related-from-batch] ok links=${links} suggestions=${suggestions.length} flow=${args.flowId} dryRun=${args.dryRun ? "1" : "0"} suggestOut=${suggestOutAbs}`
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (process.env.FIGMA_UI_AUTOLINK_STRICT === "1" && suggestions.length) {
|
|
305
|
+
process.exit(3);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
main();
|
|
310
|
+
|
|
@@ -16,6 +16,21 @@ function resolveMaybeAbsolutePath(input) {
|
|
|
16
16
|
return path.isAbsolute(input) ? path.normalize(input) : path.join(ROOT, input);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Batch / CLI `target` paths are relative to the target business project root (not toolchain cwd). */
|
|
20
|
+
function resolveTargetInProject(rawTarget, targetProject) {
|
|
21
|
+
if (!rawTarget) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
const trimmed = String(rawTarget).trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
if (path.isAbsolute(trimmed)) {
|
|
29
|
+
return path.normalize(trimmed);
|
|
30
|
+
}
|
|
31
|
+
return path.join(targetProject, trimmed);
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
function parseArgs(argv) {
|
|
20
35
|
const options = {
|
|
21
36
|
targetProject: "",
|
|
@@ -31,9 +46,13 @@ function parseArgs(argv) {
|
|
|
31
46
|
autoBootstrapContract: true,
|
|
32
47
|
autoEnsureOnMiss: false,
|
|
33
48
|
allowSkeletonWithFigmaMcp: false,
|
|
34
|
-
|
|
49
|
+
// Default to "best effort" evidence to support 1:1 audits out-of-the-box.
|
|
50
|
+
completeness: "layout,text,tokens,assets,interactions,states,accessibility",
|
|
35
51
|
batchFile: "",
|
|
36
52
|
fixLoop: 0,
|
|
53
|
+
emitAgentTaskOnFail: false,
|
|
54
|
+
agentTaskPath: "",
|
|
55
|
+
allowSkippedCodeLevelComparison: false,
|
|
37
56
|
};
|
|
38
57
|
|
|
39
58
|
argv.forEach((arg) => {
|
|
@@ -103,6 +122,18 @@ function parseArgs(argv) {
|
|
|
103
122
|
if (arg.startsWith("--fix-loop=")) {
|
|
104
123
|
const n = Number(arg.split("=").slice(1).join("=").trim());
|
|
105
124
|
options.fixLoop = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (arg === "--emit-agent-task-on-fail") {
|
|
128
|
+
options.emitAgentTaskOnFail = true;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (arg.startsWith("--agent-task-path=")) {
|
|
132
|
+
options.agentTaskPath = arg.split("=").slice(1).join("=").trim();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (arg === "--allow-skipped-code-level-comparison") {
|
|
136
|
+
options.allowSkippedCodeLevelComparison = true;
|
|
106
137
|
}
|
|
107
138
|
});
|
|
108
139
|
|
|
@@ -164,6 +195,63 @@ function normalizeSlash(input) {
|
|
|
164
195
|
return String(input || "").replace(/\\/g, "/");
|
|
165
196
|
}
|
|
166
197
|
|
|
198
|
+
function writeAgentTask(targetProject, options, payload) {
|
|
199
|
+
const defaultPath = path.join(targetProject, "agent-task.md");
|
|
200
|
+
const taskPath = options.agentTaskPath
|
|
201
|
+
? resolveMaybeAbsolutePath(options.agentTaskPath)
|
|
202
|
+
: defaultPath;
|
|
203
|
+
const lines = [];
|
|
204
|
+
lines.push("# Agent Task: UI E2E Recovery");
|
|
205
|
+
lines.push("");
|
|
206
|
+
lines.push("## Goal");
|
|
207
|
+
lines.push("Fix target project implementation so ui acceptance passes.");
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push("## Constraints");
|
|
210
|
+
lines.push("- Must run ui acceptance after code changes.");
|
|
211
|
+
lines.push("- Do not bypass by lowering thresholds unless explicitly requested.");
|
|
212
|
+
lines.push("- Prioritize real component/contract/recipe fixes.");
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push("## Context");
|
|
215
|
+
lines.push(`- targetProject: ${normalizeSlash(payload.targetProject || "")}`);
|
|
216
|
+
lines.push(`- mode: ${payload.mode || "single"}`);
|
|
217
|
+
lines.push(`- profile: ${payload.profile || "standard"}`);
|
|
218
|
+
lines.push(`- autoEnsureOnMiss: ${payload.autoEnsureOnMiss ? "true" : "false"}`);
|
|
219
|
+
lines.push(`- fixLoop: ${Number(payload.fixLoop || 0)}`);
|
|
220
|
+
lines.push("");
|
|
221
|
+
lines.push("## Cases");
|
|
222
|
+
(payload.cases || []).forEach((entry, idx) => {
|
|
223
|
+
lines.push(`### Case ${idx + 1}`);
|
|
224
|
+
lines.push(`- cacheKey: ${entry.cacheKey || ""}`);
|
|
225
|
+
lines.push(`- targetPath: ${normalizeSlash(entry.targetPath || "")}`);
|
|
226
|
+
lines.push(`- reason: ${entry.reason || "unknown"}`);
|
|
227
|
+
if (entry.attemptLogs && entry.attemptLogs.length) {
|
|
228
|
+
lines.push("- attemptLogs:");
|
|
229
|
+
entry.attemptLogs.forEach((log) => {
|
|
230
|
+
lines.push(
|
|
231
|
+
` - attempt ${log.attempt}: ${log.ok ? "ok" : "fail"}${log.reason ? ` (${log.reason})` : ""}`
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
lines.push("");
|
|
236
|
+
});
|
|
237
|
+
lines.push("## Required Command");
|
|
238
|
+
lines.push("Run this command in toolchain repo after fixes:");
|
|
239
|
+
lines.push("");
|
|
240
|
+
lines.push("```bash");
|
|
241
|
+
lines.push(payload.retryCommand || "npm run figma:ui:e2e:cross -- --target-project=<...>");
|
|
242
|
+
lines.push("```");
|
|
243
|
+
lines.push("");
|
|
244
|
+
lines.push("## Completion Criteria");
|
|
245
|
+
lines.push("- e2e command exits with code 0");
|
|
246
|
+
lines.push("- summaryStatus is healthy");
|
|
247
|
+
lines.push("- no unresolved blocking items");
|
|
248
|
+
lines.push("");
|
|
249
|
+
|
|
250
|
+
fs.mkdirSync(path.dirname(taskPath), { recursive: true });
|
|
251
|
+
fs.writeFileSync(taskPath, `${lines.join("\n")}\n`, "utf8");
|
|
252
|
+
return taskPath;
|
|
253
|
+
}
|
|
254
|
+
|
|
167
255
|
function parseCacheKey(cacheKey) {
|
|
168
256
|
const value = String(cacheKey || "").trim();
|
|
169
257
|
const [fileKey, nodeId] = value.split("#");
|
|
@@ -256,10 +344,13 @@ function runSingleCase(input, context) {
|
|
|
256
344
|
if (!cacheKey) {
|
|
257
345
|
throw new Error("batch item missing cacheKey or (fileKey + nodeId)");
|
|
258
346
|
}
|
|
259
|
-
const targetPath = item.target ?
|
|
347
|
+
const targetPath = item.target ? resolveTargetInProject(item.target, targetProject) : "";
|
|
260
348
|
if (!targetPath) {
|
|
261
349
|
throw new Error(`batch item ${cacheKey} missing target path`);
|
|
262
350
|
}
|
|
351
|
+
if (!fs.existsSync(targetPath)) {
|
|
352
|
+
throw new Error(`batch item ${cacheKey} target path does not exist: ${targetPath}`);
|
|
353
|
+
}
|
|
263
354
|
|
|
264
355
|
const acceptArgs = [
|
|
265
356
|
`--cacheKey=${cacheKey}`,
|
|
@@ -301,6 +392,18 @@ function runSingleCase(input, context) {
|
|
|
301
392
|
try {
|
|
302
393
|
acceptanceJson = JSON.parse(acceptanceOutput);
|
|
303
394
|
} catch {}
|
|
395
|
+
if (
|
|
396
|
+
!options.allowSkippedCodeLevelComparison &&
|
|
397
|
+
acceptanceJson &&
|
|
398
|
+
Array.isArray(acceptanceJson.warnings) &&
|
|
399
|
+
acceptanceJson.warnings.some((entry) =>
|
|
400
|
+
/code-level comparison skipped/i.test(String(entry || ""))
|
|
401
|
+
)
|
|
402
|
+
) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`acceptance produced skipped code-level comparison for ${cacheKey}; target linkage is invalid`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
304
407
|
attemptLogs.push({ attempt, ok: true });
|
|
305
408
|
return {
|
|
306
409
|
ok: true,
|
|
@@ -339,11 +442,15 @@ function run() {
|
|
|
339
442
|
}
|
|
340
443
|
const isBatchMode = !!options.batchFile;
|
|
341
444
|
if (!isBatchMode) {
|
|
342
|
-
const targetPath = options.target ?
|
|
445
|
+
const targetPath = options.target ? resolveTargetInProject(options.target, targetProject) : "";
|
|
343
446
|
if (!targetPath) {
|
|
344
447
|
console.error("cross-project-e2e failed: --target is required for real component validation");
|
|
345
448
|
process.exit(FAIL_EXIT_CODE);
|
|
346
449
|
}
|
|
450
|
+
if (!fs.existsSync(targetPath)) {
|
|
451
|
+
console.error(`cross-project-e2e failed: --target path does not exist: ${targetPath}`);
|
|
452
|
+
process.exit(FAIL_EXIT_CODE);
|
|
453
|
+
}
|
|
347
454
|
const cacheKey = resolveCacheKey(options);
|
|
348
455
|
if (!cacheKey) {
|
|
349
456
|
console.error("cross-project-e2e failed: provide --cacheKey or (--fileKey + --nodeId)");
|
|
@@ -352,6 +459,7 @@ function run() {
|
|
|
352
459
|
}
|
|
353
460
|
|
|
354
461
|
let tarballPath = "";
|
|
462
|
+
let taskPayload = null;
|
|
355
463
|
try {
|
|
356
464
|
tarballPath = npmPackAndGetTarball();
|
|
357
465
|
runCommand(`npm i -D "${tarballPath}"`, targetProject);
|
|
@@ -404,11 +512,26 @@ function run() {
|
|
|
404
512
|
caseFailures.push({
|
|
405
513
|
index: indexNo,
|
|
406
514
|
cacheKey: entry && (entry.cacheKey || resolveCacheKey(entry)),
|
|
515
|
+
targetPath: entry && entry.target,
|
|
516
|
+
attemptLogs: [],
|
|
407
517
|
reason: error.message,
|
|
408
518
|
});
|
|
409
519
|
}
|
|
410
520
|
});
|
|
411
521
|
if (caseFailures.length) {
|
|
522
|
+
taskPayload = {
|
|
523
|
+
targetProject,
|
|
524
|
+
mode: isBatchMode ? "batch" : "single",
|
|
525
|
+
profile: options.profile || "standard",
|
|
526
|
+
autoEnsureOnMiss: options.autoEnsureOnMiss,
|
|
527
|
+
fixLoop: options.fixLoop,
|
|
528
|
+
cases: caseFailures,
|
|
529
|
+
retryCommand: `npm run figma:ui:e2e:cross -- --target-project=${normalizeSlash(
|
|
530
|
+
targetProject
|
|
531
|
+
)}${options.batchFile ? ` --batch-file=${normalizeSlash(options.batchFile)}` : ""}${
|
|
532
|
+
options.autoEnsureOnMiss ? " --auto-ensure-on-miss" : ""
|
|
533
|
+
}${options.fixLoop ? ` --fix-loop=${options.fixLoop}` : ""}`,
|
|
534
|
+
};
|
|
412
535
|
throw new Error(`batch cases failed: ${JSON.stringify(caseFailures)}`);
|
|
413
536
|
}
|
|
414
537
|
|
|
@@ -424,9 +547,9 @@ function run() {
|
|
|
424
547
|
completeness: options.completeness,
|
|
425
548
|
tarballPath,
|
|
426
549
|
reports: {
|
|
427
|
-
preflight: path.join(reportBase, "ui-preflight-report.json"),
|
|
428
|
-
audit: path.join(reportBase, "ui-1to1-report.json"),
|
|
429
|
-
summary: path.join(reportBase, "ui-quality-summary.json"),
|
|
550
|
+
preflight: path.join(reportBase, "runtime", "ui-preflight-report.json"),
|
|
551
|
+
audit: path.join(reportBase, "runtime", "ui-1to1-report.json"),
|
|
552
|
+
summary: path.join(reportBase, "runtime", "ui-quality-summary.json"),
|
|
430
553
|
},
|
|
431
554
|
cases: caseResults,
|
|
432
555
|
};
|
|
@@ -438,8 +561,42 @@ function run() {
|
|
|
438
561
|
}
|
|
439
562
|
console.log(JSON.stringify(output, null, 2));
|
|
440
563
|
} catch (error) {
|
|
564
|
+
let taskPath = "";
|
|
565
|
+
if (options.emitAgentTaskOnFail) {
|
|
566
|
+
try {
|
|
567
|
+
const payload =
|
|
568
|
+
taskPayload ||
|
|
569
|
+
({
|
|
570
|
+
targetProject,
|
|
571
|
+
mode: isBatchMode ? "batch" : "single",
|
|
572
|
+
profile: options.profile || "standard",
|
|
573
|
+
autoEnsureOnMiss: options.autoEnsureOnMiss,
|
|
574
|
+
fixLoop: options.fixLoop,
|
|
575
|
+
cases: [
|
|
576
|
+
{
|
|
577
|
+
cacheKey: resolveCacheKey(options),
|
|
578
|
+
targetPath: options.target,
|
|
579
|
+
reason: error.message,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
retryCommand: `npm run figma:ui:e2e:cross -- --target-project=${normalizeSlash(
|
|
583
|
+
targetProject
|
|
584
|
+
)}${options.batchFile ? ` --batch-file=${normalizeSlash(options.batchFile)}` : ""}${
|
|
585
|
+
options.cacheKey ? ` --cacheKey=${options.cacheKey}` : ""
|
|
586
|
+
}${options.fileKey ? ` --fileKey=${options.fileKey}` : ""}${
|
|
587
|
+
options.nodeId ? ` --nodeId=${options.nodeId}` : ""
|
|
588
|
+
}${options.target ? ` --target=${normalizeSlash(options.target)}` : ""}${
|
|
589
|
+
options.autoEnsureOnMiss ? " --auto-ensure-on-miss" : ""
|
|
590
|
+
}${options.fixLoop ? ` --fix-loop=${options.fixLoop}` : ""} --emit-agent-task-on-fail`,
|
|
591
|
+
});
|
|
592
|
+
taskPath = writeAgentTask(targetProject, options, payload);
|
|
593
|
+
} catch {}
|
|
594
|
+
}
|
|
441
595
|
console.error("cross-project-e2e failed:");
|
|
442
596
|
console.error(`- ${error.message}`);
|
|
597
|
+
if (taskPath) {
|
|
598
|
+
console.error(`- agent task emitted: ${normalizeSlash(taskPath)}`);
|
|
599
|
+
}
|
|
443
600
|
process.exit(FAIL_EXIT_CODE);
|
|
444
601
|
} finally {
|
|
445
602
|
if (tarballPath && !options.keepPackage) {
|