cardbuddy 0.14.3 → 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/cli.js +0 -0
- package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
- package/dist/preview-editor/assets/index-Bro07DTa.js +313 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/standalone.js +3 -0
- package/dist/vendored/card-editor/runtime/adapter.js +2 -2
- package/dist/vendored/mcp/client.mjs +9 -2
- package/dist/vendored/server/core/ai-chat.js +1702 -134
- package/dist/vendored/server/core/browser.js +12 -3
- package/dist/vendored/server/core/cardFiles.js +3 -0
- package/dist/vendored/server/core/cardHistory.js +1 -0
- package/dist/vendored/server/core/cardImportMeta.js +7 -6
- package/dist/vendored/server/core/cardOpenFolder.js +1 -0
- package/dist/vendored/server/core/cardSave.js +1 -0
- package/dist/vendored/server/core/cardSync.js +3 -0
- package/dist/vendored/server/core/cardValidate.js +16 -5
- package/dist/vendored/server/core/codeBridge.js +5 -1
- package/dist/vendored/server/core/logStream.js +1 -0
- package/dist/vendored/server/core/sliceStatic.js +24 -4
- package/dist/vendored/server/core/sliceUpload.js +1 -0
- package/dist/vendored/server/core/turnJournal.js +6 -0
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +257 -201
- 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-ai/SKILL.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +208 -380
- 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 +233 -61
- package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +202 -4
- package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +87 -0
- package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +42 -33
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +59 -11
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +44 -8
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +26 -6
- package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +54 -0
- package/dist/vendored/skills/dsl-card-generator/references/semantic_adjudication.md +23 -0
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
- package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +33 -4
- package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +13 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
- package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +133 -2
- package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +226 -19
- package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +6 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
- package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +92 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +277 -27
- package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +147 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +12 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/ir_merge.mjs +288 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/ir_restore.mjs +175 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +1015 -54
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/geometry_audit.mjs +131 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout_rules.mjs +160 -0
- 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/slicePath.mjs +17 -5
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +102 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +5727 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +4214 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +1110 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +322 -71
- package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +87 -43
- 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/render_card.mjs +91 -43
- package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +7 -11
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +272 -46
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +43 -10499
- package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +188 -0
- 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 +43 -20
- package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +4 -4
- package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +47 -9
- 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 +546 -91
- package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +51 -5
- package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +217 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +64 -34
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-Cm0USikc.js +0 -307
- package/dist/preview-editor/assets/index-cbTCHp8J.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()
|
|
@@ -777,46 +796,95 @@ async function main() {
|
|
|
777
796
|
console.log(`[inherit] 从 ${path.basename(inheritFrom)} 继承 ${inheritedCases.length} 例,本轮重跑 ${cases.length} 例`)
|
|
778
797
|
}
|
|
779
798
|
const sourceBefore = sourceChanges()
|
|
799
|
+
const roundStartedAt = Date.now()
|
|
780
800
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
// 不必白起一套 Chrome + HTTP server(本机实测 ~2-14s 启停开销)
|
|
801
|
+
// 渲染会话 per-worker 懒启动(2026-09-25 起支持 --concurrency 并发):
|
|
802
|
+
// 每个 worker 持有独立 Chrome + HTTP server(render_card.mjs 的 server.listen(0) 随机端口、
|
|
803
|
+
// mkdtempSync 唯一 userDataDir,多实例天然安全)。worker=1 时行为与旧单会话串行完全一致。
|
|
804
|
+
// 上限 4:Chrome 内存线性涨,且 CPU 侧像素评分(同步循环)会互相阻塞事件循环,再多无益。
|
|
786
805
|
if (!cases.length) console.log('[skip] 本轮无待跑案例(选样为空或已全部完成)')
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
806
|
+
// 像素评分重计算已下沉 worker_threads(P0-2),主事件循环不再被同步评分阻塞,
|
|
807
|
+
// 故并发上限由 4 提到 6;再高会受 Chrome 内存与 spawnSync 校验/ lint 串行拖累。
|
|
808
|
+
const workerCount = Math.max(1, Math.min(Number(args.concurrency) || 2, cases.length || 1, 6))
|
|
809
|
+
// 像素评分 worker 池:池大小与渲染会话数一致,避免 oversubscription;
|
|
810
|
+
// worker 不可用时 scorePixel 自动同步回退,训练轮不崩。
|
|
811
|
+
if (cases.length) initPixelWorkers(workerCount)
|
|
812
|
+
if (workerCount > 1) console.log(`[concurrency] ${workerCount} 个渲染会话并行`)
|
|
813
|
+
// —— 轮级 telemetry:耗时 / 错误 / token 三类信号,收尾落 reports/telemetry.json ——
|
|
814
|
+
const telemetry = {
|
|
815
|
+
round: args.round,
|
|
816
|
+
startedAt: new Date().toISOString(),
|
|
817
|
+
concurrency: workerCount,
|
|
818
|
+
pixelWorker: pixelWorkerStatus(),
|
|
819
|
+
cases: cases.length,
|
|
820
|
+
scoredCases: 0,
|
|
821
|
+
renderCalls: 0,
|
|
822
|
+
renderRetries: 0,
|
|
823
|
+
sessionRebuilds: 0,
|
|
824
|
+
stageMs: {},
|
|
825
|
+
stageErrors: {},
|
|
826
|
+
caseErrors: 0,
|
|
827
|
+
usage: null,
|
|
828
|
+
reviewTokensEst: { total: 0, cases: 0 },
|
|
791
829
|
}
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
830
|
+
const accumulateStage = (name, ms) => {
|
|
831
|
+
telemetry.stageMs[name] = (telemetry.stageMs[name] || 0) + ms
|
|
832
|
+
}
|
|
833
|
+
const recordStageError = (stage, message) => {
|
|
834
|
+
telemetry.stageErrors[stage] = (telemetry.stageErrors[stage] || 0) + 1
|
|
835
|
+
return { stage, message: String(message).slice(0, 300) }
|
|
836
|
+
}
|
|
837
|
+
const makeWorkerSession = () => {
|
|
838
|
+
const state = { session: null }
|
|
839
|
+
const ensureRenderSession = async () => {
|
|
840
|
+
if (!state.session) state.session = await createRenderSession({ cardsRoot: cardsDir })
|
|
841
|
+
return state.session
|
|
842
|
+
}
|
|
843
|
+
const renderOnce = async (id, w, h) => {
|
|
844
|
+
telemetry.renderCalls += 1
|
|
845
|
+
let res = await (await ensureRenderSession()).render({
|
|
801
846
|
id, width: w, captureHeight: h, format: 'png', captureElements: true,
|
|
802
847
|
})
|
|
848
|
+
if (!res.ok && /session-closed|Target|WebSocket|Chrome|浏览器|超时/i.test(String(res.error || '') + String(res.message || ''))) {
|
|
849
|
+
// Chrome 崩溃/断连:重建会话再试一次,避免整轮作废
|
|
850
|
+
telemetry.renderRetries += 1
|
|
851
|
+
telemetry.sessionRebuilds += 1
|
|
852
|
+
try { await state.session.close() } catch {}
|
|
853
|
+
state.session = await createRenderSession({ cardsRoot: cardsDir })
|
|
854
|
+
res = await state.session.render({
|
|
855
|
+
id, width: w, captureHeight: h, format: 'png', captureElements: true,
|
|
856
|
+
})
|
|
857
|
+
}
|
|
858
|
+
return res
|
|
803
859
|
}
|
|
804
|
-
return
|
|
860
|
+
return { state, ensureRenderSession, renderOnce }
|
|
805
861
|
}
|
|
806
|
-
|
|
807
|
-
|
|
862
|
+
const liveSessions = []
|
|
863
|
+
const processCase = async (testCase, index, worker) => {
|
|
808
864
|
const started = Date.now()
|
|
809
865
|
const cardId = `${args.round}__${testCase.caseId}`
|
|
866
|
+
const timer = createStageTimer()
|
|
867
|
+
const stageErrors = []
|
|
868
|
+
// try 外提升:catch / 收尾 telemetry 汇总也要读(案例中途失败时可能是 null)
|
|
869
|
+
let designBuf = null
|
|
870
|
+
let renderBuf = null
|
|
871
|
+
let designCard = null
|
|
872
|
+
// P1-2:失败案例结构化诊断(在环归因证据包),try 内算出、收尾 telemetry / semantic-review 复用
|
|
873
|
+
let _diag = null
|
|
810
874
|
const report = {
|
|
811
875
|
caseId: testCase.caseId, cardId, source: testCase.source,
|
|
812
876
|
artboardName: testCase.artboardName, split: testCase.split,
|
|
813
877
|
validate: { errors: 0 },
|
|
814
878
|
lint: { errors: 0 },
|
|
879
|
+
stageErrors,
|
|
880
|
+
timings: timer.timings,
|
|
815
881
|
}
|
|
816
882
|
try {
|
|
883
|
+
// 设计稿只读一次(校正守卫 / 最终像素评分 / token 估算三处复用,省重复磁盘 IO)
|
|
884
|
+
designBuf = timer.time('parse', () => fs.readFileSync(testCase.input.designImage))
|
|
817
885
|
const spec = JSON.parse(fs.readFileSync(testCase.input.specJson, 'utf8'))
|
|
818
886
|
const cardPosition = Number(testCase.cardIndex) || 0
|
|
819
|
-
|
|
887
|
+
designCard = (spec.cards || [])[cardPosition]
|
|
820
888
|
if (!designCard) throw new Error('focus-card-missing-or-skipped')
|
|
821
889
|
// 纯文本区域(对话气泡/回复段落)不是卡片:无富组件(壳/切图/符号)且 ≥3 文本块。
|
|
822
890
|
// 用户规则:对话气泡一律跳过,不计入训练指标(避免污染还原度统计与生成器归因)。
|
|
@@ -824,9 +892,9 @@ async function main() {
|
|
|
824
892
|
if ((_fm.richComponentCount || 0) === 0 && (_fm.textCount || 0) >= 3) {
|
|
825
893
|
console.log('[SKIP] conversation-text ' + testCase.caseId)
|
|
826
894
|
skippedConversationTexts.push({ caseId: testCase.caseId, source: testCase.source, artboardName: testCase.artboardName })
|
|
827
|
-
|
|
895
|
+
return
|
|
828
896
|
}
|
|
829
|
-
report.upstream = upstreamQuality(spec, designCard)
|
|
897
|
+
report.upstream = timer.time('upstream', () => upstreamQuality(spec, designCard))
|
|
830
898
|
const _cardRectForExcl = designCard.rect || {}
|
|
831
899
|
const _exclCardW = Math.max(1, _cardRectForExcl.width || 1)
|
|
832
900
|
const _exclCardH = Math.max(1, _cardRectForExcl.height || 1)
|
|
@@ -879,25 +947,38 @@ async function main() {
|
|
|
879
947
|
}
|
|
880
948
|
const renderWidth = Math.round(designCard?.rect?.width || spec.size?.width || 375)
|
|
881
949
|
const renderHeight = Math.round(designCard?.rect?.height || spec.size?.height || 0) || null
|
|
882
|
-
const generated = generate(spec, {
|
|
950
|
+
const generated = timer.time('generate', () => generate(spec, {
|
|
883
951
|
id: cardId,
|
|
884
952
|
name: testCase.artboardName,
|
|
885
953
|
cardIndex: testCase.cardIndex || 0,
|
|
886
954
|
designImage: testCase.input.designImage,
|
|
887
|
-
})
|
|
955
|
+
}))
|
|
956
|
+
// LLM usage 挂钩(接口预留):v3 纯规则引擎当前恒无 usage;生成链路未来 LLM 化
|
|
957
|
+
// (meta.usage = { inputTokens, outputTokens, calls })后此处自动累计,无需再改 trainer。
|
|
958
|
+
if (generated.meta?.usage) {
|
|
959
|
+
report.usage = generated.meta.usage
|
|
960
|
+
telemetry.usage = telemetry.usage || { inputTokens: 0, outputTokens: 0, calls: 0 }
|
|
961
|
+
telemetry.usage.inputTokens += generated.meta.usage.inputTokens || 0
|
|
962
|
+
telemetry.usage.outputTokens += generated.meta.usage.outputTokens || 0
|
|
963
|
+
telemetry.usage.calls += generated.meta.usage.calls || 1
|
|
964
|
+
}
|
|
888
965
|
const cardDir = path.join(cardsDir, cardId)
|
|
889
966
|
fs.mkdirSync(cardDir, { recursive: true })
|
|
890
967
|
fs.writeFileSync(path.join(cardDir, 'schema.json'), JSON.stringify(generated.schema, null, 2) + '\n')
|
|
891
968
|
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
|
-
|
|
969
|
+
report.repair = timer.time('autofix', () => runMechanicalAutofix(path.join(cardDir, 'schema.json')))
|
|
970
|
+
report.sliceCount = timer.time('slices', () => copyReferencedSlices(generated.schema, testCase.input.slicesDir, slicesDir))
|
|
971
|
+
report.validate = timer.time('validate', () => runValidator(cardId, cardsDir))
|
|
972
|
+
if (report.validate.error) stageErrors.push(recordStageError('validate', report.validate.error))
|
|
973
|
+
report.lint = timer.time('lint', () => runLinter(cardId, cardsDir, slicesDir))
|
|
974
|
+
if (report.lint.error) stageErrors.push(recordStageError('lint', report.lint.error))
|
|
975
|
+
let rendered = await timer.timeAsync('render', () => worker.renderOnce(cardId, renderWidth, renderHeight))
|
|
897
976
|
if (!rendered.ok) {
|
|
898
977
|
report.renderRetry = { firstError: rendered.error || rendered.message }
|
|
978
|
+
stageErrors.push(recordStageError('render', rendered.message || rendered.error))
|
|
979
|
+
telemetry.renderRetries += 1
|
|
899
980
|
await sleep(500)
|
|
900
|
-
rendered = await renderOnce(cardId, renderWidth, renderHeight)
|
|
981
|
+
rendered = await timer.timeAsync('render', () => worker.renderOnce(cardId, renderWidth, renderHeight))
|
|
901
982
|
}
|
|
902
983
|
if (!rendered.ok) throw new Error(rendered.message || rendered.error || 'render failed')
|
|
903
984
|
|
|
@@ -956,23 +1037,18 @@ async function main() {
|
|
|
956
1037
|
const cardColForSim = generated.schema.view?.components?.[0]
|
|
957
1038
|
const corrected = !args.noCorrect && correctCardLayout(generated.schema, rendered.elementBoxes, _deduped, _cr, _adjustments, _sim)
|
|
958
1039
|
// ── 模拟否决快路径 ──
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
//
|
|
1040
|
+
// 纵向 paddingTop(Y)与横向 paddingLeft(X)校正,在各自轴向上都是精确的刚体平移:
|
|
1041
|
+
// Y:子树盒 +delta,后续兄弟盒 +累计 delta(纵向流式堆叠)。
|
|
1042
|
+
// X:子树盒 +delta,不跨兄弟级联(纵向流里每子项横向独立,各自 paddingLeft)。
|
|
1043
|
+
// 详见 lib/layout-correction-sim.mjs。模拟 position 分不提升 ⇒ 真实渲染必然不提升,
|
|
1044
|
+
// 可直接回退并跳过二次渲染(每轮省 ~20 次渲染)。
|
|
1045
|
+
// 原逻辑仅准入纯 Y 调整,含 X 调整必二次渲染——现放宽到 Y/X 且 rigidSafe 均成立。
|
|
962
1046
|
// CORR_SIM_VALIDATE=1 时模拟与真实渲染双跑对账,用于验证模拟器保真。
|
|
963
1047
|
let _simRejectPos = null
|
|
964
1048
|
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
|
-
})
|
|
1049
|
+
if (corrected && canSimulateReject(_adjustments, _sim.cidToChild, cardColForSim?.slots?.[0]?.components)) {
|
|
1050
|
+
const { shifted: _shifted } = simulateLayoutCorrection(
|
|
1051
|
+
rendered.elementBoxes || [], _adjustments, _sim.cidToChild)
|
|
976
1052
|
const _simPos = positionScore(_deduped,
|
|
977
1053
|
_shifted.filter((el) => el.text && ['Text', 'Input', 'Button'].includes(el.cls)),
|
|
978
1054
|
{ cardW: _cr.width || renderWidth, cardH: _cr.height || renderHeight, specShapes: _specShapes })
|
|
@@ -989,14 +1065,15 @@ async function main() {
|
|
|
989
1065
|
}
|
|
990
1066
|
} else if (corrected) {
|
|
991
1067
|
fs.writeFileSync(path.join(cardDir, 'schema.json'), JSON.stringify(generated.schema, null, 2) + '\n')
|
|
992
|
-
const correctedRendered = await renderOnce(cardId, renderWidth, renderHeight)
|
|
1068
|
+
const correctedRendered = await timer.timeAsync('reRender', () => worker.renderOnce(cardId, renderWidth, renderHeight))
|
|
993
1069
|
if (correctedRendered.ok) {
|
|
994
1070
|
const _correctedPos = positionScore(_deduped,
|
|
995
1071
|
(correctedRendered.elementBoxes || []).filter((el) => el.text && ['Text', 'Input', 'Button'].includes(el.cls)),
|
|
996
1072
|
{ cardW: _cr.width || renderWidth, cardH: _cr.height || renderHeight, specShapes: _specShapes })
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
const _pixelAfter =
|
|
1073
|
+
const _pixelBefore = await timer.timeAsync('pixel', () =>
|
|
1074
|
+
scorePixel(designBuf, Buffer.from(_renderedBackup.base64, 'base64'), exclusionRects, [], { includeAlignDy: false }))
|
|
1075
|
+
const _pixelAfter = await timer.timeAsync('pixel', () =>
|
|
1076
|
+
scorePixel(designBuf, Buffer.from(correctedRendered.base64, 'base64'), exclusionRects, [], { includeAlignDy: false }))
|
|
1000
1077
|
const _pixelOk = _pixelAfter.ratio <= _pixelBefore.ratio + 0.005
|
|
1001
1078
|
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
1079
|
if (process.env.CORR_SIM_VALIDATE && _simLastPos != null) {
|
|
@@ -1025,7 +1102,8 @@ async function main() {
|
|
|
1025
1102
|
}
|
|
1026
1103
|
|
|
1027
1104
|
const renderPath = path.join(reportDir, `${testCase.caseId}.render.png`)
|
|
1028
|
-
|
|
1105
|
+
renderBuf = Buffer.from(rendered.base64, 'base64')
|
|
1106
|
+
fs.writeFileSync(renderPath, renderBuf)
|
|
1029
1107
|
// 同步保存设计稿参考图到 reports 目录,便于 summary.md 并排对比
|
|
1030
1108
|
try { fs.copyFileSync(testCase.input.designImage, path.join(reportDir, testCase.caseId + '.design.png')) } catch {}
|
|
1031
1109
|
// ── 裁切兜底切图掩膜:导出素材缺失 → 只能从画板裁切(__artboard_crop__*)──
|
|
@@ -1037,8 +1115,8 @@ async function main() {
|
|
|
1037
1115
|
for (const element of designCard.elements || []) {
|
|
1038
1116
|
const defect = element.annotationDefect
|
|
1039
1117
|
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
|
|
1118
|
+
if (!defect || (defect.type !== 'missing-slice' && defect.type !== 'slice-visual-mismatch') || !cropFile) continue
|
|
1119
|
+
if (!(schemaJson.includes(cropFile) || schemaJson.includes(cropFile.replace(/\s+/g, '-')))) continue
|
|
1042
1120
|
const pad = 1.5
|
|
1043
1121
|
const base = toExclRect(element, 1)
|
|
1044
1122
|
exclusionRects.push({
|
|
@@ -1050,12 +1128,13 @@ async function main() {
|
|
|
1050
1128
|
})
|
|
1051
1129
|
}
|
|
1052
1130
|
}
|
|
1053
|
-
report.pixel =
|
|
1054
|
-
|
|
1055
|
-
|
|
1131
|
+
report.pixel = await timer.timeAsync('pixel', () => scorePixel(
|
|
1132
|
+
designBuf,
|
|
1133
|
+
renderBuf,
|
|
1056
1134
|
exclusionRects,
|
|
1057
1135
|
fontRects,
|
|
1058
|
-
|
|
1136
|
+
{ includeAlignDy: true },
|
|
1137
|
+
))
|
|
1059
1138
|
report.pixelExclusions = [...exclusionRects, ...fontRects]
|
|
1060
1139
|
// 掩膜覆盖率:掩膜是「不计分」而不是「还原对」。覆盖率过高时通过结论不可信,
|
|
1061
1140
|
// score_round 会把高掩膜案例单列出来要求人工复核(防止靠掩膜虚高过门)。
|
|
@@ -1113,22 +1192,24 @@ async function main() {
|
|
|
1113
1192
|
return { x: (r.x || 0) - (cardRect.x || 0), y: (r.y || 0) - (cardRect.y || 0), w: r.width || 0, h: r.height || 0 }
|
|
1114
1193
|
})
|
|
1115
1194
|
.filter((el) => el.w > 0 && el.h > 0)
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1195
|
+
timer.time('score', () => {
|
|
1196
|
+
report.position = positionScore(specTexts, renderedTexts, {
|
|
1197
|
+
cardW: cardRect.width || renderWidth,
|
|
1198
|
+
cardH: cardRect.height || renderHeight,
|
|
1199
|
+
specShapes: _specShapesFinal,
|
|
1200
|
+
})
|
|
1201
|
+
report.textOverflow = textOverflowScore(rendered.elementBoxes || [])
|
|
1202
|
+
// 语义重复渲染检测(重影):同一文案渲染次数不得超出标注次数
|
|
1203
|
+
report.duplication = duplicationScore(specTexts, renderedTexts)
|
|
1204
|
+
report.fidelity = fidelityScore(
|
|
1205
|
+
report.pixel,
|
|
1206
|
+
expectedComponents(designCard),
|
|
1207
|
+
schemaComponents(generated.schema),
|
|
1208
|
+
report.textRecall,
|
|
1209
|
+
report.position,
|
|
1210
|
+
report.textOverflow,
|
|
1211
|
+
)
|
|
1120
1212
|
})
|
|
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
1213
|
// 单案例门以 lib/scoring.mjs 的 PASS_GATE 为唯一来源(SKILL.md 验收表、score_round 同源,
|
|
1133
1214
|
// 文档数字由 tools/tests/scoring.test.mjs 反向断言),并把未达标维度按缺口落盘供聚类归因
|
|
1134
1215
|
const gate = evaluatePassGate(report)
|
|
@@ -1140,9 +1221,16 @@ async function main() {
|
|
|
1140
1221
|
report.failureClass = attribution.failureClass
|
|
1141
1222
|
report.failureReason = attribution.reason
|
|
1142
1223
|
}
|
|
1224
|
+
// P1-2:失败案例在环归因用的轻量结构化证据包(零额外渲染,复用 report / 设计稿 buffer /
|
|
1225
|
+
// 标注文本 / 渲染盒),塞进 semantic-review 队列供 Agent 消费,避免直接读双 PNG 烧图像 token。
|
|
1226
|
+
if (report.pixel?.comparable) {
|
|
1227
|
+
_diag = buildInlineDiagnosis(report, { designBuf, renderBuf, specTexts, renderedTexts })
|
|
1228
|
+
}
|
|
1143
1229
|
} catch (error) {
|
|
1144
1230
|
report.error = String(error.message || error)
|
|
1145
1231
|
report.passed = false
|
|
1232
|
+
stageErrors.push(recordStageError('case', report.error))
|
|
1233
|
+
telemetry.caseErrors += 1
|
|
1146
1234
|
report.failureSignature = failureSignature(report)
|
|
1147
1235
|
const attribution = attributeFailure(report)
|
|
1148
1236
|
if (attribution) {
|
|
@@ -1151,6 +1239,21 @@ async function main() {
|
|
|
1151
1239
|
}
|
|
1152
1240
|
}
|
|
1153
1241
|
report.durationMs = Date.now() - started
|
|
1242
|
+
// —— telemetry 汇总(阶段耗时 / 失败案例归因证据包 token 估算)——
|
|
1243
|
+
telemetry.scoredCases += 1
|
|
1244
|
+
for (const [stage, ms] of Object.entries(timer.timings)) accumulateStage(stage, ms)
|
|
1245
|
+
if (!report.passed) {
|
|
1246
|
+
const specChars = (() => { try { return JSON.stringify(designCard ?? {}).length } catch { return 0 } })()
|
|
1247
|
+
// P1-2:失败案例在环归因默认消费结构化 diagnosis(见下方 semanticReviewQueue 的 diagnosis
|
|
1248
|
+
// 字段),不再强制读取双 PNG;故 token 估算以结构化证据包为主,旧双图估算留存
|
|
1249
|
+
// reviewTokensEstImage 供对照。两者均不参与任何门禁,仅作预算参考。
|
|
1250
|
+
const estImage = estimateReviewTokens({ designBuffer: designBuf, renderBuffer: renderBuf, specChars })
|
|
1251
|
+
const est = _diag ? estimateStructuredReviewTokens(_diag) : estImage
|
|
1252
|
+
report.reviewTokensEst = est
|
|
1253
|
+
report.reviewTokensEstImage = estImage
|
|
1254
|
+
telemetry.reviewTokensEst.total += est.total
|
|
1255
|
+
telemetry.reviewTokensEst.cases += 1
|
|
1256
|
+
}
|
|
1154
1257
|
fs.writeFileSync(path.join(reportDir, `${testCase.caseId}.json`),
|
|
1155
1258
|
JSON.stringify(report, null, 2) + '\n')
|
|
1156
1259
|
if (!report.passed) reviewQueue.push({
|
|
@@ -1174,6 +1277,10 @@ async function main() {
|
|
|
1174
1277
|
color: report.pixel.color ?? null,
|
|
1175
1278
|
components: report.fidelity?.components ?? null,
|
|
1176
1279
|
},
|
|
1280
|
+
// P1-2:结构化证据包(零额外渲染,复用 report/设计稿 buffer/标注文本/渲染盒)。
|
|
1281
|
+
// 在环 Agent 据此做聚类与归因,避免直接加载两张 PNG 烧图像 token。
|
|
1282
|
+
// provenance(cid→生成器源码行)按需由 diagnose_case.mjs 深度补全。
|
|
1283
|
+
diagnosis: _diag,
|
|
1177
1284
|
evidence: {
|
|
1178
1285
|
designImage: testCase.input.designImage,
|
|
1179
1286
|
renderImage: path.join(reportDir, `${testCase.caseId}.render.png`),
|
|
@@ -1181,14 +1288,44 @@ async function main() {
|
|
|
1181
1288
|
cardIndex: testCase.cardIndex || 0,
|
|
1182
1289
|
schema: path.join(cardsDir, cardId, 'schema.json'),
|
|
1183
1290
|
},
|
|
1184
|
-
instruction: '
|
|
1291
|
+
instruction: '优先消费结构化「diagnosis」字段做聚类与归因:gateFailures(按 deficit 降序的未达标维度)、deviations(文本 dy/dx 偏差表)、hotBands(像素热区带)、metrics、hints(已给失败类建议)。仅当结构化证据不足以定位根因时,才打开 evidence.images 两张 PNG 复核。归因只输出通用 UI/DSL 规则,禁止硬编码特定卡片文案或业务实体名称。',
|
|
1185
1292
|
})
|
|
1186
|
-
console.log(`[${index + 1}/${cases.length}] ${report.passed ? 'PASS' : 'FAIL'} ${testCase.caseId}`)
|
|
1293
|
+
console.log(`[${index + 1}/${cases.length}]${workerCount > 1 ? ` (w${worker.id})` : ''} ${report.passed ? 'PASS' : 'FAIL'} ${testCase.caseId} ${report.durationMs}ms`)
|
|
1187
1294
|
}
|
|
1295
|
+
// worker 池:cursor 单调分派案例,每 worker 持独立渲染会话;单案例异常已在 processCase 内消化
|
|
1296
|
+
try {
|
|
1297
|
+
let cursor = 0
|
|
1298
|
+
const workerMain = async (workerId) => {
|
|
1299
|
+
const worker = { id: workerId + 1, ...makeWorkerSession() }
|
|
1300
|
+
liveSessions.push(worker.state)
|
|
1301
|
+
while (cursor < cases.length) {
|
|
1302
|
+
const index = cursor++
|
|
1303
|
+
await processCase(cases[index], index, worker)
|
|
1304
|
+
}
|
|
1305
|
+
try { await worker.state.session?.close() } catch {}
|
|
1306
|
+
}
|
|
1307
|
+
await Promise.all(Array.from({ length: workerCount }, (_, i) => workerMain(i)))
|
|
1188
1308
|
} finally {
|
|
1189
|
-
// 任何异常路径都要关掉 Chrome + HTTP server
|
|
1190
|
-
try { await
|
|
1309
|
+
// 任何异常路径都要关掉 Chrome + HTTP server,杜绝进程残留(worker 异常中断时兜底)
|
|
1310
|
+
await Promise.all(liveSessions.map(async (s) => { try { await s.session?.close() } catch {} }))
|
|
1311
|
+
try { await closePixelWorkers() } catch {}
|
|
1191
1312
|
}
|
|
1313
|
+
// —— 轮级 telemetry 落盘:错误 / 耗时 / token 三类信号的唯一轮级出口 ——
|
|
1314
|
+
const wallMs = Date.now() - roundStartedAt
|
|
1315
|
+
const stageAverages = Object.fromEntries(
|
|
1316
|
+
Object.entries(telemetry.stageMs)
|
|
1317
|
+
.map(([stage, ms]) => [stage, Math.round(ms / Math.max(telemetry.scoredCases, 1))]))
|
|
1318
|
+
const telemetryOut = {
|
|
1319
|
+
...telemetry,
|
|
1320
|
+
finishedAt: new Date().toISOString(),
|
|
1321
|
+
wallMs,
|
|
1322
|
+
stageAverages,
|
|
1323
|
+
wallSecondsPerCase: telemetry.scoredCases ? Number((wallMs / telemetry.scoredCases / 1000).toFixed(2)) : null,
|
|
1324
|
+
casesPerMinute: wallMs > 0 && telemetry.scoredCases ? Number((telemetry.scoredCases / (wallMs / 60000)).toFixed(2)) : null,
|
|
1325
|
+
reviewTokensNote: '失败案例 Agent 在环归因证据包的 input token 估算(图像面积/750 + spec 文本/4 chars);v3 生成器为纯规则引擎无 LLM 调用,usage 字段为生成链路 LLM 化后预留',
|
|
1326
|
+
}
|
|
1327
|
+
fs.writeFileSync(path.join(reportDir, 'telemetry.json'), JSON.stringify(telemetryOut, null, 2) + '\n')
|
|
1328
|
+
console.log(`[telemetry] ${telemetry.scoredCases} 例 / ${workerCount} 并发 / 墙钟 ${(wallMs / 1000).toFixed(1)}s(${telemetryOut.casesPerMinute ?? '-'} 例/分钟 · 渲染 ${telemetry.renderCalls} 次${telemetry.renderRetries ? ` · 重试 ${telemetry.renderRetries}` : ''}${telemetry.caseErrors ? ` · 案例错误 ${telemetry.caseErrors}` : ''})`)
|
|
1192
1329
|
fs.writeFileSync(path.join(reportDir, 'review-queue.jsonl'),
|
|
1193
1330
|
reviewQueue.map((item) => JSON.stringify(item)).join('\n') + (reviewQueue.length ? '\n' : ''))
|
|
1194
1331
|
fs.writeFileSync(path.join(reportDir, 'semantic-review.jsonl'),
|
|
@@ -1206,6 +1343,58 @@ async function main() {
|
|
|
1206
1343
|
if (scored.status !== 0) console.warn(`[WARN] score_round 退出码 ${scored.status}(summary 可能未写出)`)
|
|
1207
1344
|
}
|
|
1208
1345
|
|
|
1346
|
+
|
|
1347
|
+
// —— 轮后落盘:plan_next_round + lesson 草稿自动沉淀 + 训练总结 ——
|
|
1348
|
+
if (!args.noSink) {
|
|
1349
|
+
const planScript = path.join(import.meta.dirname, 'plan_next_round.mjs')
|
|
1350
|
+
const planArgs = [planScript, '--run', runDir, '--history', 'auto']
|
|
1351
|
+
if (args.anchor) planArgs.push('--anchor', String(args.anchor))
|
|
1352
|
+
const planned = spawnSync(process.execPath, planArgs, { encoding: 'utf8', cwd: repoRoot })
|
|
1353
|
+
const planOutput = `${planned.stdout || ''}${planned.stderr || ''}`.trim()
|
|
1354
|
+
if (planOutput) console.log(planOutput)
|
|
1355
|
+
if (planned.status !== 0) console.warn(`[WARN] plan_next_round 退出码 ${planned.status}(落盘可能不完整)`)
|
|
1356
|
+
|
|
1357
|
+
// 自动沉淀 lesson 草稿到 training/lessons/lessons.jsonl(按 signature 去重)
|
|
1358
|
+
const draftsFile = path.join(reportDir, 'lesson-drafts.jsonl')
|
|
1359
|
+
const lessonsFile = path.join(repoRoot, 'training', 'lessons', 'lessons.jsonl')
|
|
1360
|
+
if (fs.existsSync(draftsFile)) {
|
|
1361
|
+
const drafts = fs.readFileSync(draftsFile, 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l))
|
|
1362
|
+
if (drafts.length) {
|
|
1363
|
+
const existingSignatures = new Set()
|
|
1364
|
+
if (fs.existsSync(lessonsFile)) {
|
|
1365
|
+
for (const line of fs.readFileSync(lessonsFile, 'utf8').split('\n').filter(Boolean)) {
|
|
1366
|
+
try { const row = JSON.parse(line); if (row.signature) existingSignatures.add(row.signature.toLowerCase()) } catch {}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
const newLessons = drafts.filter((d) => d.signature && !existingSignatures.has(d.signature.toLowerCase()))
|
|
1370
|
+
if (newLessons.length) {
|
|
1371
|
+
fs.appendFileSync(lessonsFile, newLessons.map((l) => JSON.stringify(l)).join('\n') + '\n')
|
|
1372
|
+
console.log(`[sink] ${newLessons.length} 条新 lesson 沉淀到 ${path.relative(repoRoot, lessonsFile)}`)
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// 回灌规则到目标 skill 文档(lessons → generation_pitfalls.md / SKILL.md 等)
|
|
1378
|
+
const sinkScript = path.join(import.meta.dirname, 'sink_rules.mjs')
|
|
1379
|
+
const sunk = spawnSync(process.execPath, [sinkScript], { encoding: 'utf8', cwd: repoRoot })
|
|
1380
|
+
const sinkOutput = `${sunk.stdout || ''}${sunk.stderr || ''}`.trim()
|
|
1381
|
+
if (sinkOutput) console.log(sinkOutput.split('\n').map((l) => l.startsWith('[') ? l : null).filter(Boolean).join('\n'))
|
|
1382
|
+
if (sunk.status !== 0) console.warn(`[WARN] sink_rules 退出码 ${sunk.status}`)
|
|
1383
|
+
|
|
1384
|
+
// 写 TRAINING-SUMMARY.md(人读训练总结)
|
|
1385
|
+
const summaryFile = path.join(reportDir, 'summary.json')
|
|
1386
|
+
let summaryData = null
|
|
1387
|
+
try { summaryData = JSON.parse(fs.readFileSync(summaryFile, 'utf8')) } catch {}
|
|
1388
|
+
if (summaryData) {
|
|
1389
|
+
const pct = (v) => v == null ? 'N/A' : (v * 100).toFixed(1) + '%'
|
|
1390
|
+
const roundName = path.basename(runDir)
|
|
1391
|
+
const passed = summaryData.roundGatePassed
|
|
1392
|
+
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`
|
|
1393
|
+
fs.writeFileSync(path.join(runDir, 'TRAINING-SUMMARY.md'), md)
|
|
1394
|
+
console.log(`[sink] 训练总结写入 ${path.relative(repoRoot, path.join(runDir, 'TRAINING-SUMMARY.md'))}`)
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1209
1398
|
const newSourceChanges = sourceChanges().filter((line) => !sourceBefore.includes(line))
|
|
1210
1399
|
if (newSourceChanges.length) {
|
|
1211
1400
|
throw new Error(`SANDBOX_VIOLATION: training wrote into src/: ${newSourceChanges.join(', ')}`)
|