figma-cache-toolchain 2.0.4 → 2.0.5

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 CHANGED
@@ -125,7 +125,6 @@ npm run figma:ui:preflight
125
125
  npm run figma:ui:audit -- --min-score=85
126
126
  npm run figma:ui:report:aggregate
127
127
  npm run figma:ui:accept -- --target=src/components/YourComponent.tsx
128
- npm run figma:ui:e2e:cross -- --target-project=E:/Work/vue-demo --fileKey=<fileKey> --nodeId=9277-28772 --target=E:/Work/vue-demo/src/components/YourComponent.vue
129
128
  npm run figma:ui:gate
130
129
  npm run figma:ui:gate:pr
131
130
  npm run figma:ui:gate:main
@@ -144,11 +143,8 @@ UI preflight/gate 说明:
144
143
  - `figma:ui:gate` 会先跑 preflight + audit(默认阈值 `85`),再串联 `validate`、`cursor:shadow:check` 与 `npm test`
145
144
  - `figma:ui:report:aggregate` 会聚合 preflight + audit 报告,输出 `figma-cache/reports/ui-quality-summary.json`
146
145
  - `figma:ui:accept` 是一键自动验收:自动跑 preflight + audit + aggregate,并按效果阈值直接返回 pass/fail(退出码)
147
- - `figma:ui:e2e:cross` 是跨项目联调:自动 `npm pack` 当前包 -> 安装到目标项目 -> 执行自动验收 -> 回收报告路径与摘要
148
- - 支持 `--auto-ensure-on-miss`:cache miss 时自动尝试 `--source=figma-mcp ensure`
149
- - 支持 `--batch-file=<json>`:批量节点联调并汇总结果(单条失败即整体失败)
150
- - 支持 `--fix-loop=<N>`:失败后自动执行自修复重试(补 contract / 刷新缓存后重跑)
151
146
  - CI 建议矩阵:`figma:ui:gate:pr`(PR 最低门槛)与 `figma:ui:gate:main`(主干严格门槛)
147
+ - `figma:ui:e2e:cross` 现默认启用“真实组件链路保护”:`--target` 不存在或验收出现 `code-level comparison skipped` 会直接失败,避免“未绑定真实组件但通过”的假阳性;如需兼容历史流程可显式传 `--allow-skipped-code-level-comparison`
152
148
 
153
149
  UI profile 分层(P3):
154
150
 
@@ -83,7 +83,7 @@ npm run figma:cache:config
83
83
 
84
84
  ### UI preflight(P0 门禁)
85
85
 
86
- - `npm run figma:ui:preflight`:读取 `index.json`、adapter contract 与节点关键文件,输出结构化报告到 `figma-cache/reports/ui-preflight-report.json`
86
+ - `npm run figma:ui:preflight`:读取 `index.json`、adapter contract 与节点关键文件,输出结构化报告到 `figma-cache/reports/runtime/ui-preflight-report.json`
87
87
  - 支持参数:`--cacheKey=<fileKey#nodeId>`、`--contract=<path>`、`--report=<path>`、`--allow-warn`
88
88
  - 阻断项返回退出码 `2`:包括 cacheKey 不存在、关键文件缺失、coverage evidence 不完整、contract 缺失或映射为空、`source=figma-mcp` 时缺失 `mcp-raw-manifest.json`
89
89
  - warning 项(不阻断)会提示 `spec.md`/`state-map.md` 中的 TODO 占位
@@ -97,7 +97,7 @@ npm run figma:cache:config
97
97
  ### UI 1:1 audit(P1 质量评分)
98
98
 
99
99
  - `npm run figma:ui:audit -- --cacheKey=<fileKey#nodeId> --target=<componentPath> --min-score=85`
100
- - 默认报告:`figma-cache/reports/ui-1to1-report.json`
100
+ - 默认报告:`figma-cache/reports/runtime/ui-1to1-report.json`
101
101
  - 报告结构遵循:`figma-cache/docs/ui-1to1-report.schema.json`
102
102
  - 评分字段:`score.total/layout/text/token/state/interaction`
