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.
- package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
- package/dist/preview-editor/assets/{index-CIIfMlwI.js → index-Bro07DTa.js} +91 -88
- package/dist/preview-editor/index.html +2 -2
- package/dist/vendored/card-editor/runtime/adapter.js +2 -2
- package/dist/vendored/mcp/client.mjs +1 -1
- package/dist/vendored/server/core/ai-chat.js +251 -80
- package/dist/vendored/server/core/cardValidate.js +16 -5
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +61 -6
- package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
- package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
- package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +93 -27
- package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
- package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +169 -44
- package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +87 -3
- package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +35 -6
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +28 -27
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +30 -8
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +3 -2
- package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +5 -2
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +2 -3
- package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +1 -4
- package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
- package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +88 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +143 -42
- package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
- package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +99 -53
- package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +5 -2
- package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +388 -29
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +325 -41
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +112 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +0 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +119 -78
- package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +48 -50
- package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +5 -10
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +122 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +3 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
- package/dist/vendored/skills/shared/exit-codes.md +79 -0
- package/dist/vendored/skills/shared/spec-contract.md +118 -0
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +26 -9
- package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +3 -3
- package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +0 -8
- package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
- package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +91 -47
- package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +0 -5
- package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +75 -15
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +40 -34
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-DQn0zQBP.css +0 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
- 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 解释器解析顺序:`
|
|
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
|
-
/**
|
|
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
|
-
|
|
38
|
-
|
|
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 或设置
|
|
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
|
|
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. **修复**:改动
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
+
})
|