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.
Files changed (108) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
  3. package/dist/preview-editor/assets/index-Bro07DTa.js +313 -0
  4. package/dist/preview-editor/index.html +2 -2
  5. package/dist/standalone.js +3 -0
  6. package/dist/vendored/card-editor/runtime/adapter.js +2 -2
  7. package/dist/vendored/mcp/client.mjs +9 -2
  8. package/dist/vendored/server/core/ai-chat.js +1702 -134
  9. package/dist/vendored/server/core/browser.js +12 -3
  10. package/dist/vendored/server/core/cardFiles.js +3 -0
  11. package/dist/vendored/server/core/cardHistory.js +1 -0
  12. package/dist/vendored/server/core/cardImportMeta.js +7 -6
  13. package/dist/vendored/server/core/cardOpenFolder.js +1 -0
  14. package/dist/vendored/server/core/cardSave.js +1 -0
  15. package/dist/vendored/server/core/cardSync.js +3 -0
  16. package/dist/vendored/server/core/cardValidate.js +16 -5
  17. package/dist/vendored/server/core/codeBridge.js +5 -1
  18. package/dist/vendored/server/core/logStream.js +1 -0
  19. package/dist/vendored/server/core/sliceStatic.js +24 -4
  20. package/dist/vendored/server/core/sliceUpload.js +1 -0
  21. package/dist/vendored/server/core/turnJournal.js +6 -0
  22. package/dist/vendored/skills/card-skill-trainer/SKILL.md +257 -201
  23. package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
  24. package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
  25. package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
  26. package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
  27. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
  28. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
  29. package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
  30. package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
  31. package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
  32. package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
  33. package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
  34. package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
  35. package/dist/vendored/skills/dsl-card-ai/SKILL.md +1 -1
  36. package/dist/vendored/skills/dsl-card-generator/SKILL.md +208 -380
  37. package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
  38. package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
  39. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +233 -61
  40. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +202 -4
  41. package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +87 -0
  42. package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +1 -1
  43. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +42 -33
  44. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +59 -11
  45. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +44 -8
  46. package/dist/vendored/skills/dsl-card-generator/references/rules.md +26 -6
  47. package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +54 -0
  48. package/dist/vendored/skills/dsl-card-generator/references/semantic_adjudication.md +23 -0
  49. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
  50. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +33 -4
  51. package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +13 -1
  52. package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
  53. package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +133 -2
  54. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +226 -19
  55. package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +6 -1
  56. package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
  57. package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +92 -6
  58. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +277 -27
  59. package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +147 -0
  60. package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +12 -8
  61. package/dist/vendored/skills/dsl-card-generator/scripts/ir_merge.mjs +288 -0
  62. package/dist/vendored/skills/dsl-card-generator/scripts/ir_restore.mjs +175 -0
  63. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +1015 -54
  64. package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
  65. package/dist/vendored/skills/dsl-card-generator/scripts/lib/geometry_audit.mjs +131 -0
  66. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
  67. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout_rules.mjs +160 -0
  68. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
  69. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
  70. package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
  71. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
  72. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
  73. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
  74. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
  75. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
  76. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
  77. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
  78. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
  79. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +17 -5
  80. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +102 -0
  81. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +5727 -0
  82. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +4214 -0
  83. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +1110 -0
  84. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +322 -71
  85. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +87 -43
  86. package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
  87. package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
  88. package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +91 -43
  89. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +7 -11
  90. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +272 -46
  91. package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +43 -10499
  92. package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +188 -0
  93. package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
  94. package/dist/vendored/skills/shared/exit-codes.md +79 -0
  95. package/dist/vendored/skills/shared/spec-contract.md +118 -0
  96. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +43 -20
  97. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +4 -4
  98. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +47 -9
  99. package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
  100. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +546 -91
  101. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +51 -5
  102. package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +217 -0
  103. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +64 -34
  104. package/package.json +1 -1
  105. package/dist/preview-editor/assets/index-Cm0USikc.js +0 -307
  106. package/dist/preview-editor/assets/index-cbTCHp8J.css +0 -1
  107. package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
  108. package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * v3_regression.mjs — v3 改动回归门禁(2026-09-21 优化方案 Phase 3:Pass@1 纳入回归指标)。