103
103
  - `score.total` 低于 `--min-score` 会返回退出码 `2`(可用于 CI 门禁)
@@ -128,7 +128,7 @@ npm run figma:cache:config
128
128
  - `standard`:audit 默认阈值 85
129
129
  - `strict`:preflight warning 计入阻断、audit 默认阈值 92 且要求 `--target`
130
130
  - 报告聚合:`npm run figma:ui:report:aggregate`
131
- - 输出:`figma-cache/reports/ui-quality-summary.json`
131
+ - 输出:`figma-cache/reports/runtime/ui-quality-summary.json`
132
132
 
133
133
  ### 一键自动验收(效果导向)
134
134
 
@@ -157,6 +157,7 @@ npm run figma:cache:config
157
157
  - `--allow-skeleton-with-figma-mcp`:允许 skeleton 写入(仅建议应急)
158
158
  - `--batch-file=<json>`:批量执行,多节点一次联调
159
159
  - `--fix-loop=<N>`:失败自动重试 N 轮(重试前会补 contract 并刷新缓存)
160
+ - `--emit-agent-task-on-fail`:失败自动产出 `agent-task.md`,用于交给 Cursor Agent 接力修复
160
161
 
161
162
  ### 严格 validate 规则(默认)
162
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-cache-toolchain",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "Figma link normalization, local cache index, validation, and Node CLI (framework-agnostic core).",
5
5
  "homepage": "https://github.com/907086379/figma-cache-toolchain#readme",
