cardbuddy 0.15.0 → 0.16.0

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 (77) hide show
  1. package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
  2. package/dist/preview-editor/assets/{index-CIIfMlwI.js → index-Bro07DTa.js} +91 -88
  3. package/dist/preview-editor/index.html +2 -2
  4. package/dist/vendored/card-editor/runtime/adapter.js +2 -2
  5. package/dist/vendored/mcp/client.mjs +1 -1
  6. package/dist/vendored/server/core/ai-chat.js +251 -80
  7. package/dist/vendored/server/core/cardValidate.js +16 -5
  8. package/dist/vendored/skills/card-skill-trainer/SKILL.md +61 -6
  9. package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
  10. package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
  11. package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
  12. package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
  13. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
  14. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
  15. package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
  16. package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
  17. package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
  18. package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
  19. package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
  20. package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
  21. package/dist/vendored/skills/dsl-card-generator/SKILL.md +93 -27
  22. package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
  23. package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
  24. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +169 -44
  25. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +87 -3
  26. package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +35 -6
  27. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +28 -27
  28. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +30 -8
  29. package/dist/vendored/skills/dsl-card-generator/references/rules.md +3 -2
  30. package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +5 -2
  31. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +2 -3
  32. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +1 -4
  33. package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
  34. package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +88 -0
  35. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +143 -42
  36. package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
  37. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +99 -53
  38. package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +5 -2
  39. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +388 -29
  40. package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
  41. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
  42. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
  43. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
  44. package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
  45. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
  46. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
  47. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
  48. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
  49. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
  50. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
  51. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
  52. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
  53. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +325 -41
  54. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +112 -6
  55. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +0 -6
  56. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +119 -78
  57. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +48 -50
  58. package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
  59. package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
  60. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +5 -10
  61. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +122 -8
  62. package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +3 -1
  63. package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
  64. package/dist/vendored/skills/shared/exit-codes.md +79 -0
  65. package/dist/vendored/skills/shared/spec-contract.md +118 -0
  66. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +26 -9
  67. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +3 -3
  68. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +0 -8
  69. package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
  70. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +91 -47
  71. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +0 -5
  72. package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +75 -15
  73. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +40 -34
  74. package/package.json +1 -1
  75. package/dist/preview-editor/assets/index-DQn0zQBP.css +0 -1
  76. package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
  77. package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
@@ -17,7 +17,8 @@ const VALIDATE_SCRIPT_REL = 'dsl-card-generator/scripts/validate_schema.py'
17
17
  * - GET /__validate :全量校验,逐卡跑 Python 校验器,汇总返回
18
18
  *
19
19
  * 实现:子进程调用 `skills/dsl-card-generator/scripts/validate_schema.py --json`。
20
- * Python 解释器解析顺序:`DSL_PYTHON` → `python3` → `python`,都不存在时返回
20
+ * Python 解释器解析顺序:`DSL_CARD_PYTHON`(权威)→ `WORKBUDDY_PYTHON` → `DSL_PYTHON`
21
+ * → `PYTHON` → 探测 `python3` / `python` / `py`;都不存在时返回
21
22
  * **503 {error:'python-not-found'}** 而非 500,让 Agent / CI 能区分「环境没装」和「卡片有问题」。
22
23
  *
23
24
  * @param {{ root: string }} opts
@@ -32,10 +33,20 @@ export function scriptPath(root) {
32
33
  return resolveSkillPathOrProject(root, VALIDATE_SCRIPT_REL)
33
34
  }
34
35
 