4
+ *
5
+ * 冻结纪律(SKILL.md §11 / patterns §20)配套:改 v3_extract / v3_assemble 后必须跑本命令,
6
+ * 证明金标卡「首轮生成(无 decisions 干预)过全部门禁 + 与基线零漂移」不降。
7
+ *
8
+ * Pass@1 判据(每张金标卡):
9
+ * 1. generate 首轮成功(spec → schema/data,无 --decisions)
10
+ * 2. gate 全过(autofix → validate → derive --check → lint)
11
+ * 3. schema 与基线快照(training/pass1/baseline/<regId>.schema.json)语义等价
12
+ * (JSON 深比较,忽略格式差异;页 id/名称里的 regId 已归一)
13
+ *
14
+ * 金标注册表(CASES):v3 直组装覆盖的画板 × cardIndex。新增金标:跑
15
+ * `--update-baseline` 前把 case 加进 CASES 并先人工核对一轮产物质量。
16
+ *
17
+ * 用法:
18
+ * node scripts/v3_regression.mjs [--update-baseline] [--only <regId>] [--json]
19
+ *
20
+ * 退出码:0 = 全部通过;1 = gate 失败或基线漂移;2 = 用法/环境错误。
21
+ * --update-baseline:用当前产物刷新基线(仅在人工确认漂移是「预期改进」时使用,
22
+ * 基线文件随本次 v3 改动一起提交,reviewer 据此审漂移)。
23
+ */
24
+ import fs from 'node:fs'
25
+ import path from 'node:path'
26
+ import { spawnSync } from 'node:child_process'
27
+ import { fileURLToPath } from 'node:url'
28
+ import { resolvePython } from './lib/python.mjs'
29
+
30
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
31
+ const SCRIPTS = HERE
32
+ const SKILL = path.resolve(HERE, '..')
33
+ const REPO = path.resolve(SKILL, '..', '..')
34
+
35
+ // ---------- CLI ----------
36
+ const argv = process.argv.slice(2)
37
+ const argOf = (n) => (argv.includes(n) ? argv[argv.indexOf(n) + 1] : undefined)
38
+ const hasFlag = (n) => argv.includes(n)
39
+ const asJson = hasFlag('--json')
40
+ const updateBaseline = hasFlag('--update-baseline')
41
+ const only = argOf('--only')
42
+
43
+ // ---------- 金标注册表 ----------
44
+ // **用例数据不在本脚本内**(skills 内禁止硬编码业务 UI 产物路径):
45
+ // 注册表从 training/pass1/cases.json 读取(与 baseline 同属仓库训练数据区),
46
+ // 条目形状:{ regId, spec(相对仓库根), card(spec.cards 下标,0 起) }。
47
+ // 可用 --cases <path> 指向其他注册表(如专项回归集)。
48
+ const CASES_PATH_DEFAULT = 'training/pass1/cases.json'
49
+ function loadCases() {
50
+ const p = path.resolve(REPO, argOf('--cases') || CASES_PATH_DEFAULT)
51
+ if (!fs.existsSync(p)) {
52
+ console.error(`[error] 金标注册表不存在:${p}\n 请在仓库 training/pass1/cases.json 登记金标(形状:[{"regId","spec","card"}]),skill 脚本不持有业务路径。`)
53
+ process.exit(2)
54
+ }
55
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
56
+ }
57
+ const CASES = loadCases()
58
+
59
+ if (argv.includes('--help') || argv.includes('-h')) {
60
+ console.error('Usage: node scripts/v3_regression.mjs [--update-baseline] [--only <regId>] [--json] [--cases <path>]')
61
+ process.exit(2)
62
+ }
63
+
64
+ const workRoot = path.join(REPO, '.cache', 'v3-regression')
65
+ const workCards = path.join(workRoot, 'src', 'cards')
66
+ const baselineDir = path.join(REPO, 'training', 'pass1', 'baseline')
67
+ const node = process.execPath
68
+ // 同 gate.mjs:改为统一解析(此前 `process.env.PYTHON || 'python3'` 不认 DSL_CARD_PYTHON、不探测)
69
+ const py = resolvePython()
70
+
71
+ fs.rmSync(workRoot, { recursive: true, force: true })
72
+ fs.mkdirSync(workCards, { recursive: true })
73
+ fs.mkdirSync(baselineDir, { recursive: true })
74
+
75
+ /** 跑一条命令,返回 {ok, code, output}。 */
76
+ function run(cmd, args, env = {}) {
77
+ const res = spawnSync(cmd, args, {
78
+ cwd: REPO,
79
+ encoding: 'utf8',
80
+ timeout: 180_000,
81
+ env: { ...process.env, PYTHONIOENCODING: 'utf-8', ...env },
82
+ })
83
+ const out = `${res.stdout || ''}${res.stderr || ''}`.trim()
84
+ return { ok: res.status === 0, code: res.status, output: out }
85
+ }
86
+
87
+ /** JSON 深比较(语义等价,忽略键序/格式)。 */
88
+ function jsonEqual(a, b) {
89
+ return JSON.stringify(a) === JSON.stringify(b)
90
+ }
91
+
92
+ const results = []
93
+ let cases = only ? CASES.filter((c) => c.regId === only) : CASES
94
+ if (!cases.length) {
95
+ console.error(`[error] --only 未命中任何金标:${CASES.map((c) => c.regId).join(', ')}`)
96
+ process.exit(2)
97
+ }
98
+
99
+ for (const c of cases) {
100
+ const specPath = path.join(REPO, c.spec)
101
+ if (!fs.existsSync(specPath)) {
102
+ results.push({ regId: c.regId, pass: false, reason: `spec 不存在:${c.spec}` })
103
+ continue
104
+ }
105
+ // 1) 首轮生成(无 decisions)
106
+ const gen = run(node, [
107
+ path.join(SCRIPTS, 'spec_to_schema_v3.mjs'),
108
+ '--spec', specPath,
109
+ '--id', c.regId,
110
+ '--cardIndex', String(c.card),
111
+ '--out', workCards,
112
+ '--force',
113
+ ])
114
+ if (!gen.ok) {
115
+ results.push({ regId: c.regId, pass: false, reason: 'generate 失败', detail: gen.output.slice(-600) })
116
+ continue
117
+ }
118
+ // 2) 门禁(临时工程根,避免触碰 src/cards 真卡)
119
+ const gate = run(node, [path.join(SCRIPTS, 'gate.mjs'), '--id', c.regId], {
120
+ DSL_CARD_PROJECT_ROOT: workRoot,
121
+ })
122
+ // 3) 基线漂移
123
+ const schemaPath = path.join(workCards, c.regId, 'schema.json')
124
+ const baselinePath = path.join(baselineDir, `${c.regId}.schema.json`)
125
+ let drift = 'none'
126
+ let baselineOk = true
127
+ if (fs.existsSync(baselinePath)) {
128
+ const cur = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
129
+ const base = JSON.parse(fs.readFileSync(baselinePath, 'utf8'))
130
+ // 归一:页 id/名称里含 regId(基线与当前一致),直接深比较即可
131
+ baselineOk = jsonEqual(cur, base)
132
+ if (!baselineOk) drift = 'schema-drift'
133
+ } else {
134
+ drift = 'no-baseline'
135
+ }
136
+ if (updateBaseline && fs.existsSync(schemaPath)) {
137
+ fs.copyFileSync(schemaPath, baselinePath)
138
+ drift = 'baseline-updated'
139
+ }
140
+ results.push({
141
+ regId: c.regId,
142
+ pass: gate.ok && baselineOk,
143
+ gateOk: gate.ok,
144
+ gateFailed: gate.ok ? [] : parseFailedSteps(gate.output),
145
+ drift,
146
+ baselinePath: path.relative(REPO, baselinePath),
147
+ detail: gate.ok ? undefined : gate.output.slice(-800),
148
+ })
149
+ }
150
+
151
+ function parseFailedSteps(gateOut) {
152
+ const m = gateOut.match(/\[FAIL\] (\d+)\/\d+ 项未过:(.+)/)
153
+ return m ? m[2].split(', ').map((s) => s.trim()) : ['gate']
154
+ }
155
+
156
+ const passed = results.filter((r) => r.pass).length
157
+ const passRate = results.length ? passed / results.length : 0
158
+ const allOk = passed === results.length
159
+
160
+ const report = {
161
+ generatedAt: new Date().toISOString(),
162
+ cases: results.length,
163
+ passed,
164
+ passAt1: Number(passRate.toFixed(4)),
165
+ ok: allOk,
166
+ results,
167
+ }
168
+ fs.writeFileSync(path.join(REPO, 'training', 'pass1', 'last-report.json'), JSON.stringify(report, null, 2) + '\n')
169
+
170
+ if (asJson) {
171
+ console.log(JSON.stringify(report, null, 2))
172
+ } else {
173
+ console.log('\n=== v3 Pass@1 回归 ===')
174
+ for (const r of results) {
175
+ const mark = r.pass ? '✓' : '✗'
176
+ const extra = r.drift && r.drift !== 'none' ? ` [${r.drift}]` : ''
177
+ console.log(`${mark} ${r.regId}${extra}${r.pass ? '' : ' → ' + (r.reason || 'gate: ' + (r.gateFailed || []).join(','))}`)
178
+ }
179
+ console.log(`\nPass@1 = ${passed}/${results.length} = ${(passRate * 100).toFixed(1)}%`)
180
+ if (!allOk) {
181
+ console.log('处置:漂移 = v3 改动改变了金标卡产物 → 逐卡核对;确属预期改进才 --update-baseline 并随改动提交基线。gate 失败 → 按 lint/validate 规则号修复。')
182
+ }
183
+ if (updateBaseline) console.log('[baseline] 已用当前产物刷新基线快照(记得随 v3 改动一起提交)。')
184
+ }
185
+
186
+ // 清理工作目录(保留基线)
187
+ fs.rmSync(workRoot, { recursive: true, force: true })
188
+ process.exit(allOk ? 0 : 1)
@@ -377,7 +377,8 @@ def check_style_props(style, cn, loc, errors, warnings, style_ids=None):
377
377
  f"'{cv}'. Use one of {sorted(FLEX_ENUMS[ck])}")
