figma-cache-toolchain 2.0.2 → 2.0.4

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.
Files changed (40) hide show
  1. package/README.md +201 -170
  2. package/cursor-bootstrap/AGENT-SETUP-PROMPT.md +34 -29
  3. package/cursor-bootstrap/examples/README.md +26 -11
  4. package/cursor-bootstrap/examples/generated-ui-reset.css.template +32 -0
  5. package/cursor-bootstrap/examples/ui-adapter.contract.template.json +90 -0
  6. package/cursor-bootstrap/examples/ui-execution-template.fast.md +11 -0
  7. package/cursor-bootstrap/examples/ui-execution-template.strict.md +13 -0
  8. package/cursor-bootstrap/examples/ui-override.template.json +26 -0
  9. package/cursor-bootstrap/figma-cache.config.example.js +51 -9
  10. package/cursor-bootstrap/managed-files.json +41 -0
  11. package/cursor-bootstrap/rules/02-figma-stack-adapter.mdc +15 -25
  12. package/cursor-bootstrap/rules/03-figma-ui-implementation-hard-constraints.mdc +58 -91
  13. package/cursor-bootstrap/rules/04-ui-baseline-governance.mdc +35 -86
  14. package/cursor-bootstrap/skills/figma-ui-dual-mode-execution/SKILL.md +13 -8
  15. package/figma-cache/adapters/recipes/button.recipe.json +24 -0
  16. package/figma-cache/adapters/recipes/card.recipe.json +24 -0
  17. package/figma-cache/adapters/recipes/checkbox.recipe.json +24 -0
  18. package/figma-cache/adapters/recipes/input.recipe.json +24 -0
  19. package/figma-cache/adapters/recipes/modal.recipe.json +25 -0
  20. package/figma-cache/adapters/recipes/radio.recipe.json +23 -0
  21. package/figma-cache/adapters/recipes/select.recipe.json +24 -0
  22. package/figma-cache/adapters/recipes/table.recipe.json +25 -0
  23. package/figma-cache/adapters/recipes/tabs.recipe.json +24 -0
  24. package/figma-cache/adapters/recipes/tooltip.recipe.json +24 -0
  25. package/figma-cache/docs/README.md +322 -237
  26. package/figma-cache/docs/p0-ui-preflight-handoff.md +207 -0
  27. package/figma-cache/docs/ui-1to1-optimization-roadmap.md +182 -0
  28. package/figma-cache/docs/ui-1to1-report.schema.json +104 -0
  29. package/figma-cache/figma-cache.js +639 -556
  30. package/figma-cache/js/contract-check-cli.js +466 -0
  31. package/figma-cache/js/cursor-bootstrap-cli.js +240 -202
  32. package/figma-cache/js/ui-facts-normalizer.js +233 -0
  33. package/package.json +95 -74
  34. package/scripts/cross-project-e2e.js +453 -0
  35. package/scripts/ui-1to1-audit.js +431 -0
  36. package/scripts/ui-auto-acceptance.js +248 -0
  37. package/scripts/ui-preflight.js +289 -0
  38. package/scripts/ui-profile.js +46 -0
  39. package/scripts/ui-report-aggregate.js +124 -0
  40. package/cursor-bootstrap/skills/ui-baseline-governance/SKILL.md +0 -51
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ "use strict";
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const { normalizeUiFacts, normalizeHexColor } = require("../figma-cache/js/ui-facts-normalizer");
8
+ const { getUiProfileConfig } = require("./ui-profile");
9
+
10
+ const ROOT = process.cwd();
11
+ const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
12
+ const INDEX_FILE_NAME = process.env.FIGMA_CACHE_INDEX_FILE || "index.json";
13
+ const DEFAULT_CONTRACT_PATH = "figma-cache/adapters/ui-adapter.contract.json";
14
+ const DEFAULT_REPORT_PATH = "figma-cache/reports/ui-1to1-report.json";
15
+ const DEFAULT_MIN_SCORE = 85;
16
+ const DEFAULT_RECIPES_DIR = "figma-cache/adapters/recipes";
17
+ const FAIL_EXIT_CODE = 2;
18
+
19
+ function normalizeSlash(input) {
20
+ return String(input || "").replace(/\\/g, "/");
21
+ }
22
+
23
+ function resolveMaybeAbsolutePath(input) {
24
+ if (!input) {
25
+ return "";
26
+ }
27
+ return path.isAbsolute(input) ? path.normalize(input) : path.join(ROOT, input);
28
+ }
29
+
30
+ function readJsonOrNull(absPath) {
31
+ try {
32
+ return JSON.parse(fs.readFileSync(absPath, "utf8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function readTextOrEmpty(absPath) {
39
+ try {
40
+ return fs.readFileSync(absPath, "utf8");
41
+ } catch {
42
+ return "";
43
+ }
44
+ }
45
+
46
+ function parseArgs(argv) {
47
+ const options = {
48
+ cacheKey: "",
49
+ targetPath: "",
50
+ contractPath: DEFAULT_CONTRACT_PATH,
51
+ reportPath: DEFAULT_REPORT_PATH,
52
+ minScore: DEFAULT_MIN_SCORE,
53
+ recipesDir: DEFAULT_RECIPES_DIR,
54
+ unknownArgs: [],
55
+ };
56
+
57
+ argv.forEach((arg) => {
58
+ if (arg.startsWith("--cacheKey=")) {
59
+ options.cacheKey = arg.split("=").slice(1).join("=").trim();
60
+ return;
61
+ }
62
+ if (arg.startsWith("--target=")) {
63
+ options.targetPath = arg.split("=").slice(1).join("=").trim();
64
+ return;
65
+ }
66
+ if (arg.startsWith("--contract=")) {
67
+ options.contractPath = arg.split("=").slice(1).join("=").trim() || DEFAULT_CONTRACT_PATH;
68
+ return;
69
+ }
70
+ if (arg.startsWith("--report=")) {
71
+ options.reportPath = arg.split("=").slice(1).join("=").trim() || DEFAULT_REPORT_PATH;
72
+ return;
73
+ }
74
+ if (arg.startsWith("--min-score=")) {
75
+ const parsed = Number(arg.split("=").slice(1).join("=").trim());
76
+ options.minScore = Number.isFinite(parsed) ? parsed : DEFAULT_MIN_SCORE;
77
+ return;
78
+ }
79
+ if (arg.startsWith("--recipes-dir=")) {
80
+ options.recipesDir = arg.split("=").slice(1).join("=").trim() || DEFAULT_RECIPES_DIR;
81
+ return;
82
+ }
83
+ options.unknownArgs.push(arg);
84
+ });
85
+
86
+ return options;
87
+ }
88
+
89
+ function clampScore(input) {
90
+ const n = Number(input);
91
+ if (!Number.isFinite(n)) {
92
+ return 0;
93
+ }
94
+ return Math.max(0, Math.min(100, Math.round(n)));
95
+ }
96
+
97
+ function ensureParentDir(absPath) {
98
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
99
+ }
100
+
101
+ function average(values) {
102
+ if (!values.length) {
103
+ return 0;
104
+ }
105
+ return values.reduce((acc, n) => acc + n, 0) / values.length;
106
+ }
107
+
108
+ function readVariableDefsFromManifest(metaPath) {
109
+ if (!metaPath) {
110
+ return null;
111
+ }
112
+ const nodeDir = path.dirname(metaPath);
113
+ const manifestPath = path.join(nodeDir, "mcp-raw", "mcp-raw-manifest.json");
114
+ const manifest = readJsonOrNull(manifestPath);
115
+ if (
116
+ !manifest ||
117
+ typeof manifest !== "object" ||
118
+ !manifest.files ||
119
+ typeof manifest.files !== "object" ||
120
+ !manifest.files.get_variable_defs
121
+ ) {
122
+ return null;
123
+ }
124
+ return readJsonOrNull(path.join(nodeDir, "mcp-raw", String(manifest.files.get_variable_defs)));
125
+ }
126
+
127
+ function loadRecipes(recipesDirAbs) {
128
+ if (!recipesDirAbs || !fs.existsSync(recipesDirAbs)) {
129
+ return [];
130
+ }
131
+ return fs
132
+ .readdirSync(recipesDirAbs)
133
+ .filter((name) => name.endsWith(".json"))
134
+ .map((name) => {
135
+ const full = path.join(recipesDirAbs, name);
136
+ const data = readJsonOrNull(full);
137
+ if (!data || typeof data !== "object") {
138
+ return null;
139
+ }
140
+ return data;
141
+ })
142
+ .filter(Boolean);
143
+ }
144
+
145
+ function detectMatchedRecipes(recipes, contextText, statesInCache) {
146
+ const context = String(contextText || "").toLowerCase();
147
+ return recipes
148
+ .map((recipe) => {
149
+ const keywords = [
150
+ String(recipe.id || "").toLowerCase(),
151
+ ...(Array.isArray(recipe.structureTemplate) ? recipe.structureTemplate : []),
152
+ ...(recipe.stateMachine && Array.isArray(recipe.stateMachine.requiredStates)
153
+ ? recipe.stateMachine.requiredStates
154
+ : []),
155
+ ]
156
+ .map((k) => String(k || "").toLowerCase())
157
+ .filter(Boolean);
158
+ const matchedKeywords = keywords.filter(
159
+ (keyword) => context.includes(keyword) || statesInCache.includes(keyword)
160
+ );
161
+ return {
162
+ id: String(recipe.id || "").toLowerCase(),
163
+ score: keywords.length ? matchedKeywords.length / keywords.length : 0,
164
+ };
165
+ })
166
+ .filter((item) => item.score >= 0.2)
167
+ .sort((a, b) => b.score - a.score)
168
+ .slice(0, 3)
169
+ .map((item) => item.id);
170
+ }
171
+
172
+ function scoreItem(params) {
173
+ const { cacheKey, item, contract, targetCode, recipes } = params;
174
+ const blocking = [];
175
+ const warnings = [];
176
+ const diffs = [];
177
+
178
+ if (!item) {
179
+ blocking.push(`cache item not found: ${cacheKey}`);
180
+ return {
181
+ cacheKey,
182
+ score: {
183
+ total: 0,
184
+ layout: 0,
185
+ text: 0,
186
+ token: 0,
187
+ state: 0,
188
+ interaction: 0,
189
+ },
190
+ blocking,
191
+ warnings,
192
+ diffs,
193
+ };
194
+ }
195
+
196
+ const paths = item.paths && typeof item.paths === "object" ? item.paths : {};
197
+ const metaPath = paths.meta ? resolveMaybeAbsolutePath(paths.meta) : "";
198
+ const specPath = paths.spec ? resolveMaybeAbsolutePath(paths.spec) : "";
199
+ const stateMapPath = paths.stateMap ? resolveMaybeAbsolutePath(paths.stateMap) : "";
200
+ const rawPath = paths.raw ? resolveMaybeAbsolutePath(paths.raw) : "";
201
+ const entryReady = [metaPath, specPath, stateMapPath, rawPath].every((p) => !!p && fs.existsSync(p));
202
+ if (!entryReady) {
203
+ blocking.push("entry files not complete");
204
+ }
205
+
206
+ const specText = readTextOrEmpty(specPath);
207
+ const stateMapText = readTextOrEmpty(stateMapPath);
208
+ const rawJson = readJsonOrNull(rawPath) || {};
209
+ const completeness = Array.isArray(item.completeness) ? item.completeness : [];
210
+ const evidence =
211
+ rawJson.coverageSummary &&
212
+ rawJson.coverageSummary.evidence &&
213
+ typeof rawJson.coverageSummary.evidence === "object"
214
+ ? rawJson.coverageSummary.evidence
215
+ : {};
216
+ const evidenceReady = completeness.every((k) => Array.isArray(evidence[k]) && evidence[k].length > 0);
217
+ if (!evidenceReady) {
218
+ blocking.push("coverage evidence incomplete");
219
+ }
220
+
221
+ const variableDefsJson = readVariableDefsFromManifest(metaPath);
222
+ const normalizedFacts = normalizeUiFacts({
223
+ specText,
224
+ stateMapText,
225
+ rawJson,
226
+ variableDefsJson,
227
+ entryReady,
228
+ evidenceReady,
229
+ });
230
+ const textFacts = normalizedFacts.facts.text;
231
+ const tokenFacts = normalizedFacts.facts.tokens;
232
+ const statesInCache = normalizedFacts.facts.states;
233
+
234
+ const contractTokens = Array.isArray(contract && contract.tokenMappings) ? contract.tokenMappings : [];
235
+ const contractStates =
236
+ contract && contract.stateMappings && typeof contract.stateMappings === "object"
237
+ ? Object.values(contract.stateMappings)
238
+ .flatMap((entry) => (Array.isArray(entry.requiredStates) ? entry.requiredStates : []))
239
+ .map((v) => String(v || "").trim().toLowerCase())
240
+ .filter(Boolean)
241
+ : [];
242
+
243
+ const tokenMappedHits = tokenFacts.filter((fact) => {
244
+ const name = String(fact.name || "").trim().toLowerCase();
245
+ const value = normalizeHexColor(String(fact.value || ""));
246
+ return contractTokens.some((token) => {
247
+ const tokenName = String(token.figmaToken || "").trim().toLowerCase();
248
+ const tokenValue = normalizeHexColor(String(token.figmaValue || ""));
249
+ return (name && name === tokenName) || (value && value === tokenValue);
250
+ });
251
+ }).length;
252
+ const tokenCoverage = tokenFacts.length ? tokenMappedHits / tokenFacts.length : 1;
253
+ if (tokenCoverage < 1) {
254
+ diffs.push(`token mapping coverage ${Math.round(tokenCoverage * 100)}%`);
255
+ }
256
+
257
+ const stateHits = statesInCache.filter((state) => contractStates.includes(state)).length;
258
+ const stateCoverage = statesInCache.length ? stateHits / statesInCache.length : 1;
259
+ if (stateCoverage < 1) {
260
+ diffs.push(`state mapping coverage ${Math.round(stateCoverage * 100)}%`);
261
+ }
262
+
263
+ const hasTodo = normalizedFacts.hasPlaceholder;
264
+ const matchedRecipes = detectMatchedRecipes(
265
+ recipes,
266
+ `${specText}\n${stateMapText}\n${JSON.stringify(rawJson || {})}\n${targetCode}`,
267
+ statesInCache
268
+ );
269
+ if (!matchedRecipes.length) {
270
+ warnings.push("no recipe matched; consider adding project recipe");
271
+ }
272
+ if (hasTodo) {
273
+ warnings.push("cache facts still contain placeholder text");
274
+ }
275
+
276
+ const hasTargetCode = !!targetCode;
277
+ if (!hasTargetCode) {
278
+ warnings.push("target component path not provided; code-level comparison skipped");
279
+ }
280
+
281
+ const textCodeHits = hasTargetCode
282
+ ? textFacts.filter((fact) => targetCode.includes(fact)).length
283
+ : textFacts.length;
284
+ const tokenCodeHits = hasTargetCode
285
+ ? tokenFacts.filter((fact) =>
286
+ targetCode.toUpperCase().includes(String(normalizeHexColor(fact.value || "")).toUpperCase())
287
+ ).length
288
+ : tokenFacts.length;
289
+ const stateCodeHits = hasTargetCode
290
+ ? statesInCache.filter((state) => targetCode.toLowerCase().includes(state)).length
291
+ : statesInCache.length;
292
+
293
+ const layoutScore = entryReady ? 100 : 20;
294
+ const textScore = clampScore(100 * (textFacts.length ? textCodeHits / textFacts.length : 1));
295
+ const tokenScore = clampScore(100 * tokenCoverage * (tokenFacts.length ? tokenCodeHits / tokenFacts.length : 1));
296
+ const stateScore = clampScore(100 * stateCoverage * (statesInCache.length ? stateCodeHits / statesInCache.length : 1));
297
+ const interactionScore = hasTodo ? 70 : normalizedFacts.dimensions.interactionReady ? 100 : 80;
298
+
299
+ let totalScore = clampScore(average([layoutScore, textScore, tokenScore, stateScore, interactionScore]));
300
+ if (!hasTargetCode) {
301
+ // Baseline mode: without target code, keep gate usable while surfacing warnings/diffs.
302
+ // Strict comparison should pass --target and/or a higher --min-score.
303
+ if (!blocking.length) {
304
+ totalScore = Math.max(totalScore, 90);
305
+ }
306
+ }
307
+
308
+ return {
309
+ cacheKey,
310
+ score: {
311
+ total: totalScore,
312
+ layout: layoutScore,
313
+ text: textScore,
314
+ token: tokenScore,
315
+ state: stateScore,
316
+ interaction: interactionScore,
317
+ },
318
+ blocking,
319
+ warnings,
320
+ diffs,
321
+ matchedRecipes,
322
+ };
323
+ }
324
+
325
+ function run() {
326
+ const options = parseArgs(process.argv.slice(2));
327
+ const profileConfig = getUiProfileConfig();
328
+ const minScore =
329
+ options.minScore === DEFAULT_MIN_SCORE
330
+ ? profileConfig.auditDefaultMinScore
331
+ : options.minScore;
332
+ if (options.unknownArgs.length) {
333
+ console.error(`Unknown args: ${options.unknownArgs.join(", ")}`);
334
+ process.exit(FAIL_EXIT_CODE);
335
+ }
336
+
337
+ const cacheDir = resolveMaybeAbsolutePath(CACHE_DIR_INPUT);
338
+ const indexPath = path.isAbsolute(INDEX_FILE_NAME)
339
+ ? INDEX_FILE_NAME
340
+ : path.join(cacheDir, INDEX_FILE_NAME);
341
+ const contractPath = resolveMaybeAbsolutePath(options.contractPath);
342
+ const reportPath = resolveMaybeAbsolutePath(options.reportPath);
343
+ const recipesDir = resolveMaybeAbsolutePath(options.recipesDir);
344
+ const targetPath = options.targetPath ? resolveMaybeAbsolutePath(options.targetPath) : "";
345
+ if (profileConfig.auditRequireTargetPath && !targetPath) {
346
+ console.error(`ui-1to1-audit failed: profile '${profileConfig.profile}' requires --target`);
347
+ process.exit(FAIL_EXIT_CODE);
348
+ }
349
+
350
+ const index = readJsonOrNull(indexPath);
351
+ const contract = readJsonOrNull(contractPath);
352
+ const items = index && index.items && typeof index.items === "object" ? index.items : {};
353
+ const targetKeys = options.cacheKey ? [options.cacheKey] : Object.keys(items);
354
+ const targetCode = targetPath ? readTextOrEmpty(targetPath) : "";
355
+ const recipes = loadRecipes(recipesDir);
356
+
357
+ const itemReports = targetKeys.map((cacheKey) =>
358
+ scoreItem({
359
+ cacheKey,
360
+ item: items[cacheKey],
361
+ contract,
362
+ targetCode,
363
+ recipes,
364
+ })
365
+ );
366
+
367
+ const blocking = [];
368
+ if (!index || typeof index !== "object") {
369
+ blocking.push("index missing or invalid");
370
+ }
371
+ if (!contract || typeof contract !== "object") {
372
+ blocking.push("contract missing or invalid");
373
+ }
374
+ itemReports.forEach((item) => item.blocking.forEach((msg) => blocking.push(`${item.cacheKey}: ${msg}`)));
375
+
376
+ const warnings = itemReports.flatMap((item) => item.warnings.map((msg) => `${item.cacheKey}: ${msg}`));
377
+ const diffs = itemReports.flatMap((item) => item.diffs.map((msg) => `${item.cacheKey}: ${msg}`));
378
+ const totalScore = clampScore(average(itemReports.map((item) => item.score.total)));
379
+
380
+ if (totalScore < minScore) {
381
+ blocking.push(`score.total below threshold: ${totalScore} < ${minScore}`);
382
+ }
383
+
384
+ const report = {
385
+ ok: blocking.length === 0,
386
+ generatedAt: new Date().toISOString(),
387
+ summary: {
388
+ checkedItems: itemReports.length,
389
+ score: {
390
+ total: totalScore,
391
+ layout: clampScore(average(itemReports.map((item) => item.score.layout))),
392
+ text: clampScore(average(itemReports.map((item) => item.score.text))),
393
+ token: clampScore(average(itemReports.map((item) => item.score.token))),
394
+ state: clampScore(average(itemReports.map((item) => item.score.state))),
395
+ interaction: clampScore(average(itemReports.map((item) => item.score.interaction))),
396
+ },
397
+ blockingCount: blocking.length,
398
+ warningCount: warnings.length,
399
+ diffCount: diffs.length,
400
+ minScore,
401
+ profile: profileConfig.profile,
402
+ recipesTotal: recipes.length,
403
+ recipesMatchedItems: itemReports.filter((item) => Array.isArray(item.matchedRecipes) && item.matchedRecipes.length > 0)
404
+ .length,
405
+ },
406
+ options: {
407
+ cacheKey: options.cacheKey || null,
408
+ targetPath: targetPath ? normalizeSlash(targetPath) : null,
409
+ contractPath: normalizeSlash(contractPath),
410
+ reportPath: normalizeSlash(reportPath),
411
+ recipesDir: normalizeSlash(recipesDir),
412
+ },
413
+ blocking,
414
+ warnings,
415
+ diffs,
416
+ items: itemReports,
417
+ };
418
+
419
+ ensureParentDir(reportPath);
420
+ fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
421
+
422
+ if (!report.ok) {
423
+ console.error("ui-1to1-audit failed:");
424
+ blocking.forEach((msg) => console.error(`- ${msg}`));
425
+ process.exit(FAIL_EXIT_CODE);
426
+ }
427
+
428
+ console.log(JSON.stringify(report, null, 2));
429
+ }
430
+
431
+ run();
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ "use strict";
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const { execSync } = require("child_process");
8
+
9
+ const ROOT = process.cwd();
10
+ const SCRIPT_DIR = __dirname;
11
+ const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
12
+ const FAIL_EXIT_CODE = 2;
13
+
14
+ function resolveMaybeAbsolutePath(input) {
15
+ if (!input) {
16
+ return "";
17
+ }
18
+ return path.isAbsolute(input) ? path.normalize(input) : path.join(ROOT, input);
19
+ }
20
+
21
+ function readJsonOrNull(absPath) {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(absPath, "utf8"));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function parseArgs(argv) {
30
+ const options = {
31
+ cacheKey: "",
32
+ target: "",
33
+ contract: "",
34
+ minScore: 90,
35
+ maxWarnings: 0,
36
+ maxDiffs: 2,
37
+ reportsOnly: false,
38
+ preflightReport: "",
39
+ auditReport: "",
40
+ summaryReport: "",
41
+ };
42
+
43
+ argv.forEach((arg) => {
44
+ if (arg.startsWith("--cacheKey=")) {
45
+ options.cacheKey = arg.split("=").slice(1).join("=").trim();
46
+ return;
47
+ }
48
+ if (arg.startsWith("--target=")) {
49
+ options.target = arg.split("=").slice(1).join("=").trim();
50
+ return;
51
+ }
52
+ if (arg.startsWith("--contract=")) {
53
+ options.contract = arg.split("=").slice(1).join("=").trim();
54
+ return;
55
+ }
56
+ if (arg.startsWith("--min-score=")) {
57
+ const n = Number(arg.split("=").slice(1).join("=").trim());
58
+ options.minScore = Number.isFinite(n) ? n : options.minScore;
59
+ return;
60
+ }
61
+ if (arg.startsWith("--max-warnings=")) {
62
+ const n = Number(arg.split("=").slice(1).join("=").trim());
63
+ options.maxWarnings = Number.isFinite(n) ? n : options.maxWarnings;
64
+ return;
65
+ }
66
+ if (arg.startsWith("--max-diffs=")) {
67
+ const n = Number(arg.split("=").slice(1).join("=").trim());
68
+ options.maxDiffs = Number.isFinite(n) ? n : options.maxDiffs;
69
+ return;
70
+ }
71
+ if (arg.startsWith("--preflight-report=")) {
72
+ options.preflightReport = arg.split("=").slice(1).join("=").trim();
73
+ return;
74
+ }
75
+ if (arg.startsWith("--audit-report=")) {
76
+ options.auditReport = arg.split("=").slice(1).join("=").trim();
77
+ return;
78
+ }
79
+ if (arg.startsWith("--summary-report=")) {
80
+ options.summaryReport = arg.split("=").slice(1).join("=").trim();
81
+ return;
82
+ }
83
+ if (arg === "--reports-only") {
84
+ options.reportsOnly = true;
85
+ }
86
+ });
87
+
88
+ return options;
89
+ }
90
+
91
+ function runOrExit(command) {
92
+ try {
93
+ execSync(command, {
94
+ cwd: ROOT,
95
+ stdio: "inherit",
96
+ encoding: "utf8",
97
+ });
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ function buildReportPaths(options) {
105
+ const cacheDir = resolveMaybeAbsolutePath(CACHE_DIR_INPUT);
106
+ return {
107
+ preflight: resolveMaybeAbsolutePath(
108
+ options.preflightReport || path.join(cacheDir, "reports", "ui-preflight-report.json")
109
+ ),
110
+ audit: resolveMaybeAbsolutePath(
111
+ options.auditReport || path.join(cacheDir, "reports", "ui-1to1-report.json")
112
+ ),
113
+ summary: resolveMaybeAbsolutePath(
114
+ options.summaryReport || path.join(cacheDir, "reports", "ui-quality-summary.json")
115
+ ),
116
+ };
117
+ }
118
+
119
+ function evaluate(preflight, audit, summary, options) {
120
+ const failures = [];
121
+ const warnings = [];
122
+
123
+ if (!preflight || typeof preflight !== "object") {
124
+ failures.push("preflight report missing or invalid");
125
+ } else {
126
+ if (preflight.ok !== true) {
127
+ failures.push("preflight.ok is not true");
128
+ }
129
+ const blockingCount = Number(preflight.summary && preflight.summary.blockingCount || 0);
130
+ if (blockingCount > 0) {
131
+ failures.push(`preflight blocking count > 0 (${blockingCount})`);
132
+ }
133
+ }
134
+
135
+ if (!audit || typeof audit !== "object") {
136
+ failures.push("audit report missing or invalid");
137
+ } else {
138
+ if (audit.ok !== true) {
139
+ failures.push("audit.ok is not true");
140
+ }
141
+ const score = Number(audit.summary && audit.summary.score && audit.summary.score.total || 0);
142
+ if (score < options.minScore) {
143
+ failures.push(`audit total score too low (${score} < ${options.minScore})`);
144
+ }
145
+ const warningCount = Number(audit.summary && audit.summary.warningCount || 0);
146
+ if (warningCount > options.maxWarnings) {
147
+ failures.push(`audit warnings too many (${warningCount} > ${options.maxWarnings})`);
148
+ }
149
+ const diffCount = Number(audit.summary && audit.summary.diffCount || 0);
150
+ if (diffCount > options.maxDiffs) {
151
+ failures.push(`audit diffs too many (${diffCount} > ${options.maxDiffs})`);
152
+ }
153
+ const targetPath = audit.options && audit.options.targetPath;
154
+ if (!targetPath) {
155
+ failures.push("audit targetPath is empty; not linked to real component");
156
+ }
157
+ if (Array.isArray(audit.warnings) && audit.warnings.length) {
158
+ audit.warnings.forEach((entry) => warnings.push(entry));
159
+ }
160
+ }
161
+
162
+ if (!summary || typeof summary !== "object") {
163
+ failures.push("aggregate summary report missing or invalid");
164
+ } else {
165
+ const status = String(summary.trend && summary.trend.status || "");
166
+ if (status && status !== "healthy") {
167
+ failures.push(`summary trend is not healthy (${status})`);
168
+ }
169
+ }
170
+
171
+ return {
172
+ ok: failures.length === 0,
173
+ failures,
174
+ warnings,
175
+ };
176
+ }
177
+
178
+ function run() {
179
+ const options = parseArgs(process.argv.slice(2));
180
+ const target = options.target ? resolveMaybeAbsolutePath(options.target) : "";
181
+ const contract = options.contract ? resolveMaybeAbsolutePath(options.contract) : "";
182
+ const reportPaths = buildReportPaths(options);
183
+
184
+ if (!options.reportsOnly) {
185
+ const preflightArgs = [];
186
+ if (options.cacheKey) {
187
+ preflightArgs.push(`--cacheKey=${options.cacheKey}`);
188
+ }
189
+ if (contract) {
190
+ preflightArgs.push(`--contract=${contract}`);
191
+ }
192
+ const preflightScript = path.join(SCRIPT_DIR, "ui-preflight.js");
193
+ if (!runOrExit(`node "${preflightScript}" ${preflightArgs.join(" ")}`.trim())) {
194
+ process.exit(FAIL_EXIT_CODE);
195
+ }
196
+
197
+ const auditArgs = [];
198
+ if (options.cacheKey) {
199
+ auditArgs.push(`--cacheKey=${options.cacheKey}`);
200
+ }
201
+ if (target) {
202
+ auditArgs.push(`--target=${target}`);
203
+ }
204
+ if (contract) {
205
+ auditArgs.push(`--contract=${contract}`);
206
+ }
207
+ auditArgs.push(`--min-score=${options.minScore}`);
208
+ const auditScript = path.join(SCRIPT_DIR, "ui-1to1-audit.js");
209
+ if (!runOrExit(`node "${auditScript}" ${auditArgs.join(" ")}`.trim())) {
210
+ process.exit(FAIL_EXIT_CODE);
211
+ }
212
+
213
+ const aggregateScript = path.join(SCRIPT_DIR, "ui-report-aggregate.js");
214
+ if (!runOrExit(`node "${aggregateScript}"`)) {
215
+ process.exit(FAIL_EXIT_CODE);
216
+ }
217
+ }
218
+
219
+ const preflight = readJsonOrNull(reportPaths.preflight);
220
+ const audit = readJsonOrNull(reportPaths.audit);
221
+ const summary = readJsonOrNull(reportPaths.summary);
222
+ const verdict = evaluate(preflight, audit, summary, options);
223
+ const output = {
224
+ ok: verdict.ok,
225
+ generatedAt: new Date().toISOString(),
226
+ options: {
227
+ cacheKey: options.cacheKey || null,
228
+ target: target || null,
229
+ minScore: options.minScore,
230
+ maxWarnings: options.maxWarnings,
231
+ maxDiffs: options.maxDiffs,
232
+ reportsOnly: options.reportsOnly,
233
+ },
234
+ reports: reportPaths,
235
+ failures: verdict.failures,
236
+ warnings: verdict.warnings,
237
+ };
238
+
239
+ if (!output.ok) {
240
+ console.error("ui-auto-acceptance failed:");
241
+ output.failures.forEach((entry) => console.error(`- ${entry}`));
242
+ process.exit(FAIL_EXIT_CODE);
243
+ }
244
+
245
+ console.log(JSON.stringify(output, null, 2));
246
+ }
247
+
248
+ run();