cardbuddy 0.15.0 → 0.17.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/init.js +0 -2
- package/dist/preview-editor/assets/index-CXDf5tVg.css +1 -0
- package/dist/preview-editor/assets/{index-CIIfMlwI.js → index-DLQkCPLT.js} +113 -86
- 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/browser.js +17 -0
- package/dist/vendored/server/core/cardFiles.js +49 -1
- package/dist/vendored/server/core/cardImage.js +103 -2
- package/dist/vendored/server/core/cardValidate.js +16 -5
- package/dist/vendored/server/core/screenshot.js +94 -0
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +68 -13
- 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 +5 -2
- package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +286 -92
- 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/component_rules.md +14 -0
- 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 +217 -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/previewer_contract.md +21 -0
- 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/scaffold.mjs +48 -1
- 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 +366 -45
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +169 -12
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +17 -9
- 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 +11 -2
- package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
- package/dist/vendored/skills/finmall-card-sync/scripts/api_batch_sync.mjs +46 -2
- package/dist/vendored/skills/mbank-design-gen/SKILL.md +76 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-apps.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-arrow-right-blue.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-check.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-settings.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-sound.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-voice-switch.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/progress-thumb.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/tag-bg.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/vs-badge.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/ref/ref-chat-options.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/ref/ref-home-scene.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/ref/ref-product-detail.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/ref/ref-rate-table.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/robot.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/references/component-library.md +127 -0
- package/dist/vendored/skills/mbank-design-gen/references/design-spec.md +261 -0
- package/dist/vendored/skills/mbank-design-gen/templates/mockup-template.html +138 -0
- package/dist/vendored/skills/mbank-design-gen/tools/screenshot.mjs +34 -0
- package/dist/vendored/skills/shared/exit-codes.md +79 -0
- package/dist/vendored/skills/shared/spec-contract.md +127 -0
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +27 -10
- 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 +121 -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 +98 -16
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +55 -37
- package/dist/vendored/templates/helloCard/schema.json +62 -1
- package/dist/vendored/tools/finmall-env.mjs +1 -18
- 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
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* 跨平台 Python 探测:DSL_CARD_PYTHON > WORKBUDDY_PYTHON > DSL_PYTHON > PYTHON > python3/python/py。
|
|
5
|
+
* 语义与 `skills/dsl-card-generator/scripts/lib/python.mjs`(权威实现)对齐;trainer 需要能单独
|
|
6
|
+
* 分发,故不 import 而是照搬。`py` 必须在内:Windows 上 Python 从 Store 安装时往往只有它。
|
|
7
|
+
*/
|
|
4
8
|
function findPython() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
for (const key of ['DSL_CARD_PYTHON', 'WORKBUDDY_PYTHON', 'DSL_PYTHON', 'PYTHON']) {
|
|
10
|
+
if (process.env[key]) return process.env[key];
|
|
11
|
+
}
|
|
12
|
+
for (const bin of ['python3', 'python', 'py']) {
|
|
8
13
|
try {
|
|
9
14
|
if (spawnSync(bin, ['--version'], { stdio: 'ignore' }).status === 0) return bin;
|
|
10
15
|
} catch {}
|
|
@@ -15,6 +20,7 @@ import fs from 'node:fs'
|
|
|
15
20
|
import path from 'node:path'
|
|
16
21
|
import { pathToFileURL } from 'node:url'
|
|
17
22
|
import { PNG } from 'pngjs'
|
|
23
|
+
import { sliceNameMatches } from '../../dsl-card-generator/scripts/lib/slice_dedupe.mjs'
|
|
18
24
|
|
|
19
25
|
const repoRoot = path.resolve(import.meta.dirname, '../../..')
|
|
20
26
|
|
|
@@ -28,7 +34,7 @@ function parseArgs(argv) {
|
|
|
28
34
|
const out = {
|
|
29
35
|
dataset: '', round: '', strategy: 'v3', limit: 0, splits: 'train,dev,eval', only: '',
|
|
30
36
|
onlyFailed: '', noCorrect: false, focus: '', focusBelow: 0.85, focusLimit: 0,
|
|
31
|
-
resume: false, inherit: '', noSummary: false,
|
|
37
|
+
resume: false, inherit: '', noSummary: false, noSink: false, concurrency: 2,
|
|
32
38
|
}
|
|
33
39
|
const camel = (raw) => raw.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase())
|
|
34
40
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -86,6 +92,7 @@ function copyReferencedSlices(schema, sourceSlicesDir, targetSlicesDir) {
|
|
|
86
92
|
referenced.add(m[1])
|
|
87
93
|
}
|
|
88
94
|
let copied = 0
|
|
95
|
+
const files = []
|
|
89
96
|
const visit = (dir) => {
|
|
90
97
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
91
98
|
const full = path.join(dir, entry.name)
|
|
@@ -93,15 +100,20 @@ function copyReferencedSlices(schema, sourceSlicesDir, targetSlicesDir) {
|
|
|
93
100
|
visit(full)
|
|
94
101
|
continue
|
|
95
102
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
fs.copyFileSync(full, path.join(targetSlicesDir, relative))
|
|
100
|
-
copied += 1
|
|
101
|
-
}
|
|
103
|
+
// 扁平名(/ → _);空格等保留字符不再就地替换——匹配统一走 sliceNameMatches
|
|
104
|
+
// (与 schema 引用名同源消毒,r31c 等式)。旧数据集源文件名可带空格/保留字符。
|
|
105
|
+
files.push({ relative: path.relative(sourceSlicesDir, full).split(path.sep).join('_'), full })
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
visit(sourceSlicesDir)
|
|
109
|
+
for (const ref of referenced) {
|
|
110
|
+
const hit = files.find((f) => sliceNameMatches(f.relative, ref))
|
|
111
|
+
if (!hit) continue
|
|
112
|
+
// 落盘名 = 引用名(消毒后),URL 才能解析;源名可以带空格/保留字符(历史数据集兼容)
|
|
113
|
+
fs.mkdirSync(targetSlicesDir, { recursive: true })
|
|
114
|
+
fs.copyFileSync(hit.full, path.join(targetSlicesDir, ref))
|
|
115
|
+
copied += 1
|
|
116
|
+
}
|
|
105
117
|
return copied
|
|
106
118
|
}
|
|
107
119
|
|
|
@@ -111,10 +123,10 @@ function runValidator(cardId, cardsDir) {
|
|
|
111
123
|
path.join(cardsDir, cardId, 'schema.json'),
|
|
112
124
|
], { encoding: 'utf8' })
|
|
113
125
|
if (result.error) {
|
|
114
|
-
return { errors: 1, warnings: 0, output: String(result.error.message || result.error) }
|
|
126
|
+
return { errors: 1, warnings: 0, output: String(result.error.message || result.error), error: String(result.error.message || result.error) }
|
|
115
127
|
}
|
|
116
128
|
if (result.status !== 0 && !/\[result\]/.test(`${result.stdout || ''}${result.stderr || ''}`)) {
|
|
117
|
-
return { errors: 1, warnings: 0, output: `${result.stdout || ''}${result.stderr || ''}` }
|
|
129
|
+
return { errors: 1, warnings: 0, output: `${result.stdout || ''}${result.stderr || ''}`, error: `validator-exit-${result.status}` }
|
|
118
130
|
}
|
|
119
131
|
const output = `${result.stdout || ''}${result.stderr || ''}`
|
|
120
132
|
const errors = (output.match(/\[ERROR\]/g) || []).length
|
|
@@ -152,12 +164,16 @@ async function runMechanicalAutofix(schemaPath) {
|
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
import {
|
|
155
|
-
|
|
167
|
+
positionScore, textOverflowScore, duplicationScore, fidelityScore, fontMaskWiden,
|
|
156
168
|
loadFontMetrics,
|
|
157
169
|
evaluatePassGate, attributeFailure, failureSignature, exclusionCoverage,
|
|
158
170
|
errorKind,
|
|
159
171
|
} from './lib/scoring.mjs'
|
|
160
172
|
import { loadRunReports } from './lib/runs.mjs'
|
|
173
|
+
import { estimateReviewTokens, estimateStructuredReviewTokens, createStageTimer } from './lib/telemetry.mjs'
|
|
174
|
+
import { initPixelWorkers, closePixelWorkers, scorePixel, pixelWorkerStatus } from './lib/pixel-worker.mjs'
|
|
175
|
+
import { buildInlineDiagnosis } from './lib/diagnosis.mjs'
|
|
176
|
+
import { canSimulateReject, simulateLayoutCorrection } from './lib/layout-correction-sim.mjs'
|
|
161
177
|
|
|
162
178
|
|
|
163
179
|
function sourceChanges() {
|
|
@@ -168,6 +184,9 @@ function sourceChanges() {
|
|
|
168
184
|
return String(result.stdout || '').split('\n').map((line) => line.trim()).filter(Boolean).sort()
|
|
169
185
|
}
|
|
170
186
|
|
|
187
|
+
// ── Telemetry helpers 在 lib/telemetry.mjs(pngSize / estimateReviewTokens / createStageTimer,
|
|
188
|
+
// tools/tests/trainer-telemetry.test.mjs 直测该模块)──
|
|
189
|
+
|
|
171
190
|
function textRecall(schema, expectedTexts) {
|
|
172
191
|
if (!expectedTexts?.length) return null
|
|
173
192
|
const rendered = new Set()
|
|
@@ -298,8 +317,13 @@ function upstreamQuality(spec, card) {
|
|
|
298
317
|
? retainedTextCount / expectedTexts.length
|
|
299
318
|
: 1
|
|
300
319
|
const metrics = card.focusMetrics || {}
|
|
301
|
-
|
|
302
|
-
|
|
320
|
+
// 结构聚焦两条路(2026-09-28):
|
|
321
|
+
// a) 常规卡:rich ≥ 2 且有文本或切片;
|
|
322
|
+
// b) 图像卡:切片覆盖 ≥ 60% 卡区(整卡被设计稿导成一张大图,卡内无其他标注)。
|
|
323
|
+
// 该形态解析器兜底产 1 张卡,结构上只有这张切片,按像素/尺寸门正常评分。
|
|
324
|
+
const structuralFocus = ((metrics.richComponentCount || 0) >= 2 &&
|
|
325
|
+
((metrics.textCount || 0) > 0 || (metrics.sliceCount || 0) > 0)) ||
|
|
326
|
+
(metrics.sliceAreaRatio || 0) >= 0.6
|
|
303
327
|
const passed = card.trainingEligible !== false &&
|
|
304
328
|
!!card.image &&
|
|
305
329
|
structuralFocus &&
|
|
@@ -777,46 +801,95 @@ async function main() {
|
|
|
777
801
|
console.log(`[inherit] 从 ${path.basename(inheritFrom)} 继承 ${inheritedCases.length} 例,本轮重跑 ${cases.length} 例`)
|
|
778
802
|
}
|
|
779
803
|
const sourceBefore = sourceChanges()
|
|
804
|
+
const roundStartedAt = Date.now()
|
|
780
805
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
// 不必白起一套 Chrome + HTTP server(本机实测 ~2-14s 启停开销)
|
|
806
|
+
// 渲染会话 per-worker 懒启动(2026-09-25 起支持 --concurrency 并发):
|
|
807
|
+
// 每个 worker 持有独立 Chrome + HTTP server(render_card.mjs 的 server.listen(0) 随机端口、
|
|
808
|
+
// mkdtempSync 唯一 userDataDir,多实例天然安全)。worker=1 时行为与旧单会话串行完全一致。
|
|
809
|
+
// 上限 4:Chrome 内存线性涨,且 CPU 侧像素评分(同步循环)会互相阻塞事件循环,再多无益。
|
|
786
810
|
if (!cases.length) console.log('[skip] 本轮无待跑案例(选样为空或已全部完成)')
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
811
|
+
// 像素评分重计算已下沉 worker_threads(P0-2),主事件循环不再被同步评分阻塞,
|
|
812
|
+
// 故并发上限由 4 提到 6;再高会受 Chrome 内存与 spawnSync 校验/ lint 串行拖累。
|
|
813
|
+
const workerCount = Math.max(1, Math.min(Number(args.concurrency) || 2, cases.length || 1, 6))
|
|
814
|
+
// 像素评分 worker 池:池大小与渲染会话数一致,避免 oversubscription;
|
|
815
|
+
// worker 不可用时 scorePixel 自动同步回退,训练轮不崩。
|
|
816
|
+
if (cases.length) initPixelWorkers(workerCount)
|
|
817
|
+
if (workerCount > 1) console.log(`[concurrency] ${workerCount} 个渲染会话并行`)
|
|
818
|
+
// —— 轮级 telemetry:耗时 / 错误 / token 三类信号,收尾落 reports/telemetry.json ——
|
|
819
|
+
const telemetry = {
|
|
820
|
+
round: args.round,
|
|
821
|
+
startedAt: new Date().toISOString(),
|
|
822
|
+
concurrency: workerCount,
|
|
823
|
+
pixelWorker: pixelWorkerStatus(),
|
|
824
|
+
cases: cases.length,
|
|
825
|
+
scoredCases: 0,
|
|
826
|
+
renderCalls: 0,
|
|
827
|
+
renderRetries: 0,
|
|
828
|
+
sessionRebuilds: 0,
|
|
829
|
+
stageMs: {},
|
|
830
|
+
stageErrors: {},
|
|
831
|
+
caseErrors: 0,
|
|
832
|
+
usage: null,
|
|
833
|
+
reviewTokensEst: { total: 0, cases: 0 },
|
|
791
834
|
}
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
835
|
+
const accumulateStage = (name, ms) => {
|
|
836
|
+
telemetry.stageMs[name] = (telemetry.stageMs[name] || 0) + ms
|
|
837
|
+
}
|
|
838
|
+
const recordStageError = (stage, message) => {
|
|
839
|
+
telemetry.stageErrors[stage] = (telemetry.stageErrors[stage] || 0) + 1
|
|
840
|
+
return { stage, message: String(message).slice(0, 300) }
|
|
841
|
+
}
|
|
842
|
+
const makeWorkerSession = () => {
|
|
843
|
+
const state = { session: null }
|
|
844
|
+
const ensureRenderSession = async () => {
|
|
845
|
+
if (!state.session) state.session = await createRenderSession({ cardsRoot: cardsDir })
|
|
846
|
+
return state.session
|
|
847
|
+
}
|
|
848
|
+
const renderOnce = async (id, w, h) => {
|
|
849
|
+
telemetry.renderCalls += 1
|
|
850
|
+
let res = await (await ensureRenderSession()).render({
|
|
801
851
|
id, width: w, captureHeight: h, format: 'png', captureElements: true,
|
|
802
852
|
})
|
|
853
|
+
if (!res.ok && /session-closed|Target|WebSocket|Chrome|浏览器|超时/i.test(String(res.error || '') + String(res.message || ''))) {
|
|
854
|
+
// Chrome 崩溃/断连:重建会话再试一次,避免整轮作废
|
|
855
|
+
telemetry.renderRetries += 1
|
|
856
|
+
telemetry.sessionRebuilds += 1
|
|
857
|
+
try { await state.session.close() } catch {}
|
|
858
|
+
state.session = await createRenderSession({ cardsRoot: cardsDir })
|
|
859
|
+
res = await state.session.render({
|
|
860
|
+
id, width: w, captureHeight: h, format: 'png', captureElements: true,
|
|
861
|
+
})
|
|
862
|
+
}
|
|
863
|
+
return res
|
|
803
864
|
}
|
|
804
|
-
return
|
|
865
|
+
return { state, ensureRenderSession, renderOnce }
|
|
805
866
|
}
|
|
806
|
-
|
|
807
|
-
|
|
867
|
+
const liveSessions = []
|
|
868
|
+
const processCase = async (testCase, index, worker) => {
|
|
808
869
|
const started = Date.now()
|
|
809
870
|
const cardId = `${args.round}__${testCase.caseId}`
|
|
871
|
+
const timer = createStageTimer()
|
|
872
|
+
const stageErrors = []
|
|
873
|
+
// try 外提升:catch / 收尾 telemetry 汇总也要读(案例中途失败时可能是 null)
|
|
874
|
+
let designBuf = null
|
|
875
|
+
let renderBuf = null
|
|
876
|
+
let designCard = null
|
|
877
|
+
// P1-2:失败案例结构化诊断(在环归因证据包),try 内算出、收尾 telemetry / semantic-review 复用
|
|
878
|
+
let _diag = null
|
|
810
879
|
const report = {
|
|
811
880
|
caseId: testCase.caseId, cardId, source: testCase.source,
|
|
812
881
|
artboardName: testCase.artboardName, split: testCase.split,
|
|
813
882
|
validate: { errors: 0 },
|
|
814
883
|
lint: { errors: 0 },
|
|
884
|
+
stageErrors,
|
|
885
|
+
timings: timer.timings,
|
|
815
886
|
}
|
|
816
887
|
try {
|
|
888
|
+
// 设计稿只读一次(校正守卫 / 最终像素评分 / token 估算三处复用,省重复磁盘 IO)
|
|
889
|
+
designBuf = timer.time('parse', () => fs.readFileSync(testCase.input.designImage))
|
|
817
890
|
const spec = JSON.parse(fs.readFileSync(testCase.input.specJson, 'utf8'))
|
|
818
891
|
const cardPosition = Number(testCase.cardIndex) || 0
|
|
819
|
-
|
|
892
|
+
designCard = (spec.cards || [])[cardPosition]
|
|
820
893
|
if (!designCard) throw new Error('focus-card-missing-or-skipped')
|
|
821
894
|
// 纯文本区域(对话气泡/回复段落)不是卡片:无富组件(壳/切图/符号)且 ≥3 文本块。
|
|
822
895
|
// 用户规则:对话气泡一律跳过,不计入训练指标(避免污染还原度统计与生成器归因)。
|
|
@@ -824,9 +897,9 @@ async function main() {
|
|
|
824
897
|
if ((_fm.richComponentCount || 0) === 0 && (_fm.textCount || 0) >= 3) {
|
|
825
898
|
console.log('[SKIP] conversation-text ' + testCase.caseId)
|
|
826
899
|
skippedConversationTexts.push({ caseId: testCase.caseId, source: testCase.source, artboardName: testCase.artboardName })
|
|
827
|
-
|
|
900
|
+
return
|
|
828
901
|
}
|
|
829
|
-
report.upstream = upstreamQuality(spec, designCard)
|
|
902
|
+
report.upstream = timer.time('upstream', () => upstreamQuality(spec, designCard))
|
|
830
903
|
const _cardRectForExcl = designCard.rect || {}
|
|
831
904
|
const _exclCardW = Math.max(1, _cardRectForExcl.width || 1)
|
|
832
905
|
const _exclCardH = Math.max(1, _cardRectForExcl.height || 1)
|
|
@@ -879,25 +952,38 @@ async function main() {
|
|
|
879
952
|
}
|
|
880
953
|
const renderWidth = Math.round(designCard?.rect?.width || spec.size?.width || 375)
|
|
881
954
|
const renderHeight = Math.round(designCard?.rect?.height || spec.size?.height || 0) || null
|
|
882
|
-
const generated = generate(spec, {
|
|
955
|
+
const generated = timer.time('generate', () => generate(spec, {
|
|
883
956
|
id: cardId,
|
|
884
957
|
name: testCase.artboardName,
|
|
885
958
|
cardIndex: testCase.cardIndex || 0,
|
|
886
959
|
designImage: testCase.input.designImage,
|
|
887
|
-
})
|
|
960
|
+
}))
|
|
961
|
+
// LLM usage 挂钩(接口预留):v3 纯规则引擎当前恒无 usage;生成链路未来 LLM 化
|
|
962
|
+
// (meta.usage = { inputTokens, outputTokens, calls })后此处自动累计,无需再改 trainer。
|
|
963
|
+
if (generated.meta?.usage) {
|
|
964
|
+
report.usage = generated.meta.usage
|
|
965
|
+
telemetry.usage = telemetry.usage || { inputTokens: 0, outputTokens: 0, calls: 0 }
|
|
966
|
+
telemetry.usage.inputTokens += generated.meta.usage.inputTokens || 0
|
|
967
|
+
telemetry.usage.outputTokens += generated.meta.usage.outputTokens || 0
|
|
968
|
+
telemetry.usage.calls += generated.meta.usage.calls || 1
|
|
969
|
+
}
|
|
888
970
|
const cardDir = path.join(cardsDir, cardId)
|
|
889
971
|
fs.mkdirSync(cardDir, { recursive: true })
|
|
890
972
|
fs.writeFileSync(path.join(cardDir, 'schema.json'), JSON.stringify(generated.schema, null, 2) + '\n')
|
|
891
973
|
fs.writeFileSync(path.join(cardDir, 'data.json'), JSON.stringify(generated.data, null, 2) + '\n')
|
|
892
|
-
report.repair = runMechanicalAutofix(path.join(cardDir, 'schema.json'))
|
|
893
|
-
report.sliceCount = copyReferencedSlices(generated.schema, testCase.input.slicesDir, slicesDir)
|
|
894
|
-
report.validate = runValidator(cardId, cardsDir)
|
|
895
|
-
report.
|
|
896
|
-
|
|
974
|
+
report.repair = timer.time('autofix', () => runMechanicalAutofix(path.join(cardDir, 'schema.json')))
|
|
975
|
+
report.sliceCount = timer.time('slices', () => copyReferencedSlices(generated.schema, testCase.input.slicesDir, slicesDir))
|
|
976
|
+
report.validate = timer.time('validate', () => runValidator(cardId, cardsDir))
|
|
977
|
+
if (report.validate.error) stageErrors.push(recordStageError('validate', report.validate.error))
|
|
978
|
+
report.lint = timer.time('lint', () => runLinter(cardId, cardsDir, slicesDir))
|
|
979
|
+
if (report.lint.error) stageErrors.push(recordStageError('lint', report.lint.error))
|
|
980
|
+
let rendered = await timer.timeAsync('render', () => worker.renderOnce(cardId, renderWidth, renderHeight))
|
|
897
981
|
if (!rendered.ok) {
|
|
898
982
|
report.renderRetry = { firstError: rendered.error || rendered.message }
|
|
983
|
+
stageErrors.push(recordStageError('render', rendered.message || rendered.error))
|
|
984
|
+
telemetry.renderRetries += 1
|
|
899
985
|
await sleep(500)
|
|
900
|
-
rendered = await renderOnce(cardId, renderWidth, renderHeight)
|
|
986
|
+
rendered = await timer.timeAsync('render', () => worker.renderOnce(cardId, renderWidth, renderHeight))
|
|
901
987
|
}
|
|
902
988
|
if (!rendered.ok) throw new Error(rendered.message || rendered.error || 'render failed')
|
|
903
989
|
|
|
@@ -956,23 +1042,18 @@ async function main() {
|
|
|
956
1042
|
const cardColForSim = generated.schema.view?.components?.[0]
|
|
957
1043
|
const corrected = !args.noCorrect && correctCardLayout(generated.schema, rendered.elementBoxes, _deduped, _cr, _adjustments, _sim)
|
|
958
1044
|
// ── 模拟否决快路径 ──
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
//
|
|
1045
|
+
// 纵向 paddingTop(Y)与横向 paddingLeft(X)校正,在各自轴向上都是精确的刚体平移:
|
|
1046
|
+
// Y:子树盒 +delta,后续兄弟盒 +累计 delta(纵向流式堆叠)。
|
|
1047
|
+
// X:子树盒 +delta,不跨兄弟级联(纵向流里每子项横向独立,各自 paddingLeft)。
|
|
1048
|
+
// 详见 lib/layout-correction-sim.mjs。模拟 position 分不提升 ⇒ 真实渲染必然不提升,
|
|
1049
|
+
// 可直接回退并跳过二次渲染(每轮省 ~20 次渲染)。
|
|
1050
|
+
// 原逻辑仅准入纯 Y 调整,含 X 调整必二次渲染——现放宽到 Y/X 且 rigidSafe 均成立。
|
|
962
1051
|
// CORR_SIM_VALIDATE=1 时模拟与真实渲染双跑对账,用于验证模拟器保真。
|
|
963
1052
|
let _simRejectPos = null
|
|
964
1053
|
let _simLastPos = null
|
|
965
|
-
if (corrected && _adjustments.
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
&& ((cardColForSim?.slots?.[0]?.components) || []).every((c) => c.name !== 'AbsoluteLayout')) {
|
|
969
|
-
const _deltas = new Map()
|
|
970
|
-
for (const a of _adjustments) _deltas.set(a.childIndex, (_deltas.get(a.childIndex) || 0) + (a.to - a.from))
|
|
971
|
-
const _shiftFor = (idx) => { let acc = 0; for (const [i, d] of _deltas) if (i <= idx) acc += d; return acc }
|
|
972
|
-
const _shifted = (rendered.elementBoxes || []).map((b) => {
|
|
973
|
-
const idx = _sim.cidToChild.get(b.cid)
|
|
974
|
-
return idx == null ? b : { ...b, y: b.y + _shiftFor(idx) }
|
|
975
|
-
})
|
|
1054
|
+
if (corrected && canSimulateReject(_adjustments, _sim.cidToChild, cardColForSim?.slots?.[0]?.components)) {
|
|
1055
|
+
const { shifted: _shifted } = simulateLayoutCorrection(
|
|
1056
|
+
rendered.elementBoxes || [], _adjustments, _sim.cidToChild)
|
|
976
1057
|
const _simPos = positionScore(_deduped,
|
|
977
1058
|
_shifted.filter((el) => el.text && ['Text', 'Input', 'Button'].includes(el.cls)),
|
|
978
1059
|
{ cardW: _cr.width || renderWidth, cardH: _cr.height || renderHeight, specShapes: _specShapes })
|
|
@@ -989,14 +1070,15 @@ async function main() {
|
|
|
989
1070
|
}
|
|
990
1071
|
} else if (corrected) {
|
|
991
1072
|
fs.writeFileSync(path.join(cardDir, 'schema.json'), JSON.stringify(generated.schema, null, 2) + '\n')
|
|
992
|
-
const correctedRendered = await renderOnce(cardId, renderWidth, renderHeight)
|
|
1073
|
+
const correctedRendered = await timer.timeAsync('reRender', () => worker.renderOnce(cardId, renderWidth, renderHeight))
|
|
993
1074
|
if (correctedRendered.ok) {
|
|
994
1075
|
const _correctedPos = positionScore(_deduped,
|
|
995
1076
|
(correctedRendered.elementBoxes || []).filter((el) => el.text && ['Text', 'Input', 'Button'].includes(el.cls)),
|
|
996
1077
|
{ cardW: _cr.width || renderWidth, cardH: _cr.height || renderHeight, specShapes: _specShapes })
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
const _pixelAfter =
|
|
1078
|
+
const _pixelBefore = await timer.timeAsync('pixel', () =>
|
|
1079
|
+
scorePixel(designBuf, Buffer.from(_renderedBackup.base64, 'base64'), exclusionRects, [], { includeAlignDy: false }))
|
|
1080
|
+
const _pixelAfter = await timer.timeAsync('pixel', () =>
|
|
1081
|
+
scorePixel(designBuf, Buffer.from(correctedRendered.base64, 'base64'), exclusionRects, [], { includeAlignDy: false }))
|
|
1000
1082
|
const _pixelOk = _pixelAfter.ratio <= _pixelBefore.ratio + 0.005
|
|
1001
1083
|
if (process.env.CORR_DEBUG) console.error('[corr] initialPos=' + _initialPos.overall.toFixed(4) + ' correctedPos=' + _correctedPos.overall.toFixed(4) + ' pixel ' + _pixelBefore.ratio.toFixed(4) + '→' + _pixelAfter.ratio.toFixed(4))
|
|
1002
1084
|
if (process.env.CORR_SIM_VALIDATE && _simLastPos != null) {
|
|
@@ -1025,7 +1107,8 @@ async function main() {
|
|
|
1025
1107
|
}
|
|
1026
1108
|
|
|
1027
1109
|
const renderPath = path.join(reportDir, `${testCase.caseId}.render.png`)
|
|
1028
|
-
|
|
1110
|
+
renderBuf = Buffer.from(rendered.base64, 'base64')
|
|
1111
|
+
fs.writeFileSync(renderPath, renderBuf)
|
|
1029
1112
|
// 同步保存设计稿参考图到 reports 目录,便于 summary.md 并排对比
|
|
1030
1113
|
try { fs.copyFileSync(testCase.input.designImage, path.join(reportDir, testCase.caseId + '.design.png')) } catch {}
|
|
1031
1114
|
// ── 裁切兜底切图掩膜:导出素材缺失 → 只能从画板裁切(__artboard_crop__*)──
|
|
@@ -1037,8 +1120,8 @@ async function main() {
|
|
|
1037
1120
|
for (const element of designCard.elements || []) {
|
|
1038
1121
|
const defect = element.annotationDefect
|
|
1039
1122
|
const cropFile = defect?.cropSlice || (defect?.resolution === 'artboard-crop' ? defect?.name : null)
|
|
1040
|
-
if (!defect || defect.type !== 'missing-slice' || !cropFile) continue
|
|
1041
|
-
if (!schemaJson.includes(cropFile)) continue
|
|
1123
|
+
if (!defect || (defect.type !== 'missing-slice' && defect.type !== 'slice-visual-mismatch') || !cropFile) continue
|
|
1124
|
+
if (!(schemaJson.includes(cropFile) || schemaJson.includes(cropFile.replace(/\s+/g, '-')))) continue
|
|
1042
1125
|
const pad = 1.5
|
|
1043
1126
|
const base = toExclRect(element, 1)
|
|
1044
1127
|
exclusionRects.push({
|
|
@@ -1050,12 +1133,13 @@ async function main() {
|
|
|
1050
1133
|
})
|
|
1051
1134
|
}
|
|
1052
1135
|
}
|
|
1053
|
-
report.pixel =
|
|
1054
|
-
|
|
1055
|
-
|
|
1136
|
+
report.pixel = await timer.timeAsync('pixel', () => scorePixel(
|
|
1137
|
+
designBuf,
|
|
1138
|
+
renderBuf,
|
|
1056
1139
|
exclusionRects,
|
|
1057
1140
|
fontRects,
|
|
1058
|
-
|
|
1141
|
+
{ includeAlignDy: true },
|
|
1142
|
+
))
|
|
1059
1143
|
report.pixelExclusions = [...exclusionRects, ...fontRects]
|
|
1060
1144
|
// 掩膜覆盖率:掩膜是「不计分」而不是「还原对」。覆盖率过高时通过结论不可信,
|
|
1061
1145
|
// score_round 会把高掩膜案例单列出来要求人工复核(防止靠掩膜虚高过门)。
|
|
@@ -1113,22 +1197,24 @@ async function main() {
|
|
|
1113
1197
|
return { x: (r.x || 0) - (cardRect.x || 0), y: (r.y || 0) - (cardRect.y || 0), w: r.width || 0, h: r.height || 0 }
|
|
1114
1198
|
})
|
|
1115
1199
|
.filter((el) => el.w > 0 && el.h > 0)
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1200
|
+
timer.time('score', () => {
|
|
1201
|
+
report.position = positionScore(specTexts, renderedTexts, {
|
|
1202
|
+
cardW: cardRect.width || renderWidth,
|
|
1203
|
+
cardH: cardRect.height || renderHeight,
|
|
1204
|
+
specShapes: _specShapesFinal,
|
|
1205
|
+
})
|
|
1206
|
+
report.textOverflow = textOverflowScore(rendered.elementBoxes || [])
|
|
1207
|
+
// 语义重复渲染检测(重影):同一文案渲染次数不得超出标注次数
|
|
1208
|
+
report.duplication = duplicationScore(specTexts, renderedTexts)
|
|
1209
|
+
report.fidelity = fidelityScore(
|
|
1210
|
+
report.pixel,
|
|
1211
|
+
expectedComponents(designCard),
|
|
1212
|
+
schemaComponents(generated.schema),
|
|
1213
|
+
report.textRecall,
|
|
1214
|
+
report.position,
|
|
1215
|
+
report.textOverflow,
|
|
1216
|
+
)
|
|
1120
1217
|
})
|
|
1121
|
-
report.textOverflow = textOverflowScore(rendered.elementBoxes || [])
|
|
1122
|
-
// 语义重复渲染检测(重影):同一文案渲染次数不得超出标注次数
|
|
1123
|
-
report.duplication = duplicationScore(specTexts, renderedTexts)
|
|
1124
|
-
report.fidelity = fidelityScore(
|
|
1125
|
-
report.pixel,
|
|
1126
|
-
expectedComponents(designCard),
|
|
1127
|
-
schemaComponents(generated.schema),
|
|
1128
|
-
report.textRecall,
|
|
1129
|
-
report.position,
|
|
1130
|
-
report.textOverflow,
|
|
1131
|
-
)
|
|
1132
1218
|
// 单案例门以 lib/scoring.mjs 的 PASS_GATE 为唯一来源(SKILL.md 验收表、score_round 同源,
|
|
1133
1219
|
// 文档数字由 tools/tests/scoring.test.mjs 反向断言),并把未达标维度按缺口落盘供聚类归因
|
|
1134
1220
|
const gate = evaluatePassGate(report)
|
|
@@ -1140,9 +1226,16 @@ async function main() {
|
|
|
1140
1226
|
report.failureClass = attribution.failureClass
|
|
1141
1227
|
report.failureReason = attribution.reason
|
|
1142
1228
|
}
|
|
1229
|
+
// P1-2:失败案例在环归因用的轻量结构化证据包(零额外渲染,复用 report / 设计稿 buffer /
|
|
1230
|
+
// 标注文本 / 渲染盒),塞进 semantic-review 队列供 Agent 消费,避免直接读双 PNG 烧图像 token。
|
|
1231
|
+
if (report.pixel?.comparable) {
|
|
1232
|
+
_diag = buildInlineDiagnosis(report, { designBuf, renderBuf, specTexts, renderedTexts })
|
|
1233
|
+
}
|
|
1143
1234
|
} catch (error) {
|
|
1144
1235
|
report.error = String(error.message || error)
|
|
1145
1236
|
report.passed = false
|
|
1237
|
+
stageErrors.push(recordStageError('case', report.error))
|
|
1238
|
+
telemetry.caseErrors += 1
|
|
1146
1239
|
report.failureSignature = failureSignature(report)
|
|
1147
1240
|
const attribution = attributeFailure(report)
|
|
1148
1241
|
if (attribution) {
|
|
@@ -1151,6 +1244,21 @@ async function main() {
|
|
|
1151
1244
|
}
|
|
1152
1245
|
}
|
|
1153
1246
|
report.durationMs = Date.now() - started
|
|
1247
|
+
// —— telemetry 汇总(阶段耗时 / 失败案例归因证据包 token 估算)——
|
|
1248
|
+
telemetry.scoredCases += 1
|
|
1249
|
+
for (const [stage, ms] of Object.entries(timer.timings)) accumulateStage(stage, ms)
|
|
1250
|
+
if (!report.passed) {
|
|
1251
|
+
const specChars = (() => { try { return JSON.stringify(designCard ?? {}).length } catch { return 0 } })()
|
|
1252
|
+
// P1-2:失败案例在环归因默认消费结构化 diagnosis(见下方 semanticReviewQueue 的 diagnosis
|
|
1253
|
+
// 字段),不再强制读取双 PNG;故 token 估算以结构化证据包为主,旧双图估算留存
|
|
1254
|
+
// reviewTokensEstImage 供对照。两者均不参与任何门禁,仅作预算参考。
|
|
1255
|
+
const estImage = estimateReviewTokens({ designBuffer: designBuf, renderBuffer: renderBuf, specChars })
|
|
1256
|
+
const est = _diag ? estimateStructuredReviewTokens(_diag) : estImage
|
|
1257
|
+
report.reviewTokensEst = est
|
|
1258
|
+
report.reviewTokensEstImage = estImage
|
|
1259
|
+
telemetry.reviewTokensEst.total += est.total
|
|
1260
|
+
telemetry.reviewTokensEst.cases += 1
|
|
1261
|
+
}
|
|
1154
1262
|
fs.writeFileSync(path.join(reportDir, `${testCase.caseId}.json`),
|
|
1155
1263
|
JSON.stringify(report, null, 2) + '\n')
|
|
1156
1264
|
if (!report.passed) reviewQueue.push({
|
|
@@ -1174,6 +1282,10 @@ async function main() {
|
|
|
1174
1282
|
color: report.pixel.color ?? null,
|
|
1175
1283
|
components: report.fidelity?.components ?? null,
|
|
1176
1284
|
},
|
|
1285
|
+
// P1-2:结构化证据包(零额外渲染,复用 report/设计稿 buffer/标注文本/渲染盒)。
|
|
1286
|
+
// 在环 Agent 据此做聚类与归因,避免直接加载两张 PNG 烧图像 token。
|
|
1287
|
+
// provenance(cid→生成器源码行)按需由 diagnose_case.mjs 深度补全。
|
|
1288
|
+
diagnosis: _diag,
|
|
1177
1289
|
evidence: {
|
|
1178
1290
|
designImage: testCase.input.designImage,
|
|
1179
1291
|
renderImage: path.join(reportDir, `${testCase.caseId}.render.png`),
|
|
@@ -1181,14 +1293,44 @@ async function main() {
|
|
|
1181
1293
|
cardIndex: testCase.cardIndex || 0,
|
|
1182
1294
|
schema: path.join(cardsDir, cardId, 'schema.json'),
|
|
1183
1295
|
},
|
|
1184
|
-
instruction: '
|
|
1296
|
+
instruction: '优先消费结构化「diagnosis」字段做聚类与归因:gateFailures(按 deficit 降序的未达标维度)、deviations(文本 dy/dx 偏差表)、hotBands(像素热区带)、metrics、hints(已给失败类建议)。仅当结构化证据不足以定位根因时,才打开 evidence.images 两张 PNG 复核。归因只输出通用 UI/DSL 规则,禁止硬编码特定卡片文案或业务实体名称。',
|
|
1185
1297
|
})
|
|
1186
|
-
console.log(`[${index + 1}/${cases.length}] ${report.passed ? 'PASS' : 'FAIL'} ${testCase.caseId}`)
|
|
1298
|
+
console.log(`[${index + 1}/${cases.length}]${workerCount > 1 ? ` (w${worker.id})` : ''} ${report.passed ? 'PASS' : 'FAIL'} ${testCase.caseId} ${report.durationMs}ms`)
|
|
1187
1299
|
}
|
|
1300
|
+
// worker 池:cursor 单调分派案例,每 worker 持独立渲染会话;单案例异常已在 processCase 内消化
|
|
1301
|
+
try {
|
|
1302
|
+
let cursor = 0
|
|
1303
|
+
const workerMain = async (workerId) => {
|
|
1304
|
+
const worker = { id: workerId + 1, ...makeWorkerSession() }
|
|
1305
|
+
liveSessions.push(worker.state)
|
|
1306
|
+
while (cursor < cases.length) {
|
|
1307
|
+
const index = cursor++
|
|
1308
|
+
await processCase(cases[index], index, worker)
|
|
1309
|
+
}
|
|
1310
|
+
try { await worker.state.session?.close() } catch {}
|
|
1311
|
+
}
|
|
1312
|
+
await Promise.all(Array.from({ length: workerCount }, (_, i) => workerMain(i)))
|
|
1188
1313
|
} finally {
|
|
1189
|
-
// 任何异常路径都要关掉 Chrome + HTTP server
|
|
1190
|
-
try { await
|
|
1314
|
+
// 任何异常路径都要关掉 Chrome + HTTP server,杜绝进程残留(worker 异常中断时兜底)
|
|
1315
|
+
await Promise.all(liveSessions.map(async (s) => { try { await s.session?.close() } catch {} }))
|
|
1316
|
+
try { await closePixelWorkers() } catch {}
|
|
1191
1317
|
}
|
|
1318
|
+
// —— 轮级 telemetry 落盘:错误 / 耗时 / token 三类信号的唯一轮级出口 ——
|
|
1319
|
+
const wallMs = Date.now() - roundStartedAt
|
|
1320
|
+
const stageAverages = Object.fromEntries(
|
|
1321
|
+
Object.entries(telemetry.stageMs)
|
|
1322
|
+
.map(([stage, ms]) => [stage, Math.round(ms / Math.max(telemetry.scoredCases, 1))]))
|
|
1323
|
+
const telemetryOut = {
|
|
1324
|
+
...telemetry,
|
|
1325
|
+
finishedAt: new Date().toISOString(),
|
|
1326
|
+
wallMs,
|
|
1327
|
+
stageAverages,
|
|
1328
|
+
wallSecondsPerCase: telemetry.scoredCases ? Number((wallMs / telemetry.scoredCases / 1000).toFixed(2)) : null,
|
|
1329
|
+
casesPerMinute: wallMs > 0 && telemetry.scoredCases ? Number((telemetry.scoredCases / (wallMs / 60000)).toFixed(2)) : null,
|
|
1330
|
+
reviewTokensNote: '失败案例 Agent 在环归因证据包的 input token 估算(图像面积/750 + spec 文本/4 chars);v3 生成器为纯规则引擎无 LLM 调用,usage 字段为生成链路 LLM 化后预留',
|
|
1331
|
+
}
|
|
1332
|
+
fs.writeFileSync(path.join(reportDir, 'telemetry.json'), JSON.stringify(telemetryOut, null, 2) + '\n')
|
|
1333
|
+
console.log(`[telemetry] ${telemetry.scoredCases} 例 / ${workerCount} 并发 / 墙钟 ${(wallMs / 1000).toFixed(1)}s(${telemetryOut.casesPerMinute ?? '-'} 例/分钟 · 渲染 ${telemetry.renderCalls} 次${telemetry.renderRetries ? ` · 重试 ${telemetry.renderRetries}` : ''}${telemetry.caseErrors ? ` · 案例错误 ${telemetry.caseErrors}` : ''})`)
|
|
1192
1334
|
fs.writeFileSync(path.join(reportDir, 'review-queue.jsonl'),
|
|
1193
1335
|
reviewQueue.map((item) => JSON.stringify(item)).join('\n') + (reviewQueue.length ? '\n' : ''))
|
|
1194
1336
|
fs.writeFileSync(path.join(reportDir, 'semantic-review.jsonl'),
|
|
@@ -1206,6 +1348,58 @@ async function main() {
|
|
|
1206
1348
|
if (scored.status !== 0) console.warn(`[WARN] score_round 退出码 ${scored.status}(summary 可能未写出)`)
|
|
1207
1349
|
}
|
|
1208
1350
|
|
|
1351
|
+
|
|
1352
|
+
// —— 轮后落盘:plan_next_round + lesson 草稿自动沉淀 + 训练总结 ——
|
|
1353
|
+
if (!args.noSink) {
|
|
1354
|
+
const planScript = path.join(import.meta.dirname, 'plan_next_round.mjs')
|
|
1355
|
+
const planArgs = [planScript, '--run', runDir, '--history', 'auto']
|
|
1356
|
+
if (args.anchor) planArgs.push('--anchor', String(args.anchor))
|
|
1357
|
+
const planned = spawnSync(process.execPath, planArgs, { encoding: 'utf8', cwd: repoRoot })
|
|
1358
|
+
const planOutput = `${planned.stdout || ''}${planned.stderr || ''}`.trim()
|
|
1359
|
+
if (planOutput) console.log(planOutput)
|
|
1360
|
+
if (planned.status !== 0) console.warn(`[WARN] plan_next_round 退出码 ${planned.status}(落盘可能不完整)`)
|
|
1361
|
+
|
|
1362
|
+
// 自动沉淀 lesson 草稿到 training/lessons/lessons.jsonl(按 signature 去重)
|
|
1363
|
+
const draftsFile = path.join(reportDir, 'lesson-drafts.jsonl')
|
|
1364
|
+
const lessonsFile = path.join(repoRoot, 'training', 'lessons', 'lessons.jsonl')
|
|
1365
|
+
if (fs.existsSync(draftsFile)) {
|
|
1366
|
+
const drafts = fs.readFileSync(draftsFile, 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l))
|
|
1367
|
+
if (drafts.length) {
|
|
1368
|
+
const existingSignatures = new Set()
|
|
1369
|
+
if (fs.existsSync(lessonsFile)) {
|
|
1370
|
+
for (const line of fs.readFileSync(lessonsFile, 'utf8').split('\n').filter(Boolean)) {
|
|
1371
|
+
try { const row = JSON.parse(line); if (row.signature) existingSignatures.add(row.signature.toLowerCase()) } catch {}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const newLessons = drafts.filter((d) => d.signature && !existingSignatures.has(d.signature.toLowerCase()))
|
|
1375
|
+
if (newLessons.length) {
|
|
1376
|
+
fs.appendFileSync(lessonsFile, newLessons.map((l) => JSON.stringify(l)).join('\n') + '\n')
|
|
1377
|
+
console.log(`[sink] ${newLessons.length} 条新 lesson 沉淀到 ${path.relative(repoRoot, lessonsFile)}`)
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// 回灌规则到目标 skill 文档(lessons → generation_pitfalls.md / SKILL.md 等)
|
|
1383
|
+
const sinkScript = path.join(import.meta.dirname, 'sink_rules.mjs')
|
|
1384
|
+
const sunk = spawnSync(process.execPath, [sinkScript], { encoding: 'utf8', cwd: repoRoot })
|
|
1385
|
+
const sinkOutput = `${sunk.stdout || ''}${sunk.stderr || ''}`.trim()
|
|
1386
|
+
if (sinkOutput) console.log(sinkOutput.split('\n').map((l) => l.startsWith('[') ? l : null).filter(Boolean).join('\n'))
|
|
1387
|
+
if (sunk.status !== 0) console.warn(`[WARN] sink_rules 退出码 ${sunk.status}`)
|
|
1388
|
+
|
|
1389
|
+
// 写 TRAINING-SUMMARY.md(人读训练总结)
|
|
1390
|
+
const summaryFile = path.join(reportDir, 'summary.json')
|
|
1391
|
+
let summaryData = null
|
|
1392
|
+
try { summaryData = JSON.parse(fs.readFileSync(summaryFile, 'utf8')) } catch {}
|
|
1393
|
+
if (summaryData) {
|
|
1394
|
+
const pct = (v) => v == null ? 'N/A' : (v * 100).toFixed(1) + '%'
|
|
1395
|
+
const roundName = path.basename(runDir)
|
|
1396
|
+
const passed = summaryData.roundGatePassed
|
|
1397
|
+
const md = `# 训练总结 — ${roundName}\n\n## 门禁结果:${summaryData.roundGatePassed ? 'PASS' : 'FAIL'}\n\n| 维度 | 分数 | 门禁 | 状态 |\n|------|------|------|------|\n| 还原度 | ${pct(summaryData.avgFidelity)} | >=85% | ${passed ? 'PASS' : 'FAIL'} |\n| 像素差异 | ${pct(summaryData.avgPixelDiff)} | <=8% | ${passed ? 'PASS' : 'FAIL'} |\n| 位置精度 | ${pct(summaryData.avgPositionFidelity)} | >=80% | ${passed ? 'PASS' : 'FAIL'} |\n| 结构还原 | ${pct(summaryData.avgStructureFidelity)} | >=85% | ${passed ? 'PASS' : 'FAIL'} |\n| 通过率 | ${pct(summaryData.passRate)} | >=95% | ${passed ? 'PASS' : 'FAIL'} |\n\n- 案例: ${summaryData.total} / 通过: ${summaryData.passed} / 失败: ${summaryData.total - summaryData.passed}\n- 渲染调用: ${summaryData.telemetry?.renderCalls ?? 'N/A'}\n- 墙钟: ${((summaryData.telemetry?.wallMs ?? 0) / 1000).toFixed(1)}s\n- 失败签名: ${(summaryData.failureSignatures || []).map((s) => s.signature + 'x' + s.count).join(', ') || '无'}\n\n## 产物路径\n- 训练产物: \`training/runs/${roundName}/\`\n- 诊断报告: \`training/runs/${roundName}/reports/\`\n- 下一轮计划: \`training/runs/${roundName}/reports/next-round-plan.json\`\n- Lesson 草稿: \`training/runs/${roundName}/reports/lesson-drafts.jsonl\`\n`
|
|
1398
|
+
fs.writeFileSync(path.join(runDir, 'TRAINING-SUMMARY.md'), md)
|
|
1399
|
+
console.log(`[sink] 训练总结写入 ${path.relative(repoRoot, path.join(runDir, 'TRAINING-SUMMARY.md'))}`)
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1209
1403
|
const newSourceChanges = sourceChanges().filter((line) => !sourceBefore.includes(line))
|
|
1210
1404
|
if (newSourceChanges.length) {
|
|
1211
1405
|
throw new Error(`SANDBOX_VIOLATION: training wrote into src/: ${newSourceChanges.join(', ')}`)
|