6
6
  "keywords": [
@@ -34,6 +34,9 @@ function parseArgs(argv) {
34
34
  completeness: "layout,text,tokens,interactions,states,accessibility",
35
35
  batchFile: "",
36
36
  fixLoop: 0,
37
+ emitAgentTaskOnFail: false,
38
+ agentTaskPath: "",
39
+ allowSkippedCodeLevelComparison: false,
37
40
  };
38
41
 
39
42
  argv.forEach((arg) => {
@@ -103,6 +106,18 @@ function parseArgs(argv) {
103
106
  if (arg.startsWith("--fix-loop=")) {
104
107
  const n = Number(arg.split("=").slice(1).join("=").trim());
105
108
  options.fixLoop = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
109
+ return;
110
+ }
111
+ if (arg === "--emit-agent-task-on-fail") {
112
+ options.emitAgentTaskOnFail = true;
113
+ return;
114
+ }
115
+ if (arg.startsWith("--agent-task-path=")) {
116
+ options.agentTaskPath = arg.split("=").slice(1).join("=").trim();
117
+ return;
118
+ }
119
+ if (arg === "--allow-skipped-code-level-comparison") {
120
+ options.allowSkippedCodeLevelComparison = true;
106
121
  }
107
122
  });
108
123
 
@@ -164,6 +179,63 @@ function normalizeSlash(input) {
164
179
  return String(input || "").replace(/\\/g, "/");
165
180
  }
166
181
 
182
+ function writeAgentTask(targetProject, options, payload) {
183
+ const defaultPath = path.join(targetProject, "agent-task.md");
184
+ const taskPath = options.agentTaskPath
185
+ ? resolveMaybeAbsolutePath(options.agentTaskPath)
186
+ : defaultPath;
187
+ const lines = [];
188
+ lines.push("# Agent Task: UI E2E Recovery");
189
+ lines.push("");
190
+ lines.push("## Goal");
191
+ lines.push("Fix target project implementation so ui acceptance passes.");
192
+ lines.push("");
193
+ lines.push("## Constraints");
194
+ lines.push("- Must run ui acceptance after code changes.");
195
+ lines.push("- Do not bypass by lowering thresholds unless explicitly requested.");
196
+ lines.push("- Prioritize real component/contract/recipe fixes.");
197
+ lines.push("");
198
+ lines.push("## Context");
199
+ lines.push(`- targetProject: ${normalizeSlash(payload.targetProject || "")}`);
200
+ lines.push(`- mode: ${payload.mode || "single"}`);
201
+ lines.push(`- profile: ${payload.profile || "standard"}`);
202
+ lines.push(`- autoEnsureOnMiss: ${payload.autoEnsureOnMiss ? "true" : "false"}`);
203
+ lines.push(`- fixLoop: ${Number(payload.fixLoop || 0)}`);
204
+ lines.push("");
205
+ lines.push("## Cases");
206
+ (payload.cases || []).forEach((entry, idx) => {
207
+ lines.push(`### Case ${idx + 1}`);
208
+ lines.push(`- cacheKey: ${entry.cacheKey || ""}`);
209
+ lines.push(`- targetPath: ${normalizeSlash(entry.targetPath || "")}`);
210
+ lines.push(`- reason: ${entry.reason || "unknown"}`);
211
+ if (entry.attemptLogs && entry.attemptLogs.length) {
212
+ lines.push("- attemptLogs:");
213
+ entry.attemptLogs.forEach((log) => {
214
+ lines.push(
215
+ ` - attempt ${log.attempt}: ${log.ok ? "ok" : "fail"}${log.reason ? ` (${log.reason})` : ""}`
216
+ );
217
+ });
218
+ }
219
+ lines.push("");
220
+ });
221
+ lines.push("## Required Command");
222
+ lines.push("Run this command in toolchain repo after fixes:");
223
+ lines.push("");
224
+ lines.push("```bash");
225
+ lines.push(payload.retryCommand || "npm run figma:ui:e2e:cross -- --target-project=<...>");
226
+ lines.push("```");
227
+ lines.push("");
228
+ lines.push("## Completion Criteria");
229
+ lines.push("- e2e command exits with code 0");
230
+ lines.push("- summaryStatus is healthy");
231
+ lines.push("- no unresolved blocking items");
232
+ lines.push("");
233
+
234
+ fs.mkdirSync(path.dirname(taskPath), { recursive: true });
235
+ fs.writeFileSync(taskPath, `${lines.join("\n")}\n`, "utf8");
236
+ return taskPath;
237
+ }
238
+
167
239
  function parseCacheKey(cacheKey) {
168
240
  const value = String(cacheKey || "").trim();
169
241
  const [fileKey, nodeId] = value.split("#");
@@ -260,6 +332,9 @@ function runSingleCase(input, context) {
260
332
  if (!targetPath) {
261
333
  throw new Error(`batch item ${cacheKey} missing target path`);
262
334
  }
335
+ if (!fs.existsSync(targetPath)) {
336
+ throw new Error(`batch item ${cacheKey} target path does not exist: ${targetPath}`);
337
+ }
263
338
 
264
339
  const acceptArgs = [
265
340
  `--cacheKey=${cacheKey}`,
@@ -301,6 +376,18 @@ function runSingleCase(input, context) {
301
376
  try {
302
377
  acceptanceJson = JSON.parse(acceptanceOutput);
303
378
  } catch {}
379
+ if (
380
+ !options.allowSkippedCodeLevelComparison &&
381
+ acceptanceJson &&
382
+ Array.isArray(acceptanceJson.warnings) &&
383
+ acceptanceJson.warnings.some((entry) =>
384
+ /code-level comparison skipped/i.test(String(entry || ""))
385
+ )
386
+ ) {
387
+ throw new Error(
388
+ `acceptance produced skipped code-level comparison for ${cacheKey}; target linkage is invalid`
389
+ );
390
+ }
304
391
  attemptLogs.push({ attempt, ok: true });
305
392
  return {
306
393
  ok: true,
@@ -344,6 +431,10 @@ function run() {
344
431
  console.error("cross-project-e2e failed: --target is required for real component validation");
345
432
  process.exit(FAIL_EXIT_CODE);
346
433
  }
434
+ if (!fs.existsSync(targetPath)) {
435
+ console.error(`cross-project-e2e failed: --target path does not exist: ${targetPath}`);
436
+ process.exit(FAIL_EXIT_CODE);
437
+ }
347
438
  const cacheKey = resolveCacheKey(options);
348
439
  if (!cacheKey) {
349
440
  console.error("cross-project-e2e failed: provide --cacheKey or (--fileKey + --nodeId)");
@@ -352,6 +443,7 @@ function run() {
352
443
  }
353
444
 
354
445
  let tarballPath = "";
446
+ let taskPayload = null;
355
447
  try {
356
448
  tarballPath = npmPackAndGetTarball();
357
449
  runCommand(`npm i -D "${tarballPath}"`, targetProject);
@@ -404,11 +496,26 @@ function run() {
404
496
  caseFailures.push({
405
497
  index: indexNo,
406
498
  cacheKey: entry && (entry.cacheKey || resolveCacheKey(entry)),
499
+ targetPath: entry && entry.target,
500
+ attemptLogs: [],
407
501
  reason: error.message,
408
502
  });
409
503
  }
410
504
  });
411
505
  if (caseFailures.length) {
506
+ taskPayload = {
507
+ targetProject,
508
+ mode: isBatchMode ? "batch" : "single",
509
+ profile: options.profile || "standard",
510
+ autoEnsureOnMiss: options.autoEnsureOnMiss,
511
+ fixLoop: options.fixLoop,
512
+ cases: caseFailures,
513
+ retryCommand: `npm run figma:ui:e2e:cross -- --target-project=${normalizeSlash(
514
+ targetProject
515
+ )}${options.batchFile ? ` --batch-file=${normalizeSlash(options.batchFile)}` : ""}${
516
+ options.autoEnsureOnMiss ? " --auto-ensure-on-miss" : ""
517
+ }${options.fixLoop ? ` --fix-loop=${options.fixLoop}` : ""}`,
518
+ };
412
519
  throw new Error(`batch cases failed: ${JSON.stringify(caseFailures)}`);
413
520
  }
414
521
 
@@ -424,9 +531,9 @@ function run() {
424
531
  completeness: options.completeness,
425
532
  tarballPath,
426
533
  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"),
534
+ preflight: path.join(reportBase, "runtime", "ui-preflight-report.json"),
535
+ audit: path.join(reportBase, "runtime", "ui-1to1-report.json"),
536
+ summary: path.join(reportBase, "runtime", "ui-quality-summary.json"),
430
537
  },
431
538
  cases: caseResults,
432
539
  };
@@ -438,8 +545,42 @@ function run() {
438
545
  }
439
546
  console.log(JSON.stringify(output, null, 2));
440
547
  } catch (error) {
548
+ let taskPath = "";
549
+ if (options.emitAgentTaskOnFail) {
550
+ try {
551
+ const payload =
552
+ taskPayload ||
553
+ ({
554
+ targetProject,
555
+ mode: isBatchMode ? "batch" : "single",
556
+ profile: options.profile || "standard",
557
+ autoEnsureOnMiss: options.autoEnsureOnMiss,
558
+ fixLoop: options.fixLoop,
559
+ cases: [
560
+ {
561
+ cacheKey: resolveCacheKey(options),
562
+ targetPath: options.target,
563
+ reason: error.message,
564
+ },
565
+ ],
566
+ retryCommand: `npm run figma:ui:e2e:cross -- --target-project=${normalizeSlash(
567
+ targetProject
568
+ )}${options.batchFile ? ` --batch-file=${normalizeSlash(options.batchFile)}` : ""}${
569
+ options.cacheKey ? ` --cacheKey=${options.cacheKey}` : ""
570
+ }${options.fileKey ? ` --fileKey=${options.fileKey}` : ""}${
571
+ options.nodeId ? ` --nodeId=${options.nodeId}` : ""
572
+ }${options.target ? ` --target=${normalizeSlash(options.target)}` : ""}${
573
+ options.autoEnsureOnMiss ? " --auto-ensure-on-miss" : ""
574
+ }${options.fixLoop ? ` --fix-loop=${options.fixLoop}` : ""} --emit-agent-task-on-fail`,
575
+ });
576
+ taskPath = writeAgentTask(targetProject, options, payload);
577
+ } catch {}
578
+ }
441
579
  console.error("cross-project-e2e failed:");
442
580
  console.error(`- ${error.message}`);
581
+ if (taskPath) {
582
+ console.error(`- agent task emitted: ${normalizeSlash(taskPath)}`);
583
+ }
443
584
  process.exit(FAIL_EXIT_CODE);
444
585
  } finally {
445
586
  if (tarballPath && !options.keepPackage) {
@@ -11,7 +11,7 @@ const ROOT = process.cwd();
11
11
  const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
12
12
  const INDEX_FILE_NAME = process.env.FIGMA_CACHE_INDEX_FILE || "index.json";
13
13
  const DEFAULT_CONTRACT_PATH = "figma-cache/adapters/ui-adapter.contract.json";
14
- const DEFAULT_REPORT_PATH = "figma-cache/reports/ui-1to1-report.json";
14
+ const DEFAULT_REPORT_PATH = "figma-cache/reports/runtime/ui-1to1-report.json";
15
15
  const DEFAULT_MIN_SCORE = 85;
16
16
  const DEFAULT_RECIPES_DIR = "figma-cache/adapters/recipes";
17
17
  const FAIL_EXIT_CODE = 2;
@@ -105,13 +105,13 @@ function buildReportPaths(options) {
105
105
  const cacheDir = resolveMaybeAbsolutePath(CACHE_DIR_INPUT);
106
106
  return {
107
107
  preflight: resolveMaybeAbsolutePath(
108
- options.preflightReport || path.join(cacheDir, "reports", "ui-preflight-report.json")
108
+ options.preflightReport || path.join(cacheDir, "reports", "runtime", "ui-preflight-report.json")
109
109
  ),
110
110
  audit: resolveMaybeAbsolutePath(
111
- options.auditReport || path.join(cacheDir, "reports", "ui-1to1-report.json")
111
+ options.auditReport || path.join(cacheDir, "reports", "runtime", "ui-1to1-report.json")
112
112
  ),
113
113
  summary: resolveMaybeAbsolutePath(
114
- options.summaryReport || path.join(cacheDir, "reports", "ui-quality-summary.json")
114
+ options.summaryReport || path.join(cacheDir, "reports", "runtime", "ui-quality-summary.json")
115
115
  ),
116
116
  };
117
117
  }
@@ -10,7 +10,7 @@ const ROOT = process.cwd();
10
10
  const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
11
11
  const INDEX_FILE_NAME = process.env.FIGMA_CACHE_INDEX_FILE || "index.json";
12
12
  const DEFAULT_CONTRACT_PATH = "figma-cache/adapters/ui-adapter.contract.json";
13
- const DEFAULT_REPORT_PATH = "figma-cache/reports/ui-preflight-report.json";
13
+ const DEFAULT_REPORT_PATH = "figma-cache/reports/runtime/ui-preflight-report.json";
14
14
  const BLOCKING_EXIT_CODE = 2;
15
15
 
16
16
  function normalizeSlash(input) {
@@ -8,7 +8,7 @@ const { getUiProfileConfig } = require("./ui-profile");
8
8
 
9
9
  const ROOT = process.cwd();
10
10
  const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
11
- const DEFAULT_OUTPUT_PATH = "figma-cache/reports/ui-quality-summary.json";
11
+ const DEFAULT_OUTPUT_PATH = "figma-cache/reports/runtime/ui-quality-summary.json";
12
12
 
13
13
  function resolveMaybeAbsolutePath(input) {
14
14
  if (!input) {
@@ -62,10 +62,10 @@ function run() {
62
62
  const options = parseArgs(process.argv.slice(2));
63
63
  const cacheDir = resolveMaybeAbsolutePath(CACHE_DIR_INPUT);
64
64
  const preflightPath = resolveMaybeAbsolutePath(
65
- options.preflightReport || path.join(cacheDir, "reports", "ui-preflight-report.json")
65
+ options.preflightReport || path.join(cacheDir, "reports", "runtime", "ui-preflight-report.json")
66
66
  );
67
67
  const auditPath = resolveMaybeAbsolutePath(
68
- options.auditReport || path.join(cacheDir, "reports", "ui-1to1-report.json")
68
+ options.auditReport || path.join(cacheDir, "reports", "runtime", "ui-1to1-report.json")
69
69
  );
70
70
  const outputPath = resolveMaybeAbsolutePath(options.output);
71
71
  const profileConfig = getUiProfileConfig();