cardbuddy 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
  2. package/dist/preview-editor/assets/{index-CIIfMlwI.js → index-Bro07DTa.js} +91 -88
  3. package/dist/preview-editor/index.html +2 -2
  4. package/dist/vendored/card-editor/runtime/adapter.js +2 -2
  5. package/dist/vendored/mcp/client.mjs +1 -1
  6. package/dist/vendored/server/core/ai-chat.js +251 -80
  7. package/dist/vendored/server/core/cardValidate.js +16 -5
  8. package/dist/vendored/skills/card-skill-trainer/SKILL.md +61 -6
  9. package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
  10. package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
  11. package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
  12. package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
  13. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
  14. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
  15. package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
  16. package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
  17. package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
  18. package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
  19. package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
  20. package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
  21. package/dist/vendored/skills/dsl-card-generator/SKILL.md +93 -27
  22. package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
  23. package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
  24. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +169 -44
  25. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +87 -3
  26. package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +35 -6
  27. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +28 -27
  28. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +30 -8
  29. package/dist/vendored/skills/dsl-card-generator/references/rules.md +3 -2
  30. package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +5 -2
  31. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +2 -3
  32. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +1 -4
  33. package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
  34. package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +88 -0
  35. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +143 -42
  36. package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
  37. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +99 -53
  38. package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +5 -2
  39. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +388 -29
  40. package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
  41. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
  42. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
  43. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
  44. package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
  45. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
  46. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
  47. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
  48. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
  49. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
  50. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
  51. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
  52. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
  53. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +325 -41
  54. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +112 -6
  55. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +0 -6
  56. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +119 -78
  57. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +48 -50
  58. package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
  59. package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
  60. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +5 -10
  61. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +122 -8
  62. package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +3 -1
  63. package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
  64. package/dist/vendored/skills/shared/exit-codes.md +79 -0
  65. package/dist/vendored/skills/shared/spec-contract.md +118 -0
  66. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +26 -9
  67. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +3 -3
  68. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +0 -8
  69. package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
  70. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +91 -47
  71. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +0 -5
  72. package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +75 -15
  73. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +40 -34
  74. package/package.json +1 -1
  75. package/dist/preview-editor/assets/index-DQn0zQBP.css +0 -1
  76. package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
  77. package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