35
- /** 解析可用的 python 解释器;找不到返回 null(导出供 MCP 复用) */
36
+ /**
37
+ * 解析可用的 python 解释器;找不到返回 null(导出供 MCP 复用)。
38
+ *
39
+ * 能力对齐 `skills/dsl-card-generator/scripts/lib/python.mjs`(权威实现):环境变量链
40
+ * 四个键 + 自动探测含 `py`。此前只认 `DSL_PYTHON` 且不含 `py` —— Windows 上 Python 从
41
+ * Store 安装时往往只有 `py` 启动器,会在这里被判成「环境没装 Python」并回 503,
42
+ * 而 skill 脚本侧其实是能跑的(同一台机器两种结论)。
43
+ */
36
44
  export function resolvePython() {
37
- if (process.env.DSL_PYTHON) return process.env.DSL_PYTHON
38
- for (const cmd of ['python3', 'python']) {
45
+ for (const key of ['DSL_CARD_PYTHON', 'WORKBUDDY_PYTHON', 'DSL_PYTHON', 'PYTHON']) {
46
+ const val = process.env[key]
47
+ if (val) return val
48
+ }
49
+ for (const cmd of ['python3', 'python', 'py']) {
39
50
  try {
40
51
  execFileSync(cmd, ['--version'], { stdio: 'ignore', timeout: 5000 })
41
52
  return cmd
@@ -92,7 +103,7 @@ export function createValidateMiddleware({ root }) {
92
103
  res.setHeader('Content-Type', 'application/json')
93
104
  return res.end(JSON.stringify({
94
105
  error: 'python-not-found',
95
- message: '未找到 python3/python,请在 PATH 中安装 Python 或设置 DSL_PYTHON 环境变量',
106
+ message: '未找到 python3/python/py,请在 PATH 中安装 Python 或设置 DSL_CARD_PYTHON 环境变量',
96
107
  }))
97
108
  }
98
109
 
@@ -49,7 +49,7 @@ DSL_CARD_SLICES_DIR=<training-run>/slices
49
49
  5. 复制 schema 实际引用的 slices 到当前 run 的隔离资源目录。
50
50
  6. 执行 Python 结构校验、Node lint、独立 Chrome 渲染、重复内容检测和像素差异计算。
51
51
  7. 聚合 Round 报告(`run_round.mjs` 收尾自动调用 `score_round.mjs`),按主失败签名与失败类出直方图。
52
- 8. `plan_next_round.mjs` 把「归因 → 聚类 → 焦点集 → 门禁判定 → 收敛信号 → lesson 草稿」一次算完,产出 `next-round-plan.json` 与 `lesson-drafts.jsonl`(`status=candidate`,人工审批后才落盘 lessons)。Agent 按聚类逐例复核 `designImage + render.png + spec.json`,把失败归入 PARSER / DSL / STYLE / DATA / INTERACTION / ASSET / EVALUATOR,并只推广通用规则。
52
+ 8. **轮后自动落盘**(`run_round.mjs` 收尾自动执行,`--no-sink` 跳过):`plan_next_round.mjs` 把「归因 → 聚类 → 焦点集 → 门禁判定 → 收敛信号 → lesson 草稿」一次算完,产出 `next-round-plan.json` 与 `lesson-drafts.jsonl`(`status=candidate`);lesson 草稿按 signature 去重后自动追加到 `training/lessons/lessons.jsonl`;最后写 `TRAINING-SUMMARY.md`(人读训练总结含门禁结果表)。Agent 按聚类逐例复核 `designImage + render.png + spec.json`,把失败归入 PARSER / DSL / STYLE / DATA / INTERACTION / ASSET / EVALUATOR,并只推广通用规则。
53
53
 
54
54
  ## 高效迭代闭环(r172 起;r263 补自动归因与增量轮补齐)
55
55
 
@@ -62,8 +62,9 @@ DSL_CARD_SLICES_DIR=<training-run>/slices
62
62
  (多案例共用一套渲染会话):门禁未达标维度(按缺口降序)、失败类归因、掩膜覆盖率、上游聚焦门、
63
63
  side-by-side、像素热区、元素偏差表、`__prov` 源码行溯源、失败类建议。先看偏差表与热区,
64
64
  再决定是否动生成器。
65
- 3. **修复**:改动 `spec_to_schema_v3.mjs` / `vision.py` / `parse_meaxure.py` 的通用几何规则
66
- (禁业务词硬编码)。
65
+ 3. **修复**:改动 v3 生成器的通用几何规则——`scripts/lib/v3_extract.mjs`(识别)/ `v3_shared.mjs`
66
+ (共享 helper)/ `v3_assemble.mjs`(组装),2026-09-21 起自 `spec_to_schema_v3.mjs`(现为 CLI 编排壳)
67
+ 拆出——以及 `vision.py` / `parse_meaxure.py`(禁业务词硬编码)。
67
68
  4. **增量验证**:`run_round.mjs --round <新轮> --focus training/runs/<上轮> --inherit training/runs/<上轮>`
68
69
  只重跑焦点集(失败例 + 低分通过例),其余案例报告与截图从上一轮继承 → 新轮仍是**完整可比轮**
69
70
  (passRate 分母正确、`round_diff` 不会把未重跑案例算成 removed)。只重跑失败例用
@@ -378,6 +379,9 @@ node skills/card-skill-trainer/scripts/run_round.mjs \
378
379
  - `--resume`:续跑既有轮目录(已有报告的案例跳过,渲染会话懒启动);进程被 kill 或渲染假死后不必整轮重来。
379
380
  - `--no-correct`:禁用布局校正器,裸测生成器真实水平(里程碑对照用)。
380
381
  - `--no-summary`:跳过收尾自动汇总(默认自动调 `score_round.mjs --quiet`,直接打出 pass/avgFid/roundGate)。
382
+ - `--concurrency N`:并行渲染会话数(默认 2,上限 4;`1` 即旧串行行为)。每个 worker 持独立
383
+ Chrome + HTTP server(随机端口 / 唯一 userDataDir,多实例安全),worker 池按案例分派;
384
+ 单案例异常隔离不变。吞吐敏感的全量轮保持默认,内存受限环境降为 1。
381
385
  - `--splits train,dev`:限定分区(迭代纪律见上文)。
382
386
  - `--only` / `--only-failed` / `--focus` 三者互斥,同时传会直接报错而不是静默取其一。
383
387
 
@@ -471,13 +475,18 @@ training/runs/<round>/
471
475
  slices/
472
476
  reports/
473
477
  <caseId>.json ← 含 gateFailures / failureSignature / failureClass / exclusionCoverage
474
- 与 layoutCorrections / correctorRejected 校正明细
478
+ 与 layoutCorrections / correctorRejected 校正明细;
479
+ telemetry 字段:timings(阶段耗时 ms)/ stageErrors(阶段化错误)/
480
+ usage(生成链路 LLM usage,预留)/ reviewTokensEst(归因证据包 token 估算)
475
481
  <caseId>.render.png ← 最终渲染截图
476
482
  <caseId>.design.png ← 设计稿参考图
477
483
  <caseId>.side.png ← diagnose_case 并排对比图(按需生成)
478
484
  summary.json ← 含 roundGate / roundGatePassed / correctorCoverage /
479
- gateFailureHistogram / failureClasses / highMaskCases
480
- summary.md ← 含晋级门禁表格、归因直方图与截图对比图廊
485
+ gateFailureHistogram / failureClasses / highMaskCases /
486
+ avgCaseMs / p50CaseMs / p95CaseMs / telemetry(速度·错误·token 汇总)
487
+ summary.md ← 含晋级门禁表格、归因直方图、「速度与消耗」段与截图对比图廊
488
+ telemetry.json ← 轮级 telemetry:墙钟 / 并发 / 阶段耗时聚合 / 渲染次数与会话重建 /
489
+ 阶段错误直方图 / LLM usage / 归因证据包 token 估算(run_round 收尾落盘)
481
490
  review-queue.jsonl
482
491
  semantic-review.jsonl
483
492
  inherited.json ← --inherit 轮:继承来源与案例清单
@@ -487,4 +496,50 @@ training/runs/<round>/
487
496
  diagnosis.json ← diagnose_case --out:批量诊断的机器可读证据包
488
497
  ```
489
498
 
499
+ ### 效率观测(telemetry,2026-09-25 起)
500
+
501
+ 训练的三类效率信号由 `run_round.mjs` 自动采集、`score_round.mjs` 聚合进 summary(口径实现:
502
+ `lib/telemetry.mjs`,直测 `tools/tests/trainer-telemetry.test.mjs`):
503
+
504
+ 1. **耗时**:每案例按阶段计时(parse / upstream / generate / autofix / slices / validate / lint /
505
+ render / reRender / pixel / score),轮级 `telemetry.json` 出阶段均值与墙钟吞吐
506
+ (`casesPerMinute`)。瓶颈定位:`render` 占比高 → 提 `--concurrency`;`reRender` 次数多 →
507
+ 校正器覆盖率高,按「校正器纪律」沉淀规则;`pixel` 占比高 → 检查掩膜数量与图像尺寸。
508
+ 2. **错误**:阶段化登记(`stageErrors`,validate/lint/render/case 分开计次),轮级直方图区分
509
+ 「工具链坏了」与「生成质量差」——前者修环境,后者走归因闭环。
510
+ 3. **token**:v3 生成器是纯规则引擎(0 LLM 调用),`usage` 字段为生成链路 LLM 化后预留(自动
511
+ 累计 `meta.usage`);训练闭环中真实发生的消耗是**失败案例的 Agent 在环多模态归因**——
512
+ `reviewTokensEst` 按「双图面积/750 + spec 文本/4 chars」估算归因证据包 input token(预算
513
+ 参考,不进门禁)。降 token 的正道 = 提高还原度减少失败案例,而不是压缩证据包。
514
+
515
+ > 口径只作效率观测,不参与 PASS_GATE / ROUND_GATE 任何判定;旧轮次报告缺字段时对应聚合自动跳过。
516
+
490
517
  `summary.md` 与 `next-round-plan.json` 是需要周期性查看的两份产物;如果所有门禁通过,可以完全不看。
518
+
519
+ ---
520
+ ### Input component was invisible to text-component fidelity(r038n 训练回灌)
521
+ > lesson: `LESSON-20260826-043` status: `promoted`
522
+
523
+ Input 是可编辑文本承载组件,组件相似度应计入 text 与 controls 两个维度;不能只把 className=Text 当作文本组件,否则会反向奖励 display-text 而惩罚正确 Input 语义。
524
+
525
+
526
+ ---
527
+ ### ListLayout template reuse was scored as missing components(r039n 训练回灌)
528
+ > lesson: `LESSON-20260826-046` status: `promoted`
529
+
530
+ 组件评分必须按 ListLayout 数据项数展开模板内 Text/Image/Input/Button 计数;模板复用是正确实现,不能因 schema 只保存一份模板而判为组件丢失。
531
+
532
+
533
+ ---
534
+ ### card focus needs visual, annotation and agent semantic cross(r039n 训练回灌)
535
+ > lesson: `LESSON-20260826-047` status: `promoted`
536
+
537
+ 卡片候选由截图视觉、标注几何和 Agent 语义三方交叉验证;完整业务容器才可接受,对话气泡、单按钮、弹窗、toast 和底部抽屉必须拒绝或跳过。规则只使用通用结构信号,禁止业务词硬编码。
538
+
539
+
540
+ ---
541
+ ### pixel-diff-over-8%(seq4 训练回灌)
542
+ > lesson: `seq4-l06` status: `promoted`
543
+
544
+ 像素掩膜门必须环境感知:探针实测「回退栈宽/标注宽」fontMaskWiden > 1.06 即该字体在本机渲染环境不可得(PingFang SC 在 Windows 同样回退合成粗体/换族),家族名豁免会让最大像素热带(文本字形带 16-36%/带)漏掩,把像素门顶在 8% 门外并误逼生成器改字重/字号扭曲 spec;探针缺失回退家族名判据。掩膜只进像素 ratio 通道(pixelOnlyRects),structure/color 剖面仍只用硬排除掩膜,否则大掩膜会把 structure 打穿(实测 87→67)。
545
+
@@ -24,8 +24,9 @@ import path from 'node:path'
24
24
  import { pathToFileURL } from 'node:url'
25
25
  import { PNG } from 'pngjs'
26
26
  import {
27
- attributeFailure, evaluatePassGate, exclusionCoverage, failureSignature, MASK_REVIEW_THRESHOLD,
27
+ attributeFailure, evaluatePassGate, exclusionCoverage, failureSignature,
28
28
  } from './lib/scoring.mjs'
29
+ import { pixelBands, pairDeviations, deriveHints } from './lib/diagnosis.mjs'
29
30
 
30
31
  const repoRoot = path.resolve(import.meta.dirname, '../../..')
31
32
 
@@ -118,72 +119,8 @@ function buildSideImage(designPng, renderPng, sidePath) {
118
119
  return sidePath
119
120
  }
120
121
 
121
- function pixelBands(designPng, renderPng, exclusions) {
122
- const w = Math.min(designPng.width, renderPng.width)
123
- const h = Math.min(designPng.height, renderPng.height)
124
- const mask = new Uint8Array(w * h)
125
- for (const rc of exclusions || []) {
126
- const x0 = Math.floor((rc.x ?? 0) * w), y0 = Math.floor((rc.y ?? 0) * h)
127
- const x1 = Math.min(w, Math.ceil(((rc.x ?? 0) + (rc.width ?? 0)) * w))
128
- const y1 = Math.min(h, Math.ceil(((rc.y ?? 0) + (rc.height ?? 0)) * h))
129
- for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) mask[y * w + x] = 1
130
- }
131
- const BANDS = 20
132
- const bandDiff = new Array(BANDS).fill(0)
133
- const bandTotal = new Array(BANDS).fill(0)
134
- let totalDiff = 0
135
- let totalCount = 0
136
- for (let y = 0; y < h; y++) {
137
- const band = Math.min(BANDS - 1, Math.floor((y / h) * BANDS))
138
- for (let x = 0; x < w; x++) {
139
- if (mask[y * w + x]) continue
140
- const i = (y * designPng.width + x) * 4
141
- const j = (y * renderPng.width + x) * 4
142
- const diff = Math.abs(designPng.data[i] - renderPng.data[j]) > 15
143
- || Math.abs(designPng.data[i + 1] - renderPng.data[j + 1]) > 15
144
- || Math.abs(designPng.data[i + 2] - renderPng.data[j + 2]) > 15
145
- if (diff) { bandDiff[band]++; totalDiff++ }
146
- bandTotal[band]++; totalCount++
147
- }
148
- }
149
- const bandPct = bandDiff.map((c, i) => (bandTotal[i] ? c / bandTotal[i] : 0))
150
- const hotBands = bandPct.map((pct, i) => ({ band: i, y: Math.round((i / BANDS) * h), pct }))
151
- .filter((b) => b.pct >= 0.05).sort((a, b) => b.pct - a.pct)
152
- return { hotBands, diffRatio: totalCount ? totalDiff / totalCount : null }
153
- }
154
-
155
- function pairDeviations(specTexts, renderTexts) {
156
- const usedSpec = new Set()
157
- const deviations = []
158
- for (const rt of renderTexts) {
159
- const key = norm(rt.text)
160
- if (!key) continue
161
- let best = null
162
- let bestScore = 0
163
- for (let i = 0; i < specTexts.length; i++) {
164
- if (usedSpec.has(i)) continue
165
- const st = specTexts[i]
166
- let score = 0
167
- if (st.text === key) score = 1
168
- else if (st.text.includes(key) || key.includes(st.text)) score = 0.9
169
- if (score > bestScore) { bestScore = score; best = { ...st, idx: i } }
170
- }
171
- if (!best || bestScore < 0.9) continue
172
- usedSpec.add(best.idx)
173
- deviations.push({
174
- text: key.slice(0, 18),
175
- dy: +(rt.y - best.y).toFixed(1),
176
- dx: +(rt.x - best.x).toFixed(1),
177
- dw: +((rt.w || 0) - best.w).toFixed(1),
178
- dh: +((rt.h || 0) - best.h).toFixed(1),
179
- render: { x: rt.x, y: rt.y, w: rt.w, h: rt.h },
180
- spec: { x: best.x, y: best.y, w: best.w, h: best.h, fs: best.fs, lineHeight: best.lineHeight },
181
- cid: rt.cid,
182
- })
183
- }
184
- deviations.sort((a, b) => (Math.abs(b.dy) + Math.abs(b.dx)) - (Math.abs(a.dy) + Math.abs(a.dx)))
185
- return deviations
186
- }
122
+ // pixelBands / pairDeviations / deriveHints 已迁至 lib/diagnosis.mjs(run_round 与
123
+ // diagnose_case 共用单一来源,避免两处漂移);本脚本仅按需引入。
187
124
 
188
125
  async function main() {
189
126
  const results = []
@@ -302,23 +239,18 @@ async function main() {
302
239
  provHits = [{ error: String(e.message || e) }]
303
240
  }
304
241
 
305
- // —— 5. 失败类建议 ——
242
+ // —— 5. 失败类建议(与 run_round 在环归因共用 deriveHints,单一口径)——
306
243
  const px = report.pixel || {}
307
244
  const fid = report.fidelity || {}
308
- const hints = []
309
- if (attribution) hints.push(`归因 ${attribution.failureClass}:${attribution.reason}`)
310
- if (px.bestAlignDy && Math.abs(px.bestAlignDy) >= 4) hints.push(`全局纵向偏移 dy=${px.bestAlignDy}:查行高/行距累积(Input 高度、行距标注优先、分割线渲染盒高)`)
311
- if (bands.hotBands.length && bands.hotBands[0].pct >= 0.2) hints.push(`单带高热 (${(bands.hotBands[0].pct * 100).toFixed(0)}%@y≈${bands.hotBands[0].y}):查该区域语义块(换行折点/缺组件/结构错配)`)
312
- if ((fid.components ?? 1) < 0.9) hints.push(`组件计数缺失 comp=${num(fid.components, 2)}:查语义消费(媒体未渲染/文本被吞/重复消费)`)
313
- if ((report.duplication?.overall ?? 1) < 0.95) {
314
- hints.push(`重影:查同源按钮/文本双路径渲染(${(report.duplication?.offenders || []).map((o) => `「${o.text}」x${o.rendered}/${o.annotated}`).join(' ')})`)
315
- }
316
- if ((coverage.total ?? 0) >= MASK_REVIEW_THRESHOLD) {
317
- hints.push(`掩膜覆盖 ${(coverage.total * 100).toFixed(1)}% ≥ ${(MASK_REVIEW_THRESHOLD * 100).toFixed(0)}%:得分不代表整卡,必须看图复核`)
318
- }
319
- const bigDy = deviations.filter((d) => Math.abs(d.dy) >= 3)
320
- if (bigDy.length >= 3) hints.push(`${bigDy.length} 个文本 |dy|≥3:纵向节奏问题(行距/padding 体系),非单点`)
321
- if (!hints.length) hints.push('像素差异弥散且量级低:多为 1-2px 抗锯齿/字体度量差异,接近噪声底')
245
+ const hints = deriveHints({
246
+ attribution: attribution ? { failureClass: attribution.failureClass, reason: attribution.reason } : null,
247
+ pixel: px,
248
+ fidelity: fid,
249
+ duplication: report.duplication || null,
250
+ coverage,
251
+ hotBands: bands.hotBands,
252
+ deviations,
253
+ })
322
254
 
323
255
  // —— 6. 输出 ——
324
256
  console.log('═══════════════════════════════════════════════════════════')
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * diagnosis.mjs — 失败案例结构化证据包的纯函数核心(2026-09-25)。
4
+ *
5
+ * 单一来源:run_round.mjs(在环归因,零额外渲染复用已有数据)与 diagnose_case.mjs
6
+ * (按需深度诊断,含 provenance 溯源)共用本模块,避免两处漂移。
7
+ *
8
+ * 导出:
9
+ * - decodeBufferToPng(buf) Buffer → PNG.sync 对象(失败返回 null)
10
+ * - pixelBands(design, render, exclusions) 逐带像素差异率 + 热区带
11
+ * - pairDeviations(specTexts, renderTexts) 标注几何 vs 渲染盒偏差表(dy/dx)
12
+ * - deriveHints(ctx) 失败类建议(与评分口径同源)
13
+ * - buildInlineDiagnosis(report, ctx) run_round 在环归因用的轻量结构化包
14
+ *
15
+ * 设计约束:本模块只做纯计算,不触碰 Chrome / 文件系统 / 任何门禁口径。
16
+ * 图像解码只在 buildInlineDiagnosis 内按需发生,且仅针对「确实失败且像素可比对」的案例。
17
+ */
18
+ import { PNG } from 'pngjs'
19
+ import { MASK_REVIEW_THRESHOLD, exclusionCoverage } from './scoring.mjs'
20
+
21
+ const norm = (value) => String(value || '').replace(/\s+/g, '')
22
+
23
+ export function decodeBufferToPng(buf) {
24
+ try { return buf ? PNG.sync.read(buf) : null } catch { return null }
25
+ }
26
+
27
+ export function pixelBands(designPng, renderPng, exclusions) {
28
+ const w = Math.min(designPng.width, renderPng.width)
29
+ const h = Math.min(designPng.height, renderPng.height)
30
+ const mask = new Uint8Array(w * h)
31
+ for (const rc of exclusions || []) {
32
+ const x0 = Math.floor((rc.x ?? 0) * w), y0 = Math.floor((rc.y ?? 0) * h)
33
+ const x1 = Math.min(w, Math.ceil(((rc.x ?? 0) + (rc.width ?? 0)) * w))
34
+ const y1 = Math.min(h, Math.ceil(((rc.y ?? 0) + (rc.height ?? 0)) * h))
35
+ for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) mask[y * w + x] = 1
36
+ }
37
+ const BANDS = 20
38
+ const bandDiff = new Array(BANDS).fill(0)
39
+ const bandTotal = new Array(BANDS).fill(0)
40
+ let totalDiff = 0
41
+ let totalCount = 0
42
+ for (let y = 0; y < h; y++) {
43
+ const band = Math.min(BANDS - 1, Math.floor((y / h) * BANDS))
44
+ for (let x = 0; x < w; x++) {
45
+ if (mask[y * w + x]) continue
46
+ const i = (y * designPng.width + x) * 4
47
+ const j = (y * renderPng.width + x) * 4
48
+ const diff = Math.abs(designPng.data[i] - renderPng.data[j]) > 15
49
+ || Math.abs(designPng.data[i + 1] - renderPng.data[j + 1]) > 15
50
+ || Math.abs(designPng.data[i + 2] - renderPng.data[j + 2]) > 15
51
+ if (diff) { bandDiff[band]++; totalDiff++ }
52
+ bandTotal[band]++; totalCount++
53
+ }
54
+ }
55
+ const bandPct = bandDiff.map((c, i) => (bandTotal[i] ? c / bandTotal[i] : 0))
56
+ const hotBands = bandPct.map((pct, i) => ({ band: i, y: Math.round((i / BANDS) * h), pct }))
57
+ .filter((b) => b.pct >= 0.05).sort((a, b) => b.pct - a.pct)
58
+ return { hotBands, diffRatio: totalCount ? totalDiff / totalCount : null }
59
+ }
60
+
61
+ export function pairDeviations(specTexts, renderTexts) {
62
+ const usedSpec = new Set()
63
+ const deviations = []
64
+ for (const rt of renderTexts) {
65
+ const key = norm(rt.text)
66
+ if (!key) continue
67
+ let best = null
68
+ let bestScore = 0
69
+ let bestDist = Infinity
70
+ for (let i = 0; i < specTexts.length; i++) {
71
+ if (usedSpec.has(i)) continue
72
+ const st = specTexts[i]
73
+ let score = 0
74
+ if (st.text === key) score = 1
75
+ else if (st.text.includes(key) || key.includes(st.text)) score = 0.9
76
+ // Geometric tiebreaker: when multiple specs have same similarity (e.g. identical texts
77
+ // like two "转账计划"), prefer the nearest by center distance — crossed matching produces
78
+ // false huge dy/dx that misleads the diagnosis.
79
+ const dist = Math.hypot(((st.x||0)+(st.w||0)/2)-((rt.x||0)+(rt.w||0)/2), ((st.y||0)+(st.h||0)/2)-((rt.y||0)+(rt.h||0)/2))
80
+ if (score > bestScore + 1e-9 || (score >= bestScore - 1e-9 && dist < bestDist)) {
81
+ bestScore = score; best = { ...st, idx: i }; bestDist = dist
82
+ }
83
+ }
84
+ if (!best || bestScore < 0.9) continue
85
+ usedSpec.add(best.idx)
86
+ deviations.push({
87
+ text: key.slice(0, 18),
88
+ dy: +(rt.y - best.y).toFixed(1),
89
+ dx: +(rt.x - best.x).toFixed(1),
90
+ dw: +((rt.w || 0) - best.w).toFixed(1),
91
+ dh: +((rt.h || 0) - best.h).toFixed(1),
92
+ render: { x: rt.x, y: rt.y, w: rt.w, h: rt.h },
93
+ spec: { x: best.x, y: best.y, w: best.w, h: best.h, fs: best.fs, lineHeight: best.lineHeight },
94
+ cid: rt.cid,
95
+ })
96
+ }
97
+ deviations.sort((a, b) => (Math.abs(b.dy) + Math.abs(b.dx)) - (Math.abs(a.dy) + Math.abs(a.dx)))
98
+ return deviations
99
+ }
100
+
101
+ /**
102
+ * 失败类建议:与 lib/scoring.mjs 的归因/门禁口径同源,run_round 与 diagnose_case 共用。
103
+ * ctx: { attribution:{failureClass,reason}, pixel, fidelity, duplication, coverage, hotBands, deviations }
104
+ */
105
+ export function deriveHints(ctx) {
106
+ const {
107
+ attribution = null, pixel = {}, fidelity = {}, duplication = null,
108
+ coverage = {}, hotBands = [], deviations = [],
109
+ } = ctx || {}
110
+ const num = (v, d = 4) => (typeof v === 'number' && Number.isFinite(v)) ? v.toFixed(d) : 'N/A'
111
+ const hints = []
112
+ if (attribution) hints.push(`归因 ${attribution.failureClass}:${attribution.reason}`)
113
+ if (pixel.bestAlignDy && Math.abs(pixel.bestAlignDy) >= 4) hints.push(`全局纵向偏移 dy=${pixel.bestAlignDy}:查行高/行距累积(Input 高度、行距标注优先、分割线渲染盒高)`)
114
+ if (hotBands.length && hotBands[0].pct >= 0.2) hints.push(`单带高热 (${(hotBands[0].pct * 100).toFixed(0)}%@y≈${hotBands[0].y}):查该区域语义块(换行折点/缺组件/结构错配)`)
115
+ if ((fidelity.components ?? 1) < 0.9) hints.push(`组件计数缺失 comp=${num(fidelity.components, 2)}:查语义消费(媒体未渲染/文本被吞/重复消费)`)
116
+ if ((duplication?.overall ?? 1) < 0.95) {
117
+ hints.push(`重影:查同源按钮/文本双路径渲染(${(duplication?.offenders || []).map((o) => `「${o.text}」x${o.rendered}/${o.annotated}`).join(' ')})`)
118
+ }
119
+ const covTotal = coverage?.total ?? 0
120
+ if (covTotal >= MASK_REVIEW_THRESHOLD) {
121
+ hints.push(`掩膜覆盖 ${(covTotal * 100).toFixed(1)}% ≥ ${(MASK_REVIEW_THRESHOLD * 100).toFixed(0)}%:得分不代表整卡,必须看图复核`)
122
+ }
123
+ const bigDy = deviations.filter((d) => Math.abs(d.dy) >= 3)
124
+ if (bigDy.length >= 3) hints.push(`${bigDy.length} 个文本 |dy|≥3:纵向节奏问题(行距/padding 体系),非单点`)
125
+ if (!hints.length) hints.push('像素差异弥散且量级低:多为 1-2px 抗锯齿/字体度量差异,接近噪声底')
126
+ return hints
127
+ }
128
+
129
+ function annDefectCount(upstream) {
130
+ if (!upstream) return 0
131
+ const ad = upstream.annotationDefects
132
+ if (Array.isArray(ad)) return ad.length
133
+ return typeof ad === 'number' ? ad : 0
134
+ }
135
+
136
+ /**
137
+ * run_round 在环归因用的轻量结构化包:复用 processCase 已算出的 report / 设计稿 buffer /
138
+ * 标注文本 / 渲染盒,零额外 Chrome 渲染。provenance(cid→生成器源码行)因需 SCHEMA_PROV
139
+ * 重生成,不在本包内——按需由 diagnose_case.mjs 深度补全。
140
+ *
141
+ * ctx: { designBuf, renderBuf, specTexts, renderedTexts }
142
+ * specTexts: run_round 过滤后的标注文本数组(含 rawText/x/y/w/h/fs)
143
+ * renderedTexts: 渲染盒数组(含 text/x/y/w/h/cid,已过滤 Text/Input/Button)
144
+ */
145
+ export function buildInlineDiagnosis(report, ctx) {
146
+ const { designBuf = null, renderBuf = null, specTexts = [], renderedTexts = [] } = ctx || {}
147
+ const designPng = designBuf ? decodeBufferToPng(designBuf) : null
148
+ const renderPng = renderBuf ? decodeBufferToPng(renderBuf) : null
149
+ const exclusions = report?.pixelExclusions || []
150
+ const bands = (designPng && renderPng)
151
+ ? pixelBands(designPng, renderPng, exclusions)
152
+ : { hotBands: [], diffRatio: null }
153
+
154
+ const specForDev = specTexts.map((el) => ({
155
+ text: norm(el.rawText ?? el.text ?? ''),
156
+ x: el.x ?? 0, y: el.y ?? 0, w: el.w ?? 0, h: el.h ?? 0,
157
+ fs: el.fs ?? el.textStyle?.fontSize ?? 14, lineHeight: el.lineHeight ?? null,
158
+ }))
159
+ const renderForDev = renderedTexts.map((el) => ({
160
+ text: el.text ?? '', x: el.x ?? 0, y: el.y ?? 0, w: el.w ?? 0, h: el.h ?? 0, cid: el.cid,
161
+ }))
162
+ const deviations = pairDeviations(specForDev, renderForDev)
163
+
164
+ const coverage = report?.exclusionCoverage || exclusionCoverage(exclusions)
165
+ const attribution = report?.failureClass
166
+ ? { failureClass: report.failureClass, reason: report.failureReason || '' }
167
+ : null
168
+ const hints = deriveHints({
169
+ attribution,
170
+ pixel: report?.pixel || {},
171
+ fidelity: report?.fidelity || {},
172
+ duplication: report?.duplication || null,
173
+ coverage,
174
+ hotBands: bands.hotBands,
175
+ deviations,
176
+ })
177
+
178
+ return {
179
+ gateFailures: report?.gateFailures || [],
180
+ failureClass: attribution?.failureClass || null,
181
+ failureReason: attribution?.reason || null,
182
+ metrics: {
183
+ fidelity: report?.fidelity?.overall ?? null,
184
+ pixel: report?.pixel?.ratio ?? null,
185
+ structure: report?.pixel?.structure ?? null,
186
+ dimension: report?.pixel?.dimension ?? null,
187
+ color: report?.pixel?.color ?? null,
188
+ position: report?.position?.overall ?? null,
189
+ components: report?.fidelity?.components ?? null,
190
+ duplication: report?.duplication?.overall ?? null,
191
+ bestAlignDy: report?.pixel?.bestAlignDy ?? null,
192
+ },
193
+ exclusionCoverage: coverage,
194
+ upstream: report?.upstream ? {
195
+ passed: report.upstream.passed,
196
+ annotationCompleteness: report.upstream.annotationCompleteness,
197
+ textRetention: report.upstream.textRetention,
198
+ structuralFocus: report.upstream.structuralFocus,
199
+ annotationDefects: annDefectCount(report.upstream),
200
+ } : null,
201
+ hotBands: bands.hotBands.slice(0, 8),
202
+ deviations: deviations.slice(0, 10),
203
+ layoutCorrections: report?.layoutCorrections || [],
204
+ correctorRejected: report?.correctorRejected || null,
205
+ hints,
206
+ }
207
+ }
@@ -0,0 +1,62 @@
1
+ // Layout-correction rigid-body simulation for the trainer's reject fast-path.
2
+ //
3
+ // When correctCardLayout only adjusts paddingTop (Y axis) inside a vertical Column,
4
+ // the correction is an exact rigid body shift: shifting child#i by Δ also shifts every
5
+ // later sibling by the cumulative Δ. The same logic extends to paddingLeft (X axis):
6
+ // a child's paddingLeft change shifts that child's whole subtree horizontally, but —
7
+ // unlike Y — does NOT cascade to later siblings in a vertical flow (each child keeps
8
+ // its own independent horizontal padding). So X deltas are per-child and non-cumulative,
9
+ // while Y deltas accumulate across siblings.
10
+ //
11
+ // Keeping this here (instead of inline in run_round.mjs) makes the admit/translate rules
12
+ // unit-testable without spinning up Chrome. Zero gate behavior changes: the simulator
13
+ // only decides whether a real re-render can be skipped; the final keep/revert decision
14
+ // always goes through the real position + pixel guard.
15
+
16
+ /**
17
+ * Whether the rigid-body reject fast-path may replace a real re-render.
18
+ * @param {Array} adjustments entries pushed by correctCardLayout (axis 'y'|'x', rigidSafe)
19
+ * @param {Map} cidToChild cid -> child index (built inside correctCardLayout)
20
+ * @param {Array} [children] card column direct children (to exclude AbsoluteLayout)
21
+ */
22
+ export function canSimulateReject(adjustments, cidToChild, children) {
23
+ if (!adjustments?.length || !cidToChild) return false
24
+ if (!adjustments.every((a) => a.rigidSafe !== false && (a.axis === 'y' || a.axis === 'x'))) return false
25
+ // AbsoluteLayout children break the simple rigid model (their own left/positioning
26
+ // context means paddingLeft/paddingTop do not translate the subtree predictably).
27
+ if (children && !children.every((c) => c.name !== 'AbsoluteLayout')) return false
28
+ return true
29
+ }
30
+
31
+ /**
32
+ * Apply the rigid-body deltas to rendered boxes, returning shifted copies.
33
+ * Boxes whose cid is not mapped to a child are returned unchanged.
34
+ * @returns {{ shifted: Array, appliedAxes: Set<string> }}
35
+ */
36
+ export function simulateLayoutCorrection(boxes, adjustments, cidToChild) {
37
+ const deltasY = new Map()
38
+ const deltasX = new Map()
39
+ const appliedAxes = new Set()
40
+ for (const a of adjustments || []) {
41
+ if (a.axis === 'y') {
42
+ deltasY.set(a.childIndex, (deltasY.get(a.childIndex) || 0) + (a.to - a.from))
43
+ appliedAxes.add('y')
44
+ } else if (a.axis === 'x') {
45
+ // X is per-child, no cross-sibling cascade.
46
+ deltasX.set(a.childIndex, (deltasX.get(a.childIndex) || 0) + (a.to - a.from))
47
+ appliedAxes.add('x')
48
+ }
49
+ }
50
+ const shiftForY = (idx) => {
51
+ let acc = 0
52
+ for (const [i, d] of deltasY) if (i <= idx) acc += d
53
+ return acc
54
+ }
55
+ const shiftForX = (idx) => deltasX.get(idx) || 0
56
+ const shifted = (boxes || []).map((b) => {
57
+ const idx = cidToChild.get(b.cid)
58
+ if (idx == null) return b
59
+ return { ...b, y: b.y + shiftForY(idx), x: b.x + shiftForX(idx) }
60
+ })
61
+ return { shifted, appliedAxes }
62
+ }
@@ -0,0 +1,76 @@
1
+ // paradigm-router.mjs — 把高频失败签名映射到已沉淀的生成器通用范式引用。
2
+ //
3
+ // 用途:plan_next_round 的 `same-signature-3-rounds` 信号原本只说「脚本补丁无效,
4
+ // 需升级 Skill 级范式」——笼统、Agent 仍需从零救火。本模块把维度级签名(不认业务词)
5
+ // 路由到已落盘的 recipes 章节 / v3 链路铁律 / 生成器识别 intent,让 Agent 直接套用范式,
6
+ // 把迭代预算从「每失败例救火」转向「每签名沉淀一条通用规则」(对应 trainer 效率维度三)。
7
+ //
8
+ // 失败签名 key 形如 `${failureClass}/${signature}`:
9
+ // failureClass ∈ {PARSER, DSL, STYLE, DATA, ASSET, EVALUATOR}
10
+ // signature 是门维度(structure/position/overall/color/pixelDiff/components/...)
11
+ // 纯维度信号粗分模式,契合 skill 纯净铁律(禁硬编码业务实体名称)。
12
+
13
+ export const PARADIGMS = {
14
+ // STYLE + 结构/位置/整体维度低 = 整块漏建(图表/雷达/分区)
15
+ 'chart-radar-block': {
16
+ label: '图表/雷达/分区整块漏建',
17
+ recipeSections: [
18
+ 'recipes.md §1 Canvas 画布标题头(onMeasured 自绘)',
19
+ 'recipes.md §10 Canvas 甜甜圈/环形比例图(donut chart)',
20
+ 'v3_assemble.mjs 散点带拓扑(中心媒体 + 环绕胶囊簇)',
21
+ ],
22
+ generatorIntents: [
23
+ 'extract_handcrafted.py canvas_chart(Canvas+onMeasured 识别)',
24
+ 'resolve_card_template.mjs 「雷达图 仪表盘」模板',
25
+ 'v3_shared.mjs R62 图表绘制层序铁律(坐标/参考系/数据图元分层)',
26
+ ],
27
+ action: '套用图表通用范式(Canvas 自绘 + onMeasured),禁止用 Text/Image 堆砌或整块漏建;治本在 IR 路径强制把图表几何信号路由到 canvas_chart 识别',
28
+ },
29
+ // STYLE + 像素/色维度低 = 底色整层错涂
30
+ 'bg-fill': {
31
+ label: '底色/背景整层错涂',
32
+ recipeSections: ['spec_audit.mjs 底色采样(角部 + gutter 众数聚类,禁用 center 采样)'],
33
+ generatorIntents: ['spec_audit fill 角部 + gutter 众数聚类'],
34
+ action: '底色取角部/边缘众数,禁止照抄 center 点(雷达/图表中心色常被误当卡底色);治本在 spec_audit 采样逻辑',
35
+ },
36
+ // DSL + 组件计数缺口 = 媒体/列表漏建
37
+ 'list-or-chart-missing': {
38
+ label: '组件/图表计数缺口(媒体或列表漏建)',
39
+ recipeSections: ['ir-format.md §2f 列表数组化', 'recipes.md §10 图表范式'],
40
+ generatorIntents: ['bindings.mjs collectArrayConsumption', 'canvas_chart intent'],
41
+ action: '先查 canvas_chart 识别与雷达/环图数据绑定(radarData/donutData),再查列表数组化;禁止静态展开或整块漏建',
42
+ },
43
+ // 兜底:仍要求沉淀通用规则,但无既有范式可直引
44
+ generic: {
45
+ label: '通用门维度失败',
46
+ recipeSections: [],
47
+ generatorIntents: [],
48
+ action: '升级 Skill 级范式:沉淀一条通用规则而非逐例救火',
49
+ },
50
+ }
51
+
52
+ /** 基于 failureClass + 门维度签名粗分范式模式(不依赖业务词)。 */
53
+ export function classifyParadigm(signatureKey) {
54
+ const [failureClass = '', signature = ''] = String(signatureKey || '').split('/')
55
+ const sig = signature.toLowerCase()
56
+ if (failureClass === 'STYLE' && /^(structure|position|overall)$/.test(sig)) return 'chart-radar-block'
57
+ if (failureClass === 'STYLE' && /^(pixeldiff|color)$/.test(sig)) return 'bg-fill'
58
+ if (failureClass === 'DSL' && sig === 'components') return 'list-or-chart-missing'
59
+ return 'generic'
60
+ }
61
+
62
+ /** 取范式引用详情,未知 key 回退 generic。 */
63
+ export function paradigmRefs(key) {
64
+ return PARADIGMS[key] || PARADIGMS.generic
65
+ }
66
+
67
+ /** 给 same-signature-3-rounds 信号生成增强 detail(含范式模式 + 引用 + 行动项)。 */
68
+ export function enrichSameSignatureDetail(item) {
69
+ const patternKey = classifyParadigm(item.signature)
70
+ const refs = paradigmRefs(patternKey)
71
+ const base = `签名 ${item.signature} 在 ${item.occurrences} 轮重复出现(${item.rounds.join(' → ')}):脚本补丁无效,需升级 Skill 级范式。`
72
+ const parts = [base, `模式=${refs.label}。${refs.action}。`]
73
+ if (refs.recipeSections.length) parts.push(`参照:${refs.recipeSections.join(';')}。`)
74
+ if (refs.generatorIntents.length) parts.push(`生成器侧已沉淀:${refs.generatorIntents.join(';')}。`)
75
+ return { pattern: patternKey, detail: parts.join('') }
76
+ }
@@ -0,0 +1,31 @@
1
+ // 像素评分 worker 运行器(P0-2):在独立线程内执行 imageFidelity,避免阻塞主事件循环。
2
+ // 纯 JS + pngjs,无 DOM 依赖,可直接在 worker_threads 内 import。
3
+ // 计算异常就地捕获并回传 error,由主侧 scorePixel 触发同步回退,保证训练轮不崩。
4
+ import { parentPort } from 'node:worker_threads'
5
+ import { imageFidelity } from './scoring.mjs'
6
+
7
+ parentPort.on('message', ({ id, payload }) => {
8
+ try {
9
+ // Buffer 经 worker_threads 结构化克隆后可能退化为 Uint8Array,PNG.sync.read 仍需真 Buffer;
10
+ // Buffer.from 对已是 Buffer 的入参仅复制、对 Uint8Array 则还原,两路都安全。
11
+ const designBuffer = Buffer.isBuffer(payload.designBuffer)
12
+ ? payload.designBuffer
13
+ : Buffer.from(payload.designBuffer)
14
+ const renderBuffer = Buffer.isBuffer(payload.renderBuffer)
15
+ ? payload.renderBuffer
16
+ : Buffer.from(payload.renderBuffer)
17
+ const result = imageFidelity(
18
+ designBuffer,
19
+ renderBuffer,
20
+ payload.exclusionRects,
21
+ payload.pixelOnlyRects,
22
+ payload.opts,
23
+ )
24
+ parentPort.postMessage({ id, result })
25
+ } catch (error) {
26
+ parentPort.postMessage({
27
+ id,
28
+ error: { message: String(error?.message || error), stack: String(error?.stack || '') },
29
+ })
30
+ }
31
+ })