378
378
 
379
379
 
380
- def check_components(components, errors, warnings, depth=0, style_ids=None, seen_ids=None, stats=None):
380
+ def check_components(components, errors, warnings, depth=0, style_ids=None, seen_ids=None, stats=None,
381
+ sibling_index=None):
381
382
  if style_ids is None:
382
383
  style_ids, _ = load_meta()
383
384
  load_style_maps()
@@ -389,6 +390,10 @@ def check_components(components, errors, warnings, depth=0, style_ids=None, seen
389
390
  warnings.append("Component tree depth exceeds 30, possible infinite nesting?")
390
391
  return
391
392
  for i, comp in enumerate(components):
393
+ # 递归下钻时按「单个子节点」传列表,enumerate 的 i 恒为 0;sibling_index 携带
394
+ # 真实兄弟下标,保证 "depth N[i]" 可区分同层兄弟(定位仍以 #componentId 为准)。
395
+ if sibling_index is not None:
396
+ i = sibling_index
392
397
  if not isinstance(comp, dict):
393
398
  errors.append(f"Component at depth {depth}[{i}] is not an object")
394
399
  continue
@@ -430,26 +435,29 @@ def check_components(components, errors, warnings, depth=0, style_ids=None, seen
430
435
  errors.append(f"Property '{pk}' in '{cn}' at depth {depth}[{i}] has itemValueFrom="
431
436
  f"'{prop.get('itemValueFrom')}' but no itemDefaultValue")
432
437
 
438
+ # Slots / children —— 检查须在下方 tag_messages 之前 append,"has no slots" /
439
+ # "has child components" 才能吃到本组件的 #cid 定位标记(此前检查在 tag 之后,
440
+ # 消息漏挂标记,只剩裸 depth[i])
441
+ slots = comp.get("slots", [])
442
+ if cn in CONTAINER_TYPES or cn == "Select":
443
+ if not slots:
444
+ warnings.append(f"Container '{cn}' at depth {depth}[{i}] has no slots")
445
+ else:
446
+ for slot in slots:
447
+ if slot.get("components"):
448
+ warnings.append(f"Leaf component '{cn}' at depth {depth}[{i}] has child components")
449
+
433
450
  # 到这里为止都是「本组件自身」的问题,统一挂上定位标记;子组件的消息由下面递归时
434
451
  # 各自挂自己的标记,不能被父级污染(否则点一下会选到父节点)
435
452
  tag_messages(errors, err_mark, cid_tag(cid))
436
453
  tag_messages(warnings, warn_mark, cid_tag(cid))
437
454
 
438
- # Slots / children
455
+ # 子组件下钻:enumerate 携带真实兄弟下标(sibling_index),否则嵌套层 i 恒为 0
439
456
  if cn in CONTAINER_TYPES or cn == "Select":
440
- slots = comp.get("slots", [])
441
- if not slots:
442
- warnings.append(f"Container '{cn}' at depth {depth}[{i}] has no slots")
443
- else:
444
- for slot in slots:
445
- for child in slot.get("components", []):
446
- check_components([child], errors, warnings, depth + 1, style_ids, seen_ids)
447
- else:
448
- slots = comp.get("slots", [])
449
- if slots:
450
- for slot in slots:
451
- if slot.get("components"):
452
- warnings.append(f"Leaf component '{cn}' at depth {depth}[{i}] has child components")
457
+ for slot in slots:
458
+ for idx, child in enumerate(slot.get("components", [])):
459
+ check_components([child], errors, warnings, depth + 1, style_ids, seen_ids,
460
+ stats=stats, sibling_index=idx)
453
461
 
454
462
 
455
463
  def check_data_json(schema, path, warnings):
@@ -0,0 +1,79 @@
1
+ # 脚本退出码语义(单一权威)
2
+
3
+ > 适用范围:`skills/dsl-card-generator/scripts/` 与 `skills/sketch-spec-to-card/scripts/` 下的
4
+ > 全部脚本(`.mjs` / `.py`)。**本文件是退出码语义的唯一权威**,其它文档只引用、不复述。
5
+ >
6
+ > 服务端记账表在 `server/core/ai-chat.js` 的 `SKILL_EXIT_SEMANTICS`;**本文件是它的来源**,
7
+ > 改动顺序永远是「先改本文件 → 再改记账表 → 跑 `tools/tests/exit-code-contract.test.mjs`」。
8
+
9
+ ## 0. 两条总则
10
+
11
+ 1. **0 = 成功,1 = 未特别声明时的通用失败**。只声明这两条是不够的 —— 见总则 2。
12
+ 2. **退出码语义按脚本查表,不跨脚本外推**。同一个数字在不同脚本里含义可以不同:
13
+ `2` 在 `iterate.mjs` 是「门禁未达标」,在 `gate.mjs` / `ir_merge.mjs` 是「用法错误」。
14
+ **永远按 (脚本名, code) 二元组查表,不要只看数字。**
15
+
16
+ 为什么这条总则值钱:模型(与服务端)天然会把**一切非 0 读成「工具坏了」**,于是去修一个
17
+ 不存在的问题、或原样重试。本链路里大量非 0 其实是**脚本正常跑完、只是结论是负面的**。
18
+
19
+ ## 1. 语义分类(kind)
20
+
21
+ | kind | 含义 | 模型该做什么 | 计入「连续失败」短路 |
22
+ |---|---|---|---|
23
+ | `diagnostic` | 正常跑完,产出就是差异/偏差清单 | 按报告逐项修 | **否** |
24
+ | `gate-fail` | 正常跑完,门禁未达标 | 按 `gate.failed` 逐项改,**不要原样重试** | 是 |
25
+ | `not-ready` | 正常跑完,输入内容尚未就绪 | 补内容后重跑(不是脚本故障) | 是 |
26
+ | `needs-user` | 正常跑完,**必须由用户做选择** | 把清单交给用户,停下等回复 | 是 |
27
+ | `fail` | 真失败(脚本报错 / 用法错误 / IO 失败) | 修调用或修脚本 | 是 |
28
+
29
+ 「计入连续失败」的判定在 `ai-chat.js:1917 skillExecFailure()`:**只有 `diagnostic` 豁免**
30
+ (连 `gate-fail` 都计入 —— 连续三次门禁不过且报错没变,短路提示比再撞一次墙更有用;
31
+ `--ir` 文件变了会换 key,所以「模型确实在改」的连续失败不会被误短路)。
32
+
33
+ ## 2. 必须登记的「非 fail」退出码
34
+
35
+ 下表每一行都会在服务端生成 `exit=N [kind:note]` 首行标签。**漏登记 = 被标成 `fail`**,
36
+ 模型会把「结论负面」当成「工具故障」。
37
+
38
+ | 脚本 | code | kind | 语义 |
39
+ |---|---|---|---|
40
+ | `readiness.mjs` | 3 | not-ready | 内容未就绪(要素 / 变量绑定 / 图片地址 / 切图落盘四类缺口),**不是脚本报错**,按报告逐项补内容后重跑 |
41
+ | `route_input.mjs` | 3 | needs-user | 输入形态判为 `unknown` —— 先问用户这是哪类输入,**不要猜、不要自己挑** |
42
+ | `parse_meaxure.py` | 4 | needs-user | 多画板选择门禁(`rules.md §十三`):脚本故意打印 1..n + 全部清单后退出,**Agent 必须把清单交给用户、停下等回复**,再用 `--pick` / `--scope-all` 重跑 |
43
+ | `iterate.mjs` | 2 | gate-fail | 门禁未达标,报告照常回喂 —— 按 `gate.failed` 逐项改 |
44
+ | `gate.mjs` | 1 | gate-fail | 门禁未过(`autofix → validate → lint → preflight` 任一步 FAIL),按规则号逐条修后重跑 |
45
+ | `preflight.mjs` | 1 | diagnostic | 正常结果:列出若干 Pre-flight 问题,按 code 逐条修 |
46
+ | `check_fidelity.mjs` | 1 | diagnostic | 正常结果:报出文本缺失 / 字号 / 颜色偏差,逐项修 |
47
+ | `lint_card.mjs` | 1 | diagnostic | 正常结果:有 lint 问题,按规则号逐项修 |
48
+ | `diff_bands.mjs` | 1 | diagnostic | 正常结果:渲染与设计的逐带差异非空 |
49
+ | `check_magic_values.mjs` | 70 | diagnostic | 正常结果:发现魔法值,改用 `lib/layout-tokens.mjs` 的 `T.*`(`70` 是历史取值,勿按数字猜语义) |
50
+ | `render_baseline.mjs` | 70 | diagnostic | 同 `check_magic_values`,历史取值 |
51
+
52
+ ## 3. 其余常见退出码(按 `fail` 处理,无需单独登记)
53
+
54
+ | 脚本 | code | 语义 |
55
+ |---|---|---|
56
+ | `expand_ir.mjs` | 1 | `validate` 未通过(报错文本写了具体项) |
57
+ | `expand_ir.mjs` | 3 | IR 含非法字段(报错已给合法值全集),改键名即可,**不要重写整份 IR** |
58
+ | `ir_restore.mjs` | 1 / 3 | 参数或文件不对 / 快照不是合法 JSON |
59
+ | `extract_subtree.mjs` | 3 | 写文件失败(IO) |
60
+ | `gate.mjs` / `ir_merge.mjs` / `describe_card.mjs` / `diff_card.mjs` / `apply_opacity_pattern.mjs` | 2 | 用法 / 参数错误 |
61
+ | `readiness.mjs` / `route_input.mjs` / `ir_merge.mjs` | 1 | 用法 / 参数错误 |
62
+
63
+ ## 4. 为什么 `iterate.mjs` 的「门禁未达标」是 `2` 而不是 `5`
64
+
65
+ 2026-09-24 的链路审计报告曾建议「统一为 `5 = 门禁未收敛`」。**评估后不采纳**,理由:
66
+
67
+ 1. **已被生产链路消费**:`SKILL_EXIT_SEMANTICS['iterate.mjs'][2]` 已是 `gate-fail`,
68
+ 语义标签与"是否计入连续失败"都按它工作;改数字是纯粹的破坏性变更,收益为零。
69
+ 2. **服务端按 (脚本, code) 查表**,不按数字跨脚本解释 —— 总则 2 已经解决了"同一个 2 两个含义"
70
+ 的问题,不需要靠换数字来解决。
71
+ 3. 报告的真实目标(「避免把收敛失败/未就绪记成工具故障」)**已由 kind 机制达成**;
72
+ 本轮做的是把它补全(§2 表里 `readiness` / `route_input` / `parse_meaxure` 三行此前缺失)。
73
+
74
+ ## 5. 新增脚本时的清单
75
+
76
+ 1. 决定了非 0 语义 → **先加进本文件 §2**(kind 四选一)。
77
+ 2. 同步加进 `server/core/ai-chat.js` 的 `SKILL_EXIT_SEMANTICS`。
78
+ 3. `npm test` 跑 `tools/tests/exit-code-contract.test.mjs`,它会断言「本文件 §2 每行都在记账表里」。
79
+ 4. 改完 `ai-chat.js` **必须重启 `npm run dev`**(后端无热重载,旧进程会继续跑旧代码)。
@@ -0,0 +1,118 @@
1
+ # spec.json 契约(sketch-spec-to-card ↔ dsl-card-generator)
2
+
3
+ > **单一权威**:本文件是 `UI/<画板>/spec.json` 的唯一契约定义。`references/spec-schema.md`(上游)只描述字段来源与判定逻辑,**字段的增删改以本文件为准**。
4
+ >
5
+ > - **生产者**:`skills/sketch-spec-to-card`(唯一写出方 = `scripts/parse_meaxure.py`;视觉判定出自 `vision.py`,列表/意图出自 `build_prompt.py`)
6
+ > - **消费者**:`skills/dsl-card-generator`(生产建卡路径)、`skills/card-skill-trainer`(训练/回归)、`skills/card-doctor`
7
+ > - **机械校验**:`tools/tests/spec-contract.test.mjs` —— 从 `UI/**/spec.json` 聚合实际字段路径,未在本文件登记的路径会让测试失败。**改产物字段必须同步改本文件**。
8
+
9
+ ## 状态口径
10
+
11
+ | 状态 | 含义 | 改动要求 |
12
+ |---|---|---|
13
+ | `active` | 两端或多端在用,**是接口** | 破坏性变更需同步改所有消费者 |
14
+ | `upstream-only` | 只有上游(sketch skill)自用;下游 0 读点 | 下游可无视;改动不通知下游 |
15
+ | `write-only` | **全仓无读点**(只写不读) | 删除候选。删之前先按 §「消重纪律」跑一轮全量确认无外部消费(如训练脚本、外部接入方) |
16
+ | `removed` | 上游已停写(2026-09-24 起) | **禁止在新代码读它**。存量旧产物仍带此字段(无害,不必重跑);登记行保留是为了门禁对存量产物放行 |
17
+
18
+ ## 顶层字段
19
+
20
+ | 字段路径 | 类型 | 生产者 | 消费者 | 状态 | 缺失 / 为 null 时的口径 |
21
+ |---|---|---|---|---|---|
22
+ | `canvas` | string | parse_meaxure | build_prompt、generate_cards、v3_assemble、build_dataset | active | 画板标识,同时是 `UI/<canvas>/` 目录名;同名画板第 2 个起追加 `-2`/`-3`(见 `spec_tree.mjs` 展示名取 `spec.canvas`);不可为空 |
23
+ | `schemaVersion` | string | parse_meaxure | **无** | removed | **已删(2026-09-24 统计定案)**:spec 产物恒 `"1.0"` 零信息量、静态读点 0 + 金标全链路运行时访问 0 → 上游已停写。存量旧产物仍带此字段,无害,不必重跑。注意 manifest.json 的 schemaVersion 是另一处(manifest 自描述),保留 |
24
+ | `size` | {width,height} | parse_meaxure | v3_extract、v3_assemble、spec_tree、iterate | active | 下游回落 `{width:375,height:812}` 并告警 |
25
+ | `cards` | array | vision.detect_cards → parse_meaxure | v3_extract、spec_tree、generate_cards、trainer | active | `[]` = 本画板无卡(`text_only` / 全弹层);下游必须跳过而非报错 |
26
+ | `rejectedCards` | array | vision.detect_cards | build_dataset(训练集构造) | upstream-only | 缺失 = 无拒绝记录;训练侧据此做负样本分析 |
27
+ | `conversationFlow` | object\|null | vision.detect_conversation_flow | **无** | write-only | **保留(2026-09-24 统计)**:96.6% 写入且 10 种真实值(inputDock 几何/bubbleCount/stackedBubblePairs,非对话卡 null)——上游花算力提取的结构化信息,值有信息量;会话流判定当前由 `artboardType=text_only` + `rejectedCards` 承载,此字段是未来对话卡生成的原料。勿与 text_only 判定混谈 |
28
+ | `focusSkipReason` | string | parse_meaxure | build_dataset | upstream-only | 非空 = **跳过了常规候选检测**(含原因)。⚠ **不是「0 卡」的同义词**:`bottom_sheet_geometry` 与 `cards` 非空可同时成立(底部抽屉由面板几何直取,不走候选检测) |
29
+ | `elements` | array | parse_meaxure | v3_extract、spec_tree、trainer | active | **必需**;全画板展平图层(含系统区/背景) |
30
+ | `styleSummary` | object | parse_meaxure | build_prompt | upstream-only | 下游不读:**样式以 `elements[].style` 为准**,禁止从摘要反推单元素样式 |
31
+ | `styleSummary.*` | {colors,gradients,fonts,borderRadius,shadows} | parse_meaxure | build_prompt | upstream-only | 五个子键均为聚合摘要,不进下游 |
32
+ | `artboardType` | string | parse_meaxure.classify_artboard | generate_cards、v3_extract、spec_tree、iterate | active | 取值 `card`/`overlay_dialog`/`subcard`/`text_only`;缺失回落 `card` |
33
+ | `interaction` | string | parse_meaxure | build_prompt、cluster_states | upstream-only | `card` 时为空串 |
34
+ | `jsapiInteractions` | array | parse_meaxure | build_prompt、cluster_states | upstream-only | 缺失 = `[]` |
35
+ | `annotationDefects` | array | parse_meaxure | card-skill-trainer(diagnose_case / plan_next_round / scoring) | active(训练侧) | 缺失 = 视为无标注缺陷 |
36
+ | `slices` | array&lt;string&gt; | parse_meaxure | v3_assemble、v3_extract | active | 本画板 `slices/` 下的相对路径(posix);空数组 = 无切图 |
37
+ | `skippedReason` | string | parse_meaxure | build_prompt | upstream-only | 仅 `cards=[]` 时存在 |
38
+
39
+ ## `cards` 空 / 非空的合法性(按 `artboardType` 分流)
40
+
41
+ `cards` 空与非空**都不代表对错** —— 上游按 `artboardType` 收口,每类的产出与建卡路径如下。下游遇到空 `cards` 时按类型分流,**不要**判成「上游自相矛盾」,也不要在下游补卡:
42
+
43
+ | artboardType | 含义 | `cards[]` | `card-N.png` | 建卡路径(Workflow A.5 口径) | 证据 |
44
+ |---|---|---|---|---|---|
45
+ | text_only | 纯文本画板(对话 / 问答 / 提示),整板无卡 | `[]` | 不产 | **不建卡**(上游按类型收口,勿人工绕过) | `classify_artboard`;`test_text_only_artboard.py` 用例 1 |
46
+ | overlay_dialog | 系统弹框 | `[]` | 视觉 scrim 判定类不产;名称兜底判定类产 fallback `card-1.png` | **不建卡** —— 由父卡 alertDialog 调用承载 | 用例 5 |
47
+ | subcard | 视觉 bottom-sheet 子卡(底部抽屉) | **1 张**(面板几何直取,**不走候选检测**) | 产 `card-1.png`(面板 rect + 顶圆角遮罩) | **建独立 Dialog 卡** —— 与普通卡同构:`rect` / `mask.radius` / 卡内 `elements` / `segments` 齐备 | `detect_artboard_cards` sheet 分支;`vision._bottom_sheet_rect`;用例 4 |
48
+ | card | 0 卡 · 结构丰富:卡片被整体导成一张切片(`vision._candidate_visuals` 跳过 `is_slice`,故检不出) | `[]` | 产 fallback `card-1.png`(去系统 chrome) | 建卡:以该兜底图 + `elements[]` 为准(**有图无坐标**) | `parse_meaxure.py:645`;`board_tag`(`:1768`);用例 2/3/6 |
49
+
50
+ > 注:本表首列**刻意不加反引号** —— `spec-contract.test.mjs` 以「首列反引号 + 整行 6 列」识别字段契约表,加反引号会让本表被误当成字段登记。
51
+
52
+ > **`subcard` 产卡(2026-09-24 修)**:此前 `bottom_sheet` 只写 `focusSkipReason` 不产 `cards`,导致 `cluster_states` 承认它是独立逻辑卡(`pageId=card-N`、`isDialog=true`)而 spec 层给不出任何卡级数据。现由 `detect_artboard_cards` 的 sheet 分支把面板几何(`vision._bottom_sheet_rect` 早已算出 rect + 顶圆角)落成 1 张卡;`focusSkipReason` **保持不变**(`classify_artboard` 靠它判 `subcard`,改动会连带打翻 `is_text_only_artboard` / `board_verdict` / trainer 的 `is_popup_case`)。**旧版存量 spec 仍可能是 `subcard` + `cards=[]` → 重跑上游,不要在下游补卡。**
53
+
54
+ > `readiness.mjs` 门 **F**:`cards=[]` 的三类(text_only / overlay_dialog / card)只在 `checks.spec.notes` 记说明,**均不进 `blockers`**;`cards` 非空时只校验 `rect` / `mask` 是否缺失(产卡与否由 Workflow A.5 决定,不是契约缺失)。
55
+ > 门 F 真正拦的只有:`cards` 非空时 `cards[i].rect` / `cards[i].mask` 缺失(旧版产物)、`--card` 序号越界、spec 解析失败。
56
+
57
+ ## `cards[]`
58
+
59
+ | 字段路径 | 类型 | 生产者 | 消费者 | 状态 | 缺失 / 为 null 时的口径 |
60
+ |---|---|---|---|---|---|
61
+ | `cards[].index` | int | parse_meaxure | v3_extract(定位 `card-N.png`)、trainer | active | **1 起**;与 `card-N.png` 的 N 严格同源 |
62
+ | `cards[].rect` | {x,y,width,height} | vision.detect_cards | v3_extract、v3_assemble、spec_tree | active | **`cards` 非空时必需**;画板坐标系设计 px |
63
+ | `cards[].image` | string | parse_meaxure | v3_assemble、render_card、迭代对比 | active | 裁切失败可缺失 → 下游按 `cards[].index` 自行拼 `card-N.png` 并容忍不存在 |
64
+ | `cards[].cardFill` | string\|null | parse_meaxure | v3_extract、v3_assemble | active | `null`(无纯色填充,如渐变/图片底)→ 下游用组件默认底色,**不得从截图像素反推** |
65
+ | `cards[].mask` | object | parse_meaxure(`:584`,**无条件产出**) | spec_tree、v3_extract | active | **必需**。存量 spec 缺失 `mask` 是**旧版产物**(在 `focusMetrics`/`mask`/`cardFill` 批次字段引入前生成)→ 重跑上游 `parse_meaxure.py --build` 即补全;`readiness.mjs` 的 spec 契约检查会拦 |
66
+ | `cards[].mask.type` | `"geometry"` | parse_meaxure | spec_tree(诊断打印) | upstream-only | 固定值;遮罩完全来自图层几何(rect + borderRadius),**不依赖背景颜色** |
67
+ | `cards[].mask.radius` | [tl,tr,br,bl]\|null | parse_meaxure `_card_mask_info` | **v3_extract → v3_assemble** | active | **`null` = 上游没找到带圆角的卡壳**(`_card_mask_info` 返回 `(None, fill)`)。下游 `v3_assemble.mjs:366` 取 `T.radius('card')` 默认圆角 —— **这是契约规定的兜底,不是 bug**。取用时用 `??` 而非 `\|\|`,否则 `radius[0]==0`(真·直角卡)会被误判成"无圆角" |
68
+ | `cards[].mask.shapeCount` | int | parse_meaxure | **无** | write-only | **保留(2026-09-24 统计)**:92% 写入、3 种值(1/2/4,mask 复杂度有变化)。无代码读点,但删除收益为零、有校验价值 |
69
+ | `cards[].focusMetrics` | object | vision.card_focus_metrics | card-skill-trainer | upstream-only | 缺失 = 该卡未参与焦点评分 |
70
+ | `cards[].focusMetrics.*` | {areaRatio,aspectRatio,heightRatio,textCount,richComponentCount,sliceCount,symbolCount,shapeCount} | vision.card_focus_metrics | card-skill-trainer | upstream-only | 训练侧打分用 |
71
+ | `cards[].segments` | array | parse_meaxure.detect_segments | **v3_assemble(真消费)**、build_prompt | active | 空数组 = 未分段;**下游不得自算几何兜底**(上游已是权威,重复判定会给出两个答案) |
72
+ | `cards[].segments[].y` | float | parse_meaxure | v3_assemble | active | 卡内相对坐标 |
73
+ | `cards[].segments[].height` | float | parse_meaxure | v3_assemble | active | 卡内相对坐标 |
74
+ | `cards[].segments[].hint` | string | parse_meaxure | build_prompt | upstream-only | 分段语义提示 |
75
+ | `cards[].segments[].items` | array | parse_meaxure | build_prompt | upstream-only | 段内元素引用 |
76
+ | `cards[].segments[].gap_top` | float | parse_meaxure | **无** | removed | **已删(2026-09-24 定案)**:纯冗余推导值——首段 `gap_top === y` 完全重复,后续段 = 相邻 `y/height` 之差,可无损推导;读点 0 + 金标运行时访问 0 → 上游已停写。间距看 `segments[].y`。存量旧产物仍带此字段,无害。(勘误:初版统计「0/609 写入」是扫描脚本 path 漏 `cards[]` 前缀的误报,真实为 48% 写入) |
77
+ | `cards[].detectedList` | object\|absent | build_prompt | build_prompt | upstream-only | 仅用于生成 prompt 文本,**不进 spec 下游链路**;v3 自行重判列表 |
78
+ | `cards[].flexLayout` | object\|absent | vision.infer_flex_layout | build_prompt、spec_tree(诊断打印) | upstream-only | **不进生成**:flex 方向由 AI 视觉主判 / IR 层决定 |
79
+ | `cards[].cardFillSampled` | string | parse_meaxure | **无** | write-only | **保留(2026-09-24 统计)**:92.3% 写入、4 种色值——是 `cardFillConflict` 的对照基线,删了 conflict 标记就失去参照 |
80
+ | `cards[].cardFillConflict` | true | parse_meaxure | **读 spec 的人/模型**(代码无读点;写入同时打 stderr) | write-only | **保留(2026-09-24 定案)**:30.8% 卡写(恒 true,仅在冲突时写)。它的消费者是**读 spec 的智能体**而非代码——警示「fill 标注不可信,按视觉裁决」;删掉会让冲突静默 |
81
+ | `cards[].trainingEligible` | bool | parse_meaxure | build_dataset、spec_tree、run_round | upstream-only | `false` = 不进训练集 |
82
+ | `cards[].elements` | array | parse_meaxure | v3_extract | active | 归属该卡的图层;结构与顶层 `elements[]` 同构 |
83
+
84
+ ## `elements[]`
85
+
86
+ | 字段路径 | 类型 | 生产者 | 消费者 | 状态 | 缺失 / 为 null 时的口径 |
87
+ |---|---|---|---|---|---|
88
+ | `elements[].name` | string | parse_meaxure | spec_tree、v3_extract | active | 原始 Sketch 层名;仅附属信息,**不参与保留/剔除判定** |
89
+ | `elements[].type` | string | parse_meaxure | v3_extract、spec_tree | active | `symbol`/`shape`/`group`/`slice`/`text` |
90
+ | `elements[].rect` | {x,y,width,height} | parse_meaxure | v3_extract、v3_assemble、spec_tree | active | 画板坐标系设计 px |
91
+ | `elements[].style` | object | parse_meaxure | v3_extract、v3_assemble | active | 平台对齐样式参考(`fill`/`border`/`radius`/`shadow`/`opacity`/`gradient`…),**样式值只从这里读** |
92
+ | `elements[].text` | string\|null | parse_meaxure | build_prompt、v3_extract | active | 仅 text 层;附属记录,不参与判定 |
93
+ | `elements[].textStyle` | object\|null | parse_meaxure | v3_extract | active | 仅 text 层(`fontSize`/`fontFace`/`color`/`textAlign`/`lineHeight`);非 text 层为 `null` |
94
+ | `elements[].slices` | string\|null | parse_meaxure | v3_assemble | active | 仅 slice 层:原始层名(切图匹配溯源);非 slice 层为 `null` |
95
+ | `elements[].annotationDefect` | object\|absent | parse_meaxure(`:1200` / `:1264`) | v3_extract、v3_assemble、v3_shared、card-skill-trainer | active | **仅当该元素的标注有缺陷时才存在**。下游据此决定切图补偿方式;**缺失 ≠ 有缺陷,而是「标注完整」**,所以判定必须用 `el.annotationDefect?.type` 而非存在性反推 |
96
+ | `elements[].annotationDefect.*` | {type,name,reason,approximateSolidColor,resolution,cropSlice} | parse_meaxure | 同上 | active | `type`(如 `missing-slice`)是判定键;`cropSlice` 是「从画板裁切补图」的产物路径 —— 为 `null` 时 `v3_shared.mjs:381` 判定无法补偿,切图会悬空 |
97
+
98
+ ## 消重纪律(v3 与上游的判定边界)
99
+
100
+ 上游已经算过的事,下游**不得自算第二遍**(同一张卡出现两个答案时无裁决机制)。当前已知的越界自算(清理中,需先埋点统计 fallback 触发率,< 5% 才删):
101
+
102
+ | 能力 | 权威(上游) | 下游越界自算 |
103
+ |---|---|---|
104
+ | 分段 segments | `parse_meaxure.detect_segments`(`:294`) | `v3_extract.mjs:277-356, 2000-2068` 几何兜底 |
105
+ | 列表检测 | `build_prompt.detect_list_in_card` → `cards[].detectedList` | `v3_extract.mjs:838-856, 2403-2472` 自行重判(且不读 `detectedList`) |
106
+ | 卡底颜色 | `cards[].cardFill` | `v3_assemble.mjs:5303-5314` 对设计稿二次像素校验 |
107
+ | 圆角 | `cards[].mask.radius` | 自算 `radiusMax`(`v3_shared.mjs:276`) |
108
+
109
+ **已正确划界(下游只消费不重做)**:keyboard / overlay_dialog / 聊天气泡 / 系统 chrome / `annotationDefects` / `artboardType`。
110
+
111
+ ## 存量产物漂移
112
+
113
+ `UI/` 下的 spec.json 是历史产物,与当前代码产出的字段集**可能不一致**:
114
+
115
+ - 缺 `mask` / `focusMetrics` / `cardFill` / `cardFillSampled` / `trainingEligible` 的 = **旧版产物**(上述批次字段引入前生成)。
116
+ - 判定口径:字段出现次数 < `cards[]` 总数即存在旧产物。
117
+ - 处置:**重跑上游**(`parse_meaxure.py --src <导出> --draft <稿名> --build`)而非手工补字段。
118
+ - 不要为了兼容旧产物在下游写 `if (!card.mask)` 分支 —— 契约规定 `mask` 必需,兼容分支会让"上游漏产"永远不被发现。