@@ -113,7 +113,7 @@ if (this.itemVisible && this.itemKey) {
113
113
  ## 4. 布局陷阱(反复中招)
114
114
 
115
115
  0. **卡片根节点绝不用画布/页面灰底;外圆角只画在卡片本体上;绝不从截图四角采样背景。**(rules.md §九)
116
- → 聚焦图 `card-N.png` 是 RGBA,卡片几何(`spec.cards[].mask`,`type:"geometry"`)之外已透明,背景可能是非均匀/非固定色,禁止取四角像素当 Root 背景。纯色卡:Root 背景=`spec.cards[].cardFill`(卡片自身 fill),外圆角放 Root,第一层容器透明且不重复圆角(geo 已按此约束)。分层卡:Root `backgroundType=00` 透明,圆角与背景放内部独立卡容器(v3 的 `whiteCard`)。`cardFill` 为 null 时 Root 必须透明。**禁止**把画布灰底(`#F5F6F8` 等)或 `TOKEN.pageBg/fallbackBg` 写进 Root 背景。
116
+ → 聚焦图 `card-N.png` 是 RGBA,卡片几何(`spec.cards[].mask`,`type:"geometry"`)之外已透明,背景可能是非均匀/非固定色,禁止取四角像素当 Root 背景。纯色卡:Root 背景=`spec.cards[].cardFill`(卡片自身 fill),外圆角放 Root,第一层容器透明且不重复圆角(v3 `whiteCard` 已按此约束)。分层卡:Root `backgroundType=00` 透明,圆角与背景放内部独立卡容器(v3 的 `whiteCard`)。`cardFill` 为 null 时 Root 必须透明。**禁止**把画布灰底(`#F5F6F8` 等)或 `TOKEN.pageBg/fallbackBg` 写进 Root 背景。
117
117
  1. **根节点(Root)不要在 style 里放 rect 组(width)。**
118
118
  → 根节点宽度由顶层 `view.width="375"` 承载,`view.style.properties` 里**不要**加 `rect` 组(含 `width:375`)——冗余且与 view.width 冲突(导出实证:49+ 张卡根 style 均无 rect)。误加时 `check_topology.py` 会报 `Root_style_rect_forbidden`(hard 级)提示删除。铁律见 `rules.md §九`。
119
119
  1. **Column 默认 `width:100%`,同 Row 内两个 100% 宽 Column 会换行。**
@@ -260,27 +260,58 @@ gen-card-test 第一轮就把收支占比条写成了静态 `43%` / `57%` 的 `D
260
260
 
261
261
  ### 自动化覆盖索引(哪些规则已由工具兜底)
262
262
 
263
+ > 2026-09-25 全量盘点刷新(原表 2026-08-19 版已过期:R23/R25/R29/R30/R31/R32/R33/R35/R42/R43/R44/R50-R57/R59/R60/R61/R63/binding-\*/§10/§20.4/§27 均已陆续落入门禁)。逐条 code 的唯一权威 = `scripts/lib/rules/registry.mjs`。
264
+
263
265
  | 规则 | 工具 | 自动校验内容 |
264
266
  |---|---|---|
265
- | **R1 属性值来源** | `validate_schema.py` | itemValueFrom 缺失/空串 → ERROR |
266
- | **R5 验收双门禁** | `render_diff.mjs` | 设计稿 vs 渲染并排对比,AI 多模态判断差异(替代旧 fidelity 分数) |
267
- | **R8 圆角归属** ✅ | `lint_card.mjs` `r8-double-border-radius` | Root 与一级子容器双重圆角 → INFO |
268
- | **R11 渲染器默认样式** | `validate_schema.py` | style 子项缺 itemValueFrom → ERROR(覆盖隐性根因) |
269
- | **R17 叶子节点防压缩** ✅ | `lint_card.mjs` `r17-leaf-no-flexshrink` | Text/Icon/Image/MultiText 缺 flexShrink:0 → **ERROR** |
270
- | **R19 space-between 宽度** ✅ | `lint_card.mjs` `r19-space-between-no-width` | Row space-between 且宽度不确定(未设/auto)又未被父容器拉伸 → WARN |
271
- | **R22 width:100% 可压缩** ✅ | `lint_card.mjs` `r22-width100-flexshrink0` | width:100% + flexShrink:0 → **ERROR** |
272
- | **R63 水平 margin 撞 width:100%** ✅ | `lint_card.mjs` `r63-width100-with-hmargin` | 交叉轴父容器(Root/Column/SingleColumn/flexDirection:column/AbsoluteLayout 的 abspos 子节点)里 width:100% + 正水平 margin(含全边 marginType:0)→ **ERROR**;主轴(Row/MultiColumn)靠 flexShrink 吸收,不报 |
273
- | **style itemId** | `validate_schema.py` | flexDirection 必须是 006 不是 002 等 → ERROR |
274
- | **Button/Icon click** | `lint_card.mjs` | Button 缺 click → WARN(祖先带 click 时降为 INFO `button-click-by-ancestor`:`Button.vue` 的 `$emit('click')` 未 `.stop`,原生 click 会冒泡到祖先 handler,按钮实际可点,只是热区被放大);Icon 缺 click → **ERROR**(R-FMT3) |
275
- | **Canvas onMeasured** | `lint_card.mjs` | Canvas 缺 onMeasured → WARN |
276
- | **Root background** | `lint_card.mjs` | Root 缺 background → INFO |
277
- | **R-FMT1 borderRadius 格式** ✅ | `lint_card.mjs` `fmt-border-radius-tuple` | borderRadius 元组 `{...}` → **ERROR** |
278
- | **R-FMT2 padding/margin 格式** ✅ | `lint_card.mjs` `fmt-padding-tuple` | padding/margin 元组 `{...}` → **ERROR** |
279
- | **R-FMT4 MultiText 默认值** ✅ | `lint_card.mjs` `multitext-item-no-default` | itemList itemValueFrom:1 缺 itemDefaultValue → **ERROR** |
280
- | **R9/R10 Root 零间距** ✅ | `lint_card.mjs` `r9-root-nonzero-padding` | view 设非零 padding/margin → **ERROR** |
281
-
282
- > **用法**:Workflow A 步骤 4 中,`validate_schema.py`(结构合法性)+ `lint_card.mjs`(行为规则)**双门禁全过 0 ERROR** 才能进入步骤 5 渲染。lint 已把渲染相关的关键规则(R17/R22/R-FMT1-4/R9)升级为 **ERROR**,在渲染前拦截——不等到截图比对才发现。
283
- > 未被工具覆盖的规则(R2/R3/R4/R6/R7/R9/R10/R12-R16/R18/R20/R21)仍需 AI 在生成时遵守,下面逐条说明。
267
+ | **§1 itemValueFrom 必填** | `validate_schema.py` + `autofix_schema.py` + lint `missing-itemValueFrom` | 缺失/空串 → ERROR;一键补 "0" |
268
+ | **§2 style itemId** | `validate_schema.py` + lint `style-itemId-mismatch` | flexDirection 002/006、font lineHeight 007 等错配 → ERROR |
269
+ | **R4 列表数据驱动** ✅ | lint `binding-repeater-no-scope`(ERROR) / `binding-array-unconsumed`(WARN) / `binding-list-static-expansion`(WARN) + readiness bindings/lists 门 | 复现器缺 value / slotScope 模板 → ERROR;数组零消费、同构静态 tile 展开 → WARN;有视觉提名(listCandidates)而零数组化 → readiness blocker |
270
+ | **R8 圆角归属** ✅ | lint `r8-double-border-radius` | Root 与一级子容器双重圆角 → INFO |
271
+ | **R9/R10 Root 零间距** ✅ | lint `r9-root-nonzero-padding` | view 设非零 padding/margin → **ERROR** |
272
+ | **R14/R28 分割线 = Divider** | preflight `r28-row-as-divider`(WARN) + fidelity `elements.shapes` | Row 模拟分割线 → WARN;spec 细长 shape 缺 Divider 对应 → 迭代报告 `gate.elementHints` |
273
+ | **R16 slice 必须 Image** | fidelity `elements.slices`(切片覆盖)+ lint `r31c`(悬空引用 → ERROR) | 源缺失静默降级由生成前 WARN 与引用悬空双拦 |
274
+ | **R17 叶子节点防压缩** ✅ | lint `r17-leaf-no-flexshrink` | Text/Icon/Image/MultiText 缺 flexShrink:0(单子容器)→ **ERROR** |
275
+ | **R19 space-between 宽度** ✅ | lint `r19-space-between-no-width` | Row space-between 且宽度不确定又未被父容器拉伸 → WARN |
276
+ | **R20 Icon 显式 rect** ✅ | lint `r20-icon-no-rect` | Icon 缺 rect width/height(按默认 32×32 渲染)→ WARN |
277
+ | **R22 width:100% 可压缩** ✅ | lint `r22-width100-flexshrink0` / `r22b` | → **ERROR** |
278
+ | **R23 百分比 Math.round** ✅ | lint `r23-no-math-round` | → **ERROR** |
279
+ | **R25 Image itemValueFrom 三态** ✅ | lint `r25-image-*` 四向 + preflight Check 3 | 静态路径 from≠2 / 表达式 from≠1 / `$` 进图片槽 → **ERROR** |
280
+ | **R29 元素遗漏** | preflight `r29-element-count` | schema 节点数 vs spec 关键元素数 → WARN |
281
+ | **R30 Icon 事件格式** | lint `icon-no-click`(**ERROR**) / preflight `r30-icon-no-action`(WARN) | 缺 click / 扁平格式 |
282
+ | **R31 切图「引用 ↔ 磁盘名」同源** ✅ | lint `r31`(空格) / `r31b`(保留字符) / `r31c`(悬空) | → **ERROR** |
283
+ | **R32 Image 显式 rect.height** ✅ | lint `r32-image-no-height` | → **ERROR** |
284
+ | **R33 根样式全量提取** | lint `r33-root-missing-border` / `r33-root-missing-shadow` | spec 有 border/shadow 而根缺 → WARN |
285
+ | **R34 渲染契约归一化** ✅ | lint `r34-null-child`(null 子节点 → ERROR)+ `r17` / `icon-no-click` 覆盖其余契约 | 生成器出口 `normalizeRenderContract` 已实现,lint 防手写回归 |
286
+ | **R35 多子容器全刚性** ✅ | lint `r35-multi-child-all-shrink0` | → **ERROR** |
287
+ | **R37/R41 源文本单一消费** | trainer `duplicationScore`(语义重复渲染 → 门禁) | 训练链路承载 |
288
+ | **R38 容器高度** | lint `r38-negative-margin`(负 marginTop → ERROR,对应 R42)+ `r38-root-fixed-height`(Root 定高 → WARN) | |
289
+ | **R42 标题+装饰条** ✅ | lint `r42-title-underline-align/z-order/fixed-width` + `r64-title-underline-flow` | → ERROR/WARN |
290
+ | **R43 卡背景禁全透明** ✅ | lint `r43-card-transparent-background` | 背景声明缺失/Type 00 → **ERROR** |
291
+ | **R44 类型+值成对** ✅ | lint `r44-*` 七向(borderRadius/border/shadow/margin/padding/background) | 有值缺类型开关 → **ERROR** |
292
+ | **R49 inputParams 完整性** | `validate_schema.py` + expand_ir `normalizeInputParams` | 7 必填字段缺失 → ERROR(展开期自动补) |
293
+ | **R50 列表循环 $$** ✅ | lint `r50-dollar-dollar-field` / `r50b-unnormalized-scope-alias` | → **ERROR** |
294
+ | **R51 事件 this.data** ✅ | lint `r51-event-this-data-misuse` | 读非 Input 绑定键 → **ERROR** |
295
+ | **R52/R53 v-show** | lint `r52-vshow-condition-name-style`(WARN) / `r53-vshow-needs-wrapper`(**ERROR**) | |
296
+ | **R54/R55/R56 事件契约** ✅ | lint `r54-list-item-must-postmessage` / `r54-globalset-no-root-listener` / `r55-event-no-dollar-dollar` / `r56-jsapi-no-window-guard` | → **ERROR** |
297
+ | **R57 透明度不支持表达式** ✅ | lint `r57-opacity-expression` | → **ERROR** |
298
+ | **R61 数字语义** | lint `r61-text-numeric-literal`(千分号/货币/单位词/日期四形态) | 疑似运行期数字/日期静态文案 → WARN |
299
+ | **R63 width:100% 撞水平 margin** ✅ | lint `r63-width100-with-hmargin` | 交叉轴父容器 → **ERROR**(主轴靠 shrink 吸收,不报) |
300
+ | **§10 子卡回传裁剪** ✅ | lint `r50c-alertdata-whole-state` | `data: this.state` 整体回传 / `state.x = res.data` 整体赋值 → WARN |
301
+ | **§14/§15 切图命名与判重** | lint `r59-slice-semantic-name` / `r60-slice-duplicate-content` | 非语义名 / 同内容多副本 → WARN |
302
+ | **§20.4 style 同名子项唯一** ✅ | lint `style-duplicate-child` | 组内重复 itemKey → **ERROR** |
303
+ | **§26 要素线(两段式门禁)** | `check_fidelity.mjs` + `iterate --gate`(`stage=structure` → `stage=pixel`) | 文案 missing/invented + symbols/shapes `elementHints`;丢要素的轮次不算进步 |
304
+ | **§27 循环插槽单容器** ✅ | lint `r65-slot-multi-children` | slotScope 插槽平铺 ≥2 子节点 → **ERROR** |
305
+ | **R-FMT1-3 元组格式** ✅ | lint `fmt-border-radius-tuple` / `fmt-padding-tuple` / `fmt-margin-tuple` | → **ERROR** |
306
+ | **R-FMT4 MultiText 默认值** ✅ | lint `multitext-item-no-default` | → **ERROR** |
307
+ | **Button/Icon click / Canvas** | lint `button-no-click`(WARN) / `button-click-by-ancestor`(INFO) / `canvas-no-onmeasured`(WARN) | |
308
+
309
+ > **用法**:Workflow A 步骤 4 中,`validate_schema.py`(结构合法性)+ `lint_card.mjs`(行为规则)**双门禁全过 0 ERROR** 才能进入步骤 5 渲染。lint 已把渲染相关的关键规则升级为 **ERROR**,在渲染前拦截。
310
+ >
311
+ > **未被工具覆盖的规则(2026-09-25 归类,三类)**——机器判据在这些规则上要么依赖设计稿语义(硬编码业务词违反 R36),要么属于流程纪律,**仍需 AI 生成时遵守**:
312
+ > 1. **几何/标注消费纪律**(判定依赖「读设计稿」的语义能力):R2(间距来源)、R10(标注可信度分层)、R15(flex 对齐心智模型)、R18(分割线配套隐藏)、R24(auto 宽紧邻定宽)、R27(space-between 配方,R17/R22/R19 组合已分别有 gate)、R45(宽度实测)、R46(装饰条表达式化)、R55/R56/R80-R87(语义装配纪律);
313
+ > 2. **生成器内部已实现**(v3 训练链路的实现逻辑,不进 schema lint):R34(出口归一化)、R36(Agent 裁决)、R39-R41、R50-R58(语义边界/行高纪律)、§11/§16-§24(几何装配与图表回放);
314
+ > 3. **流程纪律**(由 iterate 门禁与 Workflow 编排承载):R1/R5/R6/R7/R12(AI 语义复核,r61 提醒)、R13/R21/R47/§13 四红线。
284
315
 
285
316
  ### R1 属性值来源 = spec 标注 + meta,禁止凭记忆、禁止抄存量卡
286
317
  - **属性值类**(color/fontSize/lineHeight/radius/shadow/border/背景色)**直接读 spec 标注**(见 R10);组名 / 子项 key / 类型开关 / itemId 读 `vendor/preview/meta/style-meta.json` + `vendor/preview/meta/component-meta.json`。
@@ -436,7 +467,7 @@ gen-card-test 第一轮就把收支占比条写成了静态 `43%` / `57%` 的 `D
436
467
  - 图标视觉简洁(单色线条 + 语义明确)→ 优先找 sprite 字形。
437
468
  - 图标视觉复杂(多色、阴影、品牌细节)→ 切片优先。
438
469
  - sprite 字形清单:`grep -oE 'id="[^"]*"' vendor/preview/assets/svg-symbols.svg | sort -u`。
439
- - 验收:选择 Icon 时 sprite 字形存在;Icon 节点 style.rect 有 width/height 且等于设计稿尺寸;DOM 中 Icon 元素 `getBoundingClientRect` 与设计稿标注一致。
470
+ - 验收:选择 Icon 时 sprite 字形存在;Icon 节点 style.rect 有 width/height 且等于设计稿尺寸(**lint `r20-icon-no-rect` WARN 已拦**);DOM 中 Icon 元素 `getBoundingClientRect` 与设计稿标注一致。
440
471
 
441
472
  ### R21 程序化构建器陷阱(Python/JS 手写 schema 生成器,2026-08-20 固化)
442
473
  - 当卡片需要用脚本批量/半自动构建 schema(如 Python 构建器辅助生成)时,以下三类陷阱反复出现:
@@ -582,7 +613,7 @@ gen-card-test 第一轮就把收支占比条写成了静态 `43%` / `57%` 的 `D
582
613
  - **铁律**:`spec_to_schema_v3.mjs` 的 `generate()` 返回前调用 `normalizeRenderContract()`:`Text` / `Icon` / `Image` / `MultiText` 必须有 `flex.flexShrink=0`;`Icon` 必须有嵌套 `click` eventaction;组件树中禁止出现 `null` 子节点。
583
614
  - **根因**:逐段组装时容易漏掉通用契约;尤其是列表箭头 slice 命中但 PNG 未导出时,`imageFromSlice()` 返回 `null` 仍被放入插槽,导致运行期渲染挂起。
584
615
  - **处理**:契约归一化放在生成器出口,而不是依赖训练器或人工修复;可选 slice 为空时必须提供可见占位或 Icon 兜底。
585
- - **验收**:`validate_schema.py` 0 ERROR、`lint_card.mjs` 0 ERROR、递归扫描 schema 无 `null` 子节点,独立 Chrome 渲染能进入 `data-card-ready`。
616
+ - **验收**:`validate_schema.py` 0 ERROR、`lint_card.mjs` 0 ERROR、递归扫描 schema 无 `null` 子节点(**lint `r34-null-child` ERROR 已拦**),独立 Chrome 渲染能进入 `data-card-ready`。
586
617
 
587
618
  ### R36 语义组件由 Agent 裁决,脚本不得业务硬编码(2026-08-25 固化)
588
619
  - **铁律**:脚本只输出几何、标注和渲染证据;`display-value/input/action/list/media` 等组件语义由 Agent 结合截图与层级树裁决。禁止用“还款”“账单”等业务词触发组件选型。
@@ -614,42 +645,44 @@ gen-card-test 第一轮就把收支占比条写成了静态 `43%` / `57%` 的 `D
614
645
  - **根因**:`¥` 被同时消费为主金额前缀和选项组标签,造成金额重影。
615
646
  - **验收**:主金额、组标签、选项成员在 schema 中可追溯到不同源元素;渲染文本无重复。
616
647
 
617
- ### R42 列表带内分隔线不提升为顶层块(2026-08-26 训练固化)
648
+ > **R80–R87 = 2026-08-26/27 训练系列**(原编号 R42–R49,与下文 2026-09-02 系列撞号;后者被 lint / SKILL / quickref / rules 按号引用,故此处改为 R80–R87 消歧义,**内容未变**)。
649
+
650
+ ### R80 列表带内分隔线不提升为顶层块(2026-08-26 训练固化)
618
651
  - **铁律**:细长分隔线若落在重复媒体行或列表项的纵向带内,由重复项模板或列表容器承载;只有带外通栏分隔线才作为独立 divider 渲染。
619
652
  - **根因**:列表项之间的 divider 被提升为顶层块后,把列表项撑散、箭头插错位置,甚至挤掉后续内容。
620
653
  - **验收**:重复媒体行/ListLayout 的项间距来自模板;独立 divider 只出现在列表或重复带外。
621
654
 
622
- ### R43 重复行控件必须归入模板(2026-08-26 r039n 固化)
655
+ ### R81 重复行控件必须归入模板(2026-08-26 r039n 固化)
623
656
  - **铁律**:同一右缘、同尺寸、等距重复的小控件是重复行模板的一部分;ListLayout/重复媒体模板消费后,原始控件不得再进入孤立媒体块。已消费的副金额 label/value 也必须从列表标题候选互斥排除。
624
657
  - **根因**:列表模板已复用右缘箭头后,三个源箭头又被兜底成孤立媒体块;副金额值因字号和下方文本数量误判成列表标题,分别造成纵向膨胀和重影。
625
658
  - **验收**:ListLayout 渲染 N 行时只出现 N 个模板控件;schema 中同一源元素只映射到一个语义块;按钮后不得出现被面板消费的金额文本。
626
659
 
627
- ### R44 语义间距只计一次(2026-08-26 r039n 固化)
660
+ ### R82 语义间距只计一次(2026-08-26 r039n 固化)
628
661
  - **铁律**:相邻语义块的标注间距由统一边界表推导,不得在上一块底部和下一块顶部重复添加;列表标题、列表项、查看更多必须注册边界。列表行内边距用“标注行高 - 内容高”推导,ListLayout 行间距显式归零。
629
662
  - **根因**:列表语义缺失边界时,按钮误算到卡底的大留白;列表模板默认行距与项内 padding 叠加,导致内容撑出设计稿高度。
630
663
  - **验收**:DOM 顶层块顺序与标注 y 一致;内容变化时容器继续 auto 撑开;渲染总高不因固定行距或重复间距超过标注比例门禁。
631
664
 
632
- ### R45 分割线必须是原生 Divider(2026-08-26 r050n 固化)
665
+ ### R83 分割线必须是原生 Divider(2026-08-26 r050n 固化)
633
666
  - **铁律**:独立分割线用 `Divider` 组件承载,逻辑高度固定为 `1px`,颜色优先读标注填充;重叠去重阈值使用 `>=0.4` 的纵向覆盖比例。
634
667
  - **根因**:细线被兜底成普通 Image 后按倍率放大或与半像素线重复输出,视觉上变成粗线/重影。
635
668
  - **验收**:schema 中带外独立细线只有一个 `Divider`;渲染 DOM 高度为 `1px`;同位置无第二条可见线。
636
669
 
637
- ### R46 缺失切图按画板裁切优先(2026-08-26 r050n 固化)
670
+ ### R84 缺失切图按画板裁切优先(2026-08-26 r050n 固化)
638
671
  - **铁律**:缺失导出的媒体优先从原画板按标注矩形裁切为 `cropSlice`;裁切失败或切片视觉与画板局部不一致时才使用 `http://iph.href.lu/{w}x{h}?text=缺切图|切图冲突` 占位图。禁止退化为纯色块冒充图片。
639
672
  - **根因**:设计稿可能漏导切图,纯色会丢失图像结构;直接占位又浪费画板中已有的真实视觉证据。
640
673
  - **验收**:`spec.json` 缺导出项携带 `annotationDefects.resolution=artboard-crop` 与 `cropSlice`;生成器优先引用裁切文件;小切片用 alpha mask IoU 校验,低于 `0.35` 记录 `slice-visual-mismatch`。
641
674
 
642
- ### R47 面板流高裁剪必须有界(2026-08-26 r050n 固化)
675
+ ### R85 面板流高裁剪必须有界(2026-08-26 r050n 固化)
643
676
  - **铁律**:只有轻微重叠才允许把面板流式高度截到下一个重叠顶边,阈值为 `max(16px, 面板高度 * 25%)`;列表面板和大幅重叠保持完整高度。
644
677
  - **根因**:无界裁剪会让头部面板压低主金额;完全不裁剪又会保留跨面板的幽灵留白。
645
678
  - **验收**:轻微重叠卡的主金额 y 回到标注范围;列表面板不被压缩;整卡高度继续由内容撑开。
646
679
 
647
- ### R48 文本禁止压缩,辅助留白随对齐变化(2026-08-26 r050n 固化)
680
+ ### R86 文本禁止压缩,辅助留白随对齐变化(2026-08-26 r050n 固化)
648
681
  - **铁律**:所有 `Text` 节点统一设置 `flexShrink:0`;居中文本的辅助 padding 只加上下,不加左右;右对齐加右侧,左对齐加左侧。
649
682
  - **根因**:文本被挤压换行或截断后还原度失真;对称侧向 padding 又会把右值和左标签推出标注列。
650
683
  - **验收**:递归 schema 无可压缩 Text;长文案不横向压缩;居中、左对齐和右对齐辅助文本的偏移方向与标注一致。
651
684
 
652
- ### R49 列表行 padding 禁止 NaN(2026-08-27 r052 固化)
685
+ ### R87 列表行 padding 禁止 NaN(2026-08-27 r052 固化)
653
686
  - **铁律**:`T.pad('micro')` 等 token 引用必须确认键存在于对应 token 表(`PADDING` 表有 `micro`,`GAP` 表也有 `micro`);消费端用 `parseInt(val, 10) || 0` 防御性兜底。`Math.max(NaN, x)` 恒为 `NaN`,会导致 padding 静默失效。
654
687
  - **根因**:`PADDING` 表缺少 `micro` 键,`T.pad('micro')` 返回 `'undefined'`,`Number('undefined')` = `NaN`,使列表行 padding 全部为 `NaN`,行高坍塌。
655
688
  - **验收**:所有 `T.pad/T.gap` 调用的键在对应 token 表中存在;`parseInt` 兜底确保非负整数;列表行 padding 不为 `NaN` 或 `undefined`。
@@ -771,7 +804,7 @@ gen-card-test 第一轮就把收支占比条写成了静态 `43%` / `57%` 的 `D
771
804
  1. 渲染截图 vs 设计稿截图 → 像素级 band diff 分析(哪些 y 区间差异最大)。
772
805
  2. 对关键元素采样颜色(D=设计稿 RGB vs R=渲染 RGB),标注 Δ 差值。
773
806
  3. 检测元素 y 坐标偏移(渐变区域 top 差、按钮 top 差),找出级联偏移根因。
774
- 4. 针对性修改 → 重渲染 → 再对比,直到 match > 85%。
807
+ 4. 针对性修改 → 重渲染 → 再对比,直到 `pixelDiff.match` 达到收敛门禁(唯一权威 = `iterate.mjs` 的 `GATE.match`,以脚本为准,本文档不复制数字)。
775
808
  - **工具**:`iterate.mjs` 一条龙输出 pixelDiff.match 百分比 + lint.errors 计数;像素分析用 PIL/numpy 扫描。
776
809
  - **验收**:每轮迭代有量化指标(match% / Δy / Δcolor),不做盲改。
777
810
 
@@ -843,6 +876,7 @@ gen-card-test 第一轮就把收支占比条写成了静态 `43%` / `57%` 的 `D
843
876
 
844
877
  ### 验收
845
878
  - 子卡 `closeAlertCard` 回传对象无 `list`/`title`/`parentInstanceId` 等父卡注入字段;父卡回调里找不到 `that.state.xxx = res.data.currCard` 这类整体赋值。
879
+ - **lint `r50c-alertdata-whole-state`(WARN,2026-09-25 沉淀)**:`data: this.state` 整体回传 / `state.x = res.data` 整体赋值自动暴露(`res.data.<字段>` 形态无法机械区分字段提取与子对象整体赋值,不拦、交 AI 语义复核)。
846
880
  - 选中后父卡 `$currCard.xxx` 显示绑定字段齐全且值正确(事件面板用 `JSON.stringify` 核对)。
847
881
  - 关联范本:`references/event_patterns.md` alertCard/closeAlertCard 段、`references/recipes.md` §2。
848
882
 
@@ -952,16 +986,19 @@ gen-card-test 第一轮就把收支占比条写成了静态 `43%` / `57%` 的 `D
952
986
  **纪律**:`_altN` 只允许表示「同名切图在设计稿里确实标到了内容不同的多组位置」;**任何**派生/拷贝动作落盘前必须先过内容判重(sha → 像素容差 → 都不命中才写新文件)。看到 `src/slices/` 出现同内容多份 = 链路缺陷复发,先修链路再清存量(`slices_normalize.mjs --dedupe --write`)。
953
987
 
954
988
 
955
- ## 16. 图表矢量簇(柱状图)回放:flex 原生优先、Canvas 仅兜底(2026-09-18,r213-r221 沉淀,r242-r247 升级见 §19)
989
+ ## 16. 柱状图回放:flex 原生优先、Canvas 仅兜底(2026-09-18,r213-r221 沉淀;r242-r247 flex 原生化)
956
990
 
957
991
  **事故**:综合推荐/保险保障卡的柱状图(窄柱 + 网格线 + 轴标签/值标签)在 spec 里是纯矢量
958
992
  shape/text,DSL 无图表组件,生成器此前把柱体静默丢弃、网格线当 Divider 错排——整块图表区
959
993
  (约占卡高 25%)像素全错,position 分崩塌。
960
994
 
961
- **规则(spec_to_schema_v3.mjs 已实现,纯几何判定)**:
962
- - `detectChartClusters`:同基线(底沿 ±3)窄柱(宽 4-48 / 高 4-260 / 圆角 <8)≥4 根 + 横向近等距
963
- + 网格线(h≤2 且与柱区横向重叠 ≥60%)≥3 或簇内标签 ≥4 → 判为柱状图簇;
964
- - 整簇(柱 + 网格线 + 簇 bbox 扩边内的文本标签)从通用文本/形状流摘出,改由一个 Canvas
995
+ **形态判定(`lib/v3_shared.mjs` 已实现,纯几何、两路共用)**:`detectChartClusters` —— 同基线(底沿 ±3)窄柱(有纯色/渐变 fill、圆角 <8、宽 4-48 / 高 4-260)≥4 根 + 横向近等距 + (网格线(h≤2 / 宽 ≥60 / 与柱区横向重叠 ≥60%)≥3 **或** 簇内标签 ≥4)→ 判为柱状图簇;不满足则不识别,留在通用流。
996
+
997
+ **首选路径:flex 原生组件树(r242-r247,`buildFlexBarChart`)** —— Canvas 自绘会让标注文本在组件计数中**消失**(schemaComponents 只数 Text 节点,textComponents 被系统性压低,实测 0.4375),且 canvas 字体渲染与设计稿偏差大,故图簇优先 flex 原生:
998
+ - 轴列:Column(space-between, align flex-end) 承刻度文本;绘图区 Row(align flex-end) 按「类目标签中心中点」切槽位;槽内柱列宽=max(柱宽, 值标签宽+4)(渲染字形比标注 rect 略宽,不留余量会折行 h16→30 整列上移 13px),相邻标签水平重叠用负 marginLeft 拉回柱中心;值标签在柱顶之上(Column justify flex-end 流内自然落位)。
999
+ - 网格线=绘图区**硬停线性渐变背景**(坐标系线条在绘制层最底部,与 R62 同旨);类目行按槽位居中、比柱对宽时撑开 + 负 margin 锚柱对中心 + 槽位列 overflow visible。
1000
+
1001
+ **兜底路径:Canvas 回放(r213-r221,`chartBarCanvasCode`)** —— 已识别为图簇但 `buildFlexBarChart` 返回 null 时(柱<4 / 网格线<2 / 无簇内文本 / 缺轴刻度或类目标签 / 切槽后空槽),整簇(柱 + 网格线 + 簇 bbox 扩边内的文本标签)从通用文本/形状流摘出,改由一个 Canvas
965
1002
  (`rect` = 簇 bbox,`flexShrink:0`)在 onMeasured 里按簇内相对坐标回放:柱 = 四角 arc 拼路径
966
1003
  (预览器白名单**无 roundRect**)+ `fill()`;网格线 = `moveTo/lineTo/stroke`;标签 = `fillText`
967
1004
  (`textAlign/textBaseline` 按标注);渐变柱用 `createLinearGradient` + 原生 `addColorStop`。
@@ -969,12 +1006,12 @@ shape/text,DSL 无图表组件,生成器此前把柱体静默丢弃、网格
969
1006
  - 簇必须登记 `addSemanticBound(y, y+height, container=true)`:Canvas 是流内真实高度,不登记则
970
1007
  下文所有 section 的顶距从更早的底沿起算(实测下一节被推偏 200+px)。
971
1008
 
972
- **预览器 ctx 铁律(本次最大坑)**:`event.canvas` 是**画布包装对象**而不是 ctx!必须先
1009
+ **预览器 ctx 铁律(Canvas 路径最大坑)**:`event.canvas` 是**画布包装对象**而不是 ctx!必须先
973
1010
  `var cv = event.canvas.getContext('2d')` 拿代理 ctx(export-proven 范式见 assetAllocRadar 的
974
1011
  gaugeCanvas/radarCanvas)。直接把 `event.canvas` 当 ctx 调 `fillRect/arc` 会**静默无输出**
975
1012
  (不抛错、画布全空白),排查成本极高。另:`canvas.width = event.width` 是 no-op 但无害。
976
1013
 
977
- **连带护栏(同轮沉淀)**:
1014
+ **同轮沉淀的连带护栏(非图表,2026-09-18)**:
978
1015
  - 列表模板签名护栏:重复行并表前按结构签名(行图标/复合金额对/行右沿/行内操作)取多数签名,
979
1016
  少数签名行退出 ListLayout——半宽对比面板与全宽产品行曾被并成一表,模板字段错位
980
1017
  (desc 落 title 槽竖排换行 + 缺键渲染「这是一个文本」占位)且对比块双份渲染。
@@ -1013,19 +1050,16 @@ padRight 被算成 303px 把左栏压没。
1013
1050
  - 值行与**标签文本**左对齐(圆点只缩进标签行):缺这段 line2 内缩金额会顶到壳左沿。
1014
1051
  - 状态 tag 壳 fill 常报成描边同色(实为浅底):fill 与字色亮度差<0.08 时按描边色 8% tint 还原,否则彩字隐入同色底。
1015
1052
 
1016
- ## 19. 柱状图 flex 原生回放:Canvas 只作兜底(2026-09-18,r242-r247 训练沉淀)
1053
+ ## 19. 柱状图 flex 原生回放(已并入 §16)
1017
1054
 
1018
- Canvas 自绘柱状图会让标注文本在组件计数中**消失**(schemaComponents 只数 Text 节点),textComponents 被系统性压低(实测 0.4375),且 canvas 字体渲染与设计稿偏差大。柱状图簇改 flex 原生组件树:
1019
- - 轴列:Column(space-between, align flex-end) 承刻度文本;绘图区 Row(align flex-end) 按「类目标签中心中点」切槽位;槽内柱列宽=max(柱宽, 值标签宽+4)(渲染字形比标注 rect 略宽,不留余量会折行 h16→30 整列上移 13px),相邻标签水平重叠用负 marginLeft 拉回柱中心;值标签在柱顶之上(Column justify flex-end 流内自然落位)。
1020
- - 网格线=绘图区**硬停线性渐变背景**(坐标系线条在绘制层最底部,与 R62 同旨);类目行按槽位居中、比柱对宽时撑开 + 负 margin 锚柱对中心 + 槽位列 overflow visible。
1021
- - 不满足柱状图形态(柱<4 / 网格线<2 / 缺轴或类目标签)才回退 chartBarCanvasCode。
1055
+ > 本节(flex 原生柱状图回放 + Canvas 兜底判据)已全量合并进 **§16 柱状图回放:flex 原生优先、Canvas 仅兜底**。保留本节号与标题作重定向锚点,避免既有引用断链。
1022
1056
 
1023
1057
  ## 20. 预览渲染器契约四铁律(2026-09-18 实测固化,vendor/preview standalone)
1024
1058
 
1025
1059
  1. **无绝对定位**:style 解析器只认 rect/flex/background/padding/margin/borderRadius/border/shadow/transform/other;`position` 组被**整体忽略**(AbsoluteLayout 子节点 left/top 全失效、堆在 0,0)。二维散布只能「行 + 负 margin」或「列 + paddingTop」组合表达。
1026
1060
  2. **overflow 默认 hidden**(parseOther:`t.overflow = e.overflow || 'hidden'`):任何「溢出可见」必须显式 other.overflow=visible;靠溢出叠图的方案默认被裁。
1027
1061
  3. **负 margin 可用**(margin 组 marginType=1 逐边):flex 行内唯一可重叠/回拉手段;人力卡(holdFinancialProducts 等)亦在用。
1028
- 4. **style 组同名子项唯一**:makeStyle 已带空 backgroundColor/backgroundGradient 子项,绑定值必须**改写**该子项(itemValue=$x + itemValueFrom=1);追加第二个同名子项会让渲染器取到空值/绑定串原样进 doColor → 背景透明(列表逐行壳色 $rowBg 曾因此整片消失,pix 0.237→0.085 即修复此 bug)。
1062
+ 4. **style 组同名子项唯一**:makeStyle 已带空 backgroundColor/backgroundGradient 子项,绑定值必须**改写**该子项(itemValue=$x + itemValueFrom=1);追加第二个同名子项会让渲染器取到空值/绑定串原样进 doColor → 背景透明(列表逐行壳色 $rowBg 曾因此整片消失,pix 0.237→0.085 即修复此 bug)。**lint `style-duplicate-child`(ERROR,2026-09-25 沉淀)已拦**。
1029
1063
 
1030
1064
  ## 21. 散点带:中心媒体 + 环绕胶囊簇按三列重建(2026-09-18,r250-r255 训练沉淀)
1031
1065
  - **形态**:≥4 个 pill 按钮(radius≥10、h≤40、有底色/渐变)分 ≥2 行环绕一个近方形中心媒体(雷达/环图裁切图,orphanBlocks 60-200 近方)。「五笔钱」类布局即此。
@@ -1093,3 +1127,94 @@ Canvas 自绘柱状图会让标注文本在组件计数中**消失**(schemaCom
1093
1127
  影响:`/__ai/chat`(整轮对话历史 + 卡片片段,动辄几百 KB 中文)每次错一个字符 ⇒ 下一轮前端回传的历史与服务端上一轮实际发送的 prompt **不再逐字节一致**,
1094
1128
  **前缀缓存从该处起整段失效**(这是「同一张卡每轮都吃满 input token」的一个隐藏放大器);`/__save` 更糟 —— 被劈坏的字符直接写进 `schema.json`。
1095
1129
  修法:所有请求体读取前 `req.setEncoding('utf8')`(StringDecoder 会正确拼接跨块序列)。已覆盖 `ai-chat / cardFiles / cardSave / cardHistory / cardSync / sliceUpload / codeBridge / logStream / cardOpenFolder` 与 e2e 脚本。
1130
+
1131
+ ## 27. 列表循环插槽:每个循环项必须由【单一容器】包裹,slot 下禁止平铺多个子节点(2026-09-24 gen-1 汇率卡事故固化)
1132
+
1133
+ - **现象**:把写死的重复行改成 ListLayout 绑定 `$exchangeRows` 时,在 slotScope=true 的 slot 里平铺放了两个平级子节点 —— 数据行 `comp_8:Row` 和分隔线 `comp_19:Row`。结果每个「汇率条目」被拆成两段错位重复,行与分隔线互相穿插、最后一条没有分隔线。
1134
+ - **根因(确证 ListLayout 语义)**:`slotScope=true` 的 slot 是**循环模板**,其 `components[]` 数组是**一个整体模板**,会**原样整体**套到 value 数组的每一项上重复渲染。slot 里放几个平级子节点,这几个就会在每个 item 下都平铺出现 —— **不是**「各自独立显示一次」。所以把「数据行 + 分隔线」分开放成两个平级子节点,等价于每个 item 都渲染「数据行,分隔线」两个并列块,二者无法构成「一行内容 + 一条底边线」的纵向整体,互相穿插错位。
1135
+ - **正路**:每个循环 item 的**全部**元素(数据行、分隔线、角标、标签、插拔箭头…)必须包进**一个**容器(`Column` 或 `Row`),这个容器是 slot children 数组里的**唯一元素**。形如:ListLayout.slots[0].slotScope=true,children[0] = Column「item」,其内 slots[0] = [数据行 Row, 分隔线 Row, ...任意数量]。该 Column 承担 item 骨架(`rect.height` 约束、`gap` 控制行与分隔线间距、`flexShrink`),内层自由排布。**禁止**在 slot 直接平铺 ≥2 个顶层子节点。
1136
+ - **识别要点**:分隔线、角标这类「非数据主行」元素若与主行平级,会被当成 items 的一部分铺开;只有包进一个容器的 slot,才能用 flex 竖直方向把「行内容 + 分隔线」固定成一个不可拆散的整体,循环时作为一个条目对齐滚动与间距。
1137
+ - **护栏**:lint `r65-slot-multi-children`(**ERROR**,2026-09-25 沉淀:slotScope 插槽平铺 ≥2 子节点直接拦);迭代阶段 validate/lint/preflight 零 error + `renderOk` 不代表 slot 平铺多子节点不产生视觉错位 —— 必须用 `card_screenshot`(渲染原图)核对条目边界。
1138
+
1139
+
1140
+ ## 28. midCluster 固定 padding 漂移 + optionGroup 侧标签 + 像素排除缺陷(2026-09-26 转账计划确认卡训练固化)
1141
+
1142
+ ### 28.1 midCluster paddingTop 必须用 semanticTopGap
1143
+ - **现象**:midCluster 行(如「付款账户/收款账户」标签)恒定用 `mcPad=4` 作 paddingTop,与 spec 顶距(如 12px)不符,上半部所有元素系统性高 12px,逐元素累积成 IoU=0.31。
1144
+ - **根因**:midCluster 组装路径独立于通用 auxText 行,padding 写死 `mcPad`(=T.pad('mini')=4),不走 `semanticTopGap()`。
1145
+ - **正路**:`const mcTopGap = semanticTopGap(sem.midCluster.y) ?? mcPad`,paddingTop 用 `mcTopGap`。**所有**语义行的 paddingTop 都必须走 `semanticTopGap()` —— 固定常量会让该行与 spec 顶距脱钩,下游所有行跟着漂。
1146
+ - **代码**:`v3_assemble.mjs` ~L2197。
1147
+
1148
+ ### 28.2 optionGroup 侧标签(label 与按钮同 y 在左)
1149
+ - **现象**:表单标签「执行日期」位于切换按钮(仅一次/按月/按年)的**左侧同 y**,被当成独立 auxText 行竖排堆叠,凭空多出 ~28px 高度,下半部全部下移。
1150
+ - **根因**:optionGroup 标签检测只查「按钮正上方」(`bottom <= top && top - bottom <= 48`),不查「同 y 左侧」。侧标签漏检 → 进 auxTexts → 独立成行。
1151
+ - **正路**:标签检测增加侧标签路径——垂直中心重叠(`|tcY - grpCY| <= max(15, groupHeight*0.6)`)且文本右沿 ≤ 按钮组左沿 +4px。命中后标记 `sideLabel: true`,组装时标签与 chips 同行 `space-between`。
1152
+ - **anchorY 陷阱**:侧标签的 `anchorY` 必须用 `group.y`(不能用 `labelY`),否则落入 optionGroup 自身注册的 semanticBound 内 → `semanticTopGap` 归零 → 顶距丢失。
1153
+ - **代码**:`v3_extract.mjs` ~L327 侧标签检测;`v3_assemble.mjs` ~L1635 anchorY + ~L1705 内联渲染。
1154
+
1155
+ ### 28.3 像素排除:slice-visual-mismatch + cropFile 空格归一
1156
+ - **现象**:大按钮切片(`矩形 4`,有 `slice-visual-mismatch` 缺陷,走 artboard-crop 兜底)渲染为空白占位图,像素差全计分(89% 热区);但排除逻辑只认 `missing-slice` 类型 → 不排除。
1157
+ - **根因①**:排除条件 `defect.type !== 'missing-slice'` 漏了 `slice-visual-mismatch`(同一条 artboard-crop 兜底路径)。
1158
+ - **根因②**:cropFile 匹配用 spec 原始名(空格 `矩形 4`)对 schema JSON(消毒后 `矩形-4`),`includes()` 恒 false。
1159
+ - **正路**:排除条件加 `slice-visual-mismatch`;cropFile 匹配增加 `.replace(/\s+/g, '-')` 归一兜底。
1160
+ - **代码**:`run_round.mjs` ~L1118。
1161
+
1162
+ ### 28.4 pairDeviations 重复文本交叉匹配
1163
+ - **现象**:诊断输出出现 `dy=-202` 幻影偏差(两处「转账计划」交叉配对),误导修复方向。
1164
+ - **根因**:`pairDeviations` 按 score 取首个匹配,无几何 tiebreaker —— 相同文本按数组顺序配对,而非最近距离。
1165
+ - **正路**:score 并列时取几何中心最近者(与 `positionScore` 同款 tiebreaker)。
1166
+ - **代码**:`diagnosis.mjs` ~L64。
1167
+
1168
+ ## auto. generated leaves lacked flexShrink and Icon lacked click con(r002 训练回灌)
1169
+ > lesson: `LESSON-20260824-002` status: `promoted`
1170
+
1171
+ v3 generate 返回前统一执行渲染契约归一化:Text/Icon/Image/MultiText 设置 flex.flexShrink=0,Icon 补嵌套 click eventaction。
1172
+
1173
+
1174
+ ## auto. large currency value with right edit affordance rendered as (r018n 训练回灌)
1175
+ > lesson: `LESSON-20260825-036` status: `promoted`
1176
+
1177
+ 货币符号 + 数值 + 右侧编辑/箭头控件是金额输入候选;Agent 复核后用 Input + 独立货币前缀,控件高度读标注或行高推导。禁止用业务词触发该规则。
1178
+
1179
+
1180
+ ## auto. button text consumed again as list title caused duplicate gh(r018n 训练回灌)
1181
+ > lesson: `LESSON-20260825-037` status: `promoted`
1182
+
1183
+ 源文本单一消费:按钮文案必须从列表标题候选中互斥排除;列表标题必须绑定完整业务容器,而不是按钮壳。
1184
+
1185
+
1186
+ ## auto. fixed root height clipped variable-length content(r018n 训练回灌)
1187
+ > lesson: `LESSON-20260825-038` status: `promoted`
1188
+
1189
+ Root/普通容器高度必须 auto,由子内容、gap 和标注 padding 撑开;只有叶子控件可读标注高度。
1190
+
1191
+
1192
+ ## auto. auxiliary texts with different y values were grouped into on(r038n 训练回灌)
1193
+ > lesson: `LESSON-20260826-039` status: `promoted`
1194
+
1195
+ sameRow 只能消费带 rect/sourceEl 的原始标注对象;语义投影对象必须显式携带 sourceEl 或 y。分组循环禁止对丢失 rect 的投影对象重复调用几何函数。
1196
+
1197
+
1198
+ ## auto. a composed primary amount fragment was reused as an option-g(r038n 训练回灌)
1199
+ > lesson: `LESSON-20260826-041` status: `promoted`
1200
+
1201
+ 主金额合成片段不得同时成为 option-group label;label 候选必须位于主金额矩形外且不是主金额合成文本的组成部分。源文本继续执行单一消费。
1202
+
1203
+
1204
+ ## auto. standalone divider rendering split repeated media rows and l(r038n 训练回灌)
1205
+ > lesson: `LESSON-20260826-042` status: `promoted`
1206
+
1207
+ 细长分隔线只有在重复媒体行或列表项带外才是独立语义块;带内分隔线由 ListLayout/重复项模板承载,避免把项间 divider 提升成顶层块。
1208
+
1209
+
1210
+ ## auto. panel value and repeated right affordance leaked into other (r039n 训练回灌)
1211
+ > lesson: `LESSON-20260826-044` status: `promoted`
1212
+
1213
+ 源文本单一消费扩展到副金额:面板 label/value 必须从列表标题候选互斥排除;同一右缘、同尺寸、等距重复的小控件由列表或重复行模板唯一消费,不得再作为孤立媒体块渲染。
1214
+
1215
+
1216
+ ## auto. semantic gaps were double-counted and list spacing overflowe(r039n 训练回灌)
1217
+ > lesson: `LESSON-20260826-045` status: `promoted`
1218
+
1219
+ 列表标题、列表项和查看更多必须注册语义边界;相邻块间距只计一次。列表行内边距由标注行高减内容高推导,ListLayout 行间距显式归零,容器高度继续由内容撑开。
1220
+
@@ -46,7 +46,7 @@
46
46
  "id": "cardId", // pageId(可选)
47
47
  "name": "卡片名", // 可选
48
48
  "platform": "002", // 可选,默认 002
49
- "width": "375", // 可选,默认 375
49
+ "width": "343", // 卡片本体设计宽(卡面宽,非画板宽;省略默认 375。见 §2f 铁律 1)
50
50
  "viewId": "view_xxx", // Root 的 componentId(可选,round-trip 用)
51
51
  "dictionary": { ... }, // 可选;省略时展开器从 $ 绑定自动推断 inputParams + 默认 mock
52
52
  "s": { ... }, // 可选;Root(view)自身的样式
@@ -74,7 +74,26 @@
74
74
  | 动态值(金额 / 等级 / 日期 / 名称…) | `v: "$memberLevel"` | 绑定;`defaultValue` = 设计稿字面量 |
75
75
  | 条件文案 | `v: "$showAll ? '收起' : '查看更多'"` | 表达式;分支字面量也参与 fidelity 对齐 |
76
76
  | 金额展示 | 照 `recipe_snippets/money-format.json` | 格式化表达式,别把「¥7,000.00」拆成字面量 |
77
- | 重复行 / tile 组 | 数组变量 + `listScope` 插槽,子树内 `$item.xxx` / `$_index` | 范式见 `recipe_snippets/list-expand-collapse.json` 与 `src/cards/account` 的 `account_details` |
77
+ | 重复行 / tile 组 | 数组变量 + `listScope` 插槽,子树内 **`$<字段名>`** / `$_index` | 范式见 `recipe_snippets/list-expand-collapse.json` 与 `src/cards/account` 的 `account_details` |
78
+
79
+ > **列表插槽里的作用域**(运行时契约,见 `card-editor/runtime/evaluate.js` 的 `makeSlotScope`):
80
+ > `listScope` 的子作用域 = 外层作用域 ∪ **item 的字段逐个展开** ∪ `{$$: 整个 item, _index: 行索引}`。
81
+ > schema 里的**规范形**只有三种:
82
+ >
83
+ > | 你要取 | 规范形 | 也接受的别名(生成器自动改写) |
84
+ > |---|---|---|
85
+ > | item 的某个字段 | `$<字段名>` | `$item.<字段名>` |
86
+ > | 整个 item 对象 | `$$` | `$item` |
87
+ > | 行索引 | `$_index` | `$item._index` |
88
+ >
89
+ > 别名由 **`expand_ir.mjs` 在扩展开头自动归一化**(`lib/scope_alias.mjs`;schema 级则由
90
+ > `autofix_schema.py` 规则 5 兜底)。写别名不会被打回、也不会中止写盘 —— 生成器只负责把
91
+ > 规范形落到 schema,不需要你记住这条规则。**唯一的例外**:卡片真的声明了名为 `item` 的根变量
92
+ > (`dictionary.inputParams[].key === 'item'`)时 `$item.x` 是合法取值,一律不改写。
93
+ >
94
+ > 为什么必须归一化而不是「禁止 + 报错」:`$item.<字段>` 求值恒为 `undefined` 且**不抛错**
95
+ > (validate 绿、preflight 过、`/__render-status` 的 `ok=true`,只有它单列的 `bindingErrors`
96
+ > 会记一笔)。这类「静默失败」由生成器在入口消灭,比要求作者背规则可靠得多。
78
97
 
79
98
  `dictionary` 形状(每项字段齐全,`type` 空串会被 validate 拒):
80
99
 
@@ -109,7 +128,7 @@ IR 的常规写法是**一次 `fs_write` 写完整份**(最省步骤)。但
109
128
  三步走:
110
129
 
111
130
  1. **骨架**(小,一次 `fs_write`):顶层字段 + 根容器,容器数组留空
112
- `{"id":"<id>","name":"…","width":"375","children":[{"c":"Column","children":[]}]}`
131
+ `{"id":"<id>","name":"…","width":"<卡面宽>","children":[{"c":"Column","children":[]}]}`
113
132
  2. **一片一个步骤**:每片只放**一个区块**(一行 / 一组 tile / 一段自绘),`fs_write` 到
114
133
  `.cache/ai-scratch/<id>.p01.json`(名字随意,合并时按 `--part` 顺序应用):
115
134
  ```json
@@ -204,6 +223,71 @@ IR 的常规写法是**一次 `fs_write` 写完整份**(最省步骤)。但
204
223
  范式全文(适用场景、颜色/宽度绑定、存量样例)见 `recipe_snippets/title-underline-overlay.json` ——
205
224
  本节的片段与它同源,改动必须同步(`tools/tests/title-underline-recipe.test.mjs` 会校验两处一致)。
206
225
 
226
+ ---
227
+ ## 2e. 列表数组化与数据绑定完备(binding-* 门禁)
228
+
229
+ > 判定实现单一来源 `scripts/lib/rules/bindings.mjs`(lint 与 readiness 共享);本节是规则的权威文档锚点。
230
+
231
+ **设计稿里重复出现的数据行(同构行 / tile,值逐行不同)必须在视觉分析阶段就明确用数组绑定**——
232
+ S2 审计的 `listCandidates[].{fields,values}` 就是现成的字段与值矩阵。禁止把列表逐行静态展开:
233
+ 像素分可以很高,但数据维度缺失(列表数据变了视图不动),且行数错了没有任何门禁能兜住。
234
+
235
+ **三条门禁规则(2026-09-25)**:
236
+
237
+ | code | severity | 判定 |
238
+ | --- | --- | --- |
239
+ | `binding-repeater-no-scope` | **error** | ListLayout/GridLayout 的 value 没绑数组表达式(渲染 0 项),或有 value 但没有带 item 模板的 `slotScope` 插槽(渲染 N 个空壳) |
240
+ | `binding-array-unconsumed` | warn | data.json / inputParams 里的数组变量没有任何列表消费(复现器 value / slotScope 模板 / 事件代码 / 表达式都找不到引用)。名字带列表语义(list/items/rows/records/details/rules/entries)时 readiness 升级为 **blocker**(拦像素循环);Canvas 图表数据(radarData/donutData 类)可忽略 |
241
+ | `binding-list-static-expansion` | warn | 复现器外 ≥3 个结构同构的兄弟子树、全静态、且每行 ≥2 条文本且值逐行互异 → 疑似列表静态展开。评分星 / tab 标签 / 固定按钮组(单内容节点或值全同)是合法静态形态,不会被此规则命中 |
242
+
243
+ **合法消费途径(宽收口防误报)**:复现器 value 的 `$数组` 引用(含三元/切片复合式)、
244
+ slotScope 插槽内的 `$<字段>`(字段名 ∈ 数组元素键集合)、事件代码文本里的变量名
245
+ (`setData` 转造源属合法——monthlyBill 的 `$renderListData` 即此形态)、任意属性/样式/指令表达式引用。
246
+
247
+ **readiness 新增门**(内容就绪段,像素循环之前):
248
+ - `checks.bindings.arraysUnconsumed`:数组消费对账结果(hard = 列表语义未消费 ⇒ blocker)。
249
+ - `checks.lists`(传 `--audit` 时):S2 审计 `listCandidates` 提名 N 个列表、schema 里数组化列表数为 0 ⇒ **blocker**
250
+ (视觉分析已明确是列表,静态展开就是疏漏);提名 > 数组化只记 note(同构列表合并 / 提名假阳性合法)。
251
+
252
+ **全类型要素对账**(check_fidelity `elements.symbols` / `elements.shapes`,warning 级):
253
+ 设计稿 symbol 数 vs schema Icon 组件数、细长条 shape(min(w,h)≤2 且 max(w,h)≥40)数 vs Divider 组件数。
254
+ 缺口进迭代报告 `gate.elementHints` 供每轮自查(symbol 走 slice 位图实现也合法,故不硬拦)。
255
+
256
+ ---
257
+ ## 2f. 宽度自适应契约(占满语义禁写死 px,r66 门禁)
258
+
259
+ > 判定实现单一来源 `scripts/lib/rules/adaptive-width.mjs`(lint 消费);本节是规则的权威文档锚点。
260
+
261
+ **渲染契约(component-meta.json / components.css 实证)**:全部容器组件(Row/Column/MultiColumn/
262
+ ListLayout/GridLayout/AbsoluteLayout/ViewContainer)的 meta 默认 `rect.width` 就是 `"100%"`;
263
+ `adapter.doSize` 对 `%` 值原样透传;纵向流(Root/Column/AbsoluteLayout)里子项交叉轴默认 stretch。
264
+ 因此**容器写死 px 宽 = 显式覆盖默认的自适应行为**,宿主容器宽度一旦偏离设计基准,内容层锁死。
265
+
266
+ **三条铁律**:
267
+
268
+ 1. **顶层 `width` = 卡片本体设计宽(卡面宽),不是画板宽**。设计稿「画板 375 + 卡面两侧各 16 留白」
269
+ 时,IR 顶层写 `"width":"343"`,**不要**写 375 再给主容器定宽 343 复刻画板留白——留白是宿主布局
270
+ 职责,卡片只对卡面负责。
271
+ 2. **占满语义禁写死 px(r66,error)**:纵向流父级(Root/Column/AbsoluteLayout)下,容器子节点
272
+ rect.width 占满父内容盒(98%~103%)必须改 `width:"100%"`(或不写 width,meta 默认即 100%)。
273
+ 与设计宽逐像素等价(父内容盒宽 == 原 px 值),且随容器自适应。**真固定宽元素保留 px**:
274
+ 图标 / 头像 / 徽章 / 按钮 / 图表画布 / hug 宽标题壳。横向流(Row/MultiColumn)主轴里并列子项
275
+ 定宽是 space-between 等范式的合法组成,不受此限(R-FIXEDW / R-COMPARE)。
276
+ 3. **`width:"100%"` 禁叠水平 margin**(rules.md §十六 R63):100% 已等于父内容盒宽,再叠
277
+ marginLeft/Right 是往外框上加宽度 → 溢出被裁。水平内缩写**父容器 padding** 或改用小于 100% 的
278
+ 具体语义宽度。
279
+
280
+ **多行文本钉宽说明**(`textFromSpec`):多行文本 rect.width 钉 spec 框宽是为了换行折点与设计稿
281
+ 一致;当文本框宽 ≈ 父内容盒宽时应写 `100%`(折点在基准宽下不变、容器变宽时行自然跟随);
282
+ 窄于父内容盒(双列格 / 右侧留金额位)时保留 px——那是「局部定宽」语义,不属于占满。
283
+
284
+ **图表要素前置(步骤 2 下发 IR 前必须完成)**:要素盘点识别出图表(雷达 / 环形 / 仪表盘 / 柱状 /
285
+ 折线 / 散点)时,每个图表必须在 IR 下发前落定「范式来源」三件套,缺一视为要素盘点未完成:
286
+ ① `references/recipes.md` 对应图表范式章节(§13-§19);② 对应 `references/recipe_snippets/canvas-*.json`
287
+ 完整代码片段;③ `references/card_index.json` 近邻图表卡的 Canvas 实现作对照。
288
+ 图表节点一律 **Canvas + onMeasured 自绘**(数据绑定走 state 数组,范式里 dataContractNote 有契约),
289
+ **禁止**用 Text/Image 堆砌模拟图表形态——那是像素与 token 双输的垃圾结构。
290
+
207
291
  ---
208
292
  ## 3. 完整示例(list 卡)
209
293
 
@@ -11,7 +11,7 @@
11
11
  ## 2. 预算硬纪律(2026-09-21 起,硬性)
12
12
 
13
13
  - **每卡 iterate ≤ 15 轮**;连续 3 轮未刷新最好成绩(`stalled`)立即停止微调,按 §3 回退。
14
- - 触顶(轮数或 stalled)后**禁止**继续 --set 盲调、**禁止**全量重写 IR:走 `ir_restore.mjs --best` 回退,或带证据链(`diff_bands.mjs` + `generation_pitfalls.md` 对应规则号)换思路。
14
+ - 触顶(轮数或 stalled)后**禁止**继续 --set 盲调、**禁止**全量重写 IR:走 `ir_restore.mjs --best` 回退,或带证据链(`diff_bands.mjs` + 按 lint code 查 `scripts/lib/rules/registry.mjs` 登记的文档锚点)换思路。
15
15
  - `ir_merge` 分片每片多付一个 LLM 步骤(实测 40-140s),**小卡禁止拆分**;仅当单次输出被截断/写不进时用。
16
16
  - 改已存在 IR 优先 `ir_patch`(MCP)或 `iterate.mjs --set`,**一轮只改一个分项**(同时改多项无法归因)。
17
17
 
@@ -27,14 +27,33 @@ node scripts/ir_restore.mjs --round 10 --id <卡id> --ir .cache/ai-scratch/<卡i
27
27
 
28
28
  回退后必须重跑一轮 iterate 拿新报告(旧报告 schemaHash 已失效)。快照由 `iterate.mjs` 自动落:每轮 `.cache/ai-scratch/<id>.ir.rNN.json`,刷新最好成绩另存 `.ir.best.json`;报告持久化 `best:{match,round,irSnapshot}`(history 留最近 10 条,best 不被窗口挤掉);快照留最近 12 轮 + best,超出自动清理。
29
29
 
30
- ## 4. 要素线(先骨架、后样式,两段式门禁)
30
+ ## 4. 内容线(先内容、后骨架、最后样式 —— 三段式门禁)
31
31
 
32
- 设计稿里每条文案/每张切图都是**既定要素**,迭代只许改样式与布局,**不许丢要素、不许自造内容**。报告 `fidelity`(`total/matched/missing/invented/slices`)与 `gate.stage`:
32
+ ### 4.0 S4.9 内容就绪门禁(**像素循环的前置条件**)
33
33
 
34
+ `readiness.mjs` 做七道**声明性**判定(不需要渲染,所以挡在最贵的一步之前):**A** 组件契约(属性键/样式组/样式子键必须在元数据白名单内)· **B** 要素齐全(文本逐条对账;图标/分割线数量对账进 `gate.elementHints` 每轮自查)· **C** 变量绑定完备(`$var` 根变量必须真存在;列表语义数组零消费 = blocker)· **G** 列表提名闭环(`--audit` 时 listCandidates 提名了列表而 schema 数组化列表为 0 = blocker)· **D** 图片地址已配置 · **E** 切图路径已配置(引用在 `src/slices` 有对应文件)。数据绑定完备细则(数组消费对账 / 静态展开判据 / 合法消费途径)见 `ir-format.md` §2e。
35
+
36
+ ```bash
37
+ # 单跑(退出码 0=就绪 / 3=未就绪 / 1=用法错误;3 不是脚本报错)
38
+ # 退出码语义全域权威:skills/shared/exit-codes.md(本文件不再复述其它脚本的码)
39
+ node scripts/readiness.mjs --id <卡id> --spec <bundle>/spec.json --design <bundle>/card-N.png --json
40
+ ```
41
+
42
+ `iterate.mjs` **每轮自动跑这道门禁**,未就绪时:不 render、不 pixelDiff、**不消耗迭代轮次**(不写 IR 快照、不进 history),报告里 `gate.stage=content`、`gate.blocked=true`、`conclusion='blocked-content'`,`blockedAt.{stage,blockers,hint}` 给出可执行清单。补齐内容后**直接重跑 iterate**。
43
+
44
+ 为什么必须:实测(2026-09-24 gen-1)跳过这步,在 Image 的图片地址都没配(属性键写成 `image`,合法键只有 `value` ⇒ 运行时整条静默丢弃 ⇒ 破图)的情况下连刷 10 轮像素,`match` 卡在 87-90.46,两个回合 623 万 prompt token 全部作废。
45
+
46
+ 逃生门 `--no-readiness`(或 `DSL_CARD_SKIP_READINESS=1`)只在排查「门禁本身误判」时用。
47
+
48
+ ### 4.1 要素是固定真值
49
+
50
+ 设计稿里每条文案/每张切图都是**既定要素**,迭代只许改样式与布局,**不许丢要素、不许自造内容**——图标(symbol)与分割线(细长条 shape)同样算既定要素:报告 `fidelity.symbols/shapes` 的数量缺口会进 `gate.elementHints`(warning 级每轮自查;symbol 走 slice 位图实现也合法)。报告 `fidelity`(`total/matched/missing/invented/slices/symbols/shapes`)与 `gate.stage`:
51
+
52
+ - `gate.stage=content`(`gate.blocked=true`)⇒ 内容层没齐,**本轮根本没 render**。照 `gate.verdict` 里的 blockers 补内容,别调像素、别重跑空转。
34
53
  - `gate.stage=structure`(`elements.missing>0` 或 validate/lint/preflight 未过)⇒ **先补要素,别调像素**:把设计稿文案补回对应节点;绑定变量的把 `defaultValue` 改成设计稿真值(`check_fidelity.mjs` 的 `bindHints`)。改样式不会把丢掉的文案变回来。
35
- - `gate.stage=pixel` ⇒ 要素齐了才进入像素/布局收敛(一轮只改一个分项)。
54
+ - `gate.stage=pixel` ⇒ 内容与结构都齐了,才进入像素/布局收敛(一轮只改一个分项)。
36
55
 
37
- **最好成绩判据是字典序:先要素、后像素**(`best.elementsMissing` 一起落盘)。像素更高但要素更少的一轮**不算进步**、不存 `ir.best`。实测事故:模型把产品名/利率/期限整批换成自编占位值,`match` 照样刷到 89.56%、门禁全绿,内容真值整批丢失。`elements.invented` 会逐条点名自造文案;要素下滑时 `stalled.hint` 直接写明"这是在删要素凑像素"。
56
+ **最好成绩判据是加权分 `score = match − 6 × 缺要素数`(同量纲相加,`MISSING_PENALTY = 6`)**,不是字典序、也不是纯 `match`。三种判据的演进理由(前两版都在真轨迹上翻车)见 `iterate.mjs` 里 `MISSING_PENALTY` 的注释块;`best.score` 与 `isBetterThan` 必须同口径。像素更高但要素更少的一轮**不算进步**、不存 `ir.best`。实测事故:模型把产品名/利率/期限整批换成自编占位值,`match` 照样刷到 89.56%、门禁全绿,内容真值整批丢失。`elements.invented` 会逐条点名自造文案;要素下滑时 `stalled.hint` 直接写明"这是在删要素凑像素"。
38
57
 
39
58
  ## 5. 分项归因 contributors
40
59
 
@@ -50,7 +69,17 @@ flags 读法:
50
69
 
51
70
  `designH`/`dH` 为 null 表示没给 `--spec` 或文本匹配不上——只按 flags 与同类中位判断,**不要自己编设计稿高度**。
52
71
 
53
- 背景:实测 23 次 iterate 的报告只说「差 2.58 分、高度差 2px」,模型只能整份重写 IR 去试(16 次全量重写、12.4 万字符 ≈ 全部输出的一半)。`widthMismatch` 出现时先修 `--width`(宽度不对,match 与 dy 全都不可信)。
72
+ 背景:实测 23 次 iterate 的报告只说「差 2.58 分、高度差 2px」,模型只能整份重写 IR 去试(16 次全量重写、12.4 万字符 ≈ 全部输出的一半)。`widthMismatch` 出现时先修 `--width`(宽度不对,match 与 dy 全都不可信)——**它现在是 gate check(`render.widthMismatch`),会出现在 gate.failed 首列**,别再忽略。传 `--spec` 才有设计稿卡面宽基准:没传时宽度校验与 designH 都拿不到口径(gen-1 实测:--width 375 跑了两轮才被发觉,best 分数全是错基准产物)。
73
+
74
+ ## 5.5 结构性缺口(structuralGaps):先结构后微调
75
+
76
+ 报告 `pixelDiff.structuralGaps[]`(1x 卡内坐标)= 「逐行差异率 ≥85%」连续 ≥40pt 的带区:该带要么整块内容漏建(图表 / 分区 / 列表段),要么背景/底色整层错涂。`gate.structuralHint` 在 match 未达标且存在缺口时给出带区清单。
77
+
78
+ 用法:**缺口存在且 match<40 时,contributors / hotspots 的局部微调线索全部忽略**——那是「在 80% 缺失面前做 1% 微调」(gen-1 实测:雷达图整块漏建,模型照 contributors 调了 8 轮「10分/分」的 padding 后熔断)。修法:对照 spec_audit 的 `bands[]` 找到缺口带对应的元素群,把整块内容建出来(图表先查 `recipes.md` + `recipe_snippets/canvas-*.json` + `card_index` 近邻卡),或改对背景,缺口消失后再回到逐项微调。
79
+
80
+ ## 5.6 环境注意
81
+
82
+ - **Windows 环境无 jq**(实测 exit=127 连烧 3 步):读 audit / iterate JSON 用 `node -e "const a=require('./<path>.json'); ..."` 或 fs_read,不要用 jq。
54
83
 
55
84
  ## 6. IR 修改通道
56
85