cardbuddy 0.11.0 → 0.12.1

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 (313) hide show
  1. package/README.md +46 -5
  2. package/dist/cli.js +51 -1
  3. package/dist/config.js +51 -16
  4. package/dist/core.js +2 -2
  5. package/dist/init.js +425 -0
  6. package/dist/preview-editor/assets/index-CHuGGIR3.js +276 -0
  7. package/dist/preview-editor/assets/{index-B4VZgNSK.css → index-CQScMhyg.css} +1 -1
  8. package/dist/preview-editor/index.html +2 -2
  9. package/dist/standalone.js +8 -0
  10. package/dist/vendored/mcp/client.mjs +537 -0
  11. package/dist/vendored/mcp/tools.mjs +268 -0
  12. package/dist/vendored/preview/assets/default-image.png +0 -0
  13. package/dist/{server-core → vendored/server/core}/ai-chat.js +82 -38
  14. package/dist/{server-core → vendored/server/core}/browser.js +1 -1
  15. package/dist/{server-core → vendored/server/core}/cardSave.js +8 -3
  16. package/dist/{server-core → vendored/server/core}/cardSync.js +6 -14
  17. package/dist/{server-core → vendored/server/core}/cardValidate.js +11 -8
  18. package/dist/{server-core → vendored/server/core}/index.js +5 -3
  19. package/dist/vendored/server/core/skillPaths.js +124 -0
  20. package/dist/{server-core → vendored/server/core}/sliceUpload.js +5 -8
  21. package/dist/vendored/skills/README.md +93 -0
  22. package/dist/vendored/skills/card-doctor/SKILL.md +52 -0
  23. package/dist/vendored/skills/card-skill-trainer/SKILL.md +312 -0
  24. package/dist/vendored/skills/card-skill-trainer/baselines/v3-r038n.json +43 -0
  25. package/dist/vendored/skills/card-skill-trainer/baselines/v3-r039n.json +47 -0
  26. package/dist/vendored/skills/card-skill-trainer/baselines/v3-r050n.json +48 -0
  27. package/dist/vendored/skills/card-skill-trainer/scripts/build_dataset.py +472 -0
  28. package/dist/vendored/skills/card-skill-trainer/scripts/build_sample.mjs +162 -0
  29. package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +241 -0
  30. package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +525 -0
  31. package/dist/vendored/skills/card-skill-trainer/scripts/round_diff.mjs +133 -0
  32. package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +1043 -0
  33. package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +195 -0
  34. package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +122 -0
  35. package/dist/vendored/skills/dsl-card-ai/SKILL.md +191 -0
  36. package/dist/vendored/skills/dsl-card-generator/SKILL.md +480 -0
  37. package/dist/vendored/skills/dsl-card-generator/config/render-contract.json +11 -0
  38. package/dist/vendored/skills/dsl-card-generator/references/amount_formatting.md +138 -0
  39. package/dist/vendored/skills/dsl-card-generator/references/card_index.json +1597 -0
  40. package/dist/vendored/skills/dsl-card-generator/references/card_templates.json +384 -0
  41. package/dist/vendored/skills/dsl-card-generator/references/card_to_summary.md +56 -0
  42. package/dist/vendored/skills/dsl-card-generator/references/component_rules.md +76 -0
  43. package/dist/vendored/skills/dsl-card-generator/references/component_specs.md +393 -0
  44. package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +208 -0
  45. package/dist/vendored/skills/dsl-card-generator/references/event_patterns.md +287 -0
  46. package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +121 -0
  47. package/dist/vendored/skills/dsl-card-generator/references/field_name_dict.json +34 -0
  48. package/dist/vendored/skills/dsl-card-generator/references/field_naming.md +101 -0
  49. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +928 -0
  50. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/README.md +69 -0
  51. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/examples_index.json +2967 -0
  52. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/patterns.json +2835 -0
  53. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/recipes_v2.json +648 -0
  54. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/recipes_v2_schema.md +295 -0
  55. package/dist/vendored/skills/dsl-card-generator/references/image_diff.md +48 -0
  56. package/dist/vendored/skills/dsl-card-generator/references/image_to_card.md +129 -0
  57. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +212 -0
  58. package/dist/vendored/skills/dsl-card-generator/references/message-center-pattern.md +149 -0
  59. package/dist/vendored/skills/dsl-card-generator/references/platform_jsapi.md +196 -0
  60. package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +54 -0
  61. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +197 -0
  62. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/absolute-badge.json +84 -0
  63. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/accordion-group-toggle.json +18 -0
  64. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/alertCard-pair.json +12 -0
  65. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/alertDialog-call.json +12 -0
  66. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-countdown.json +15 -0
  67. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-donut.json +15 -0
  68. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-gauge-score.json +11 -0
  69. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-radar.json +10 -0
  70. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-title-head.json +13 -0
  71. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/globalSet.json +12 -0
  72. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/label-col-modes.json +21 -0
  73. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/list-expand-collapse.json +17 -0
  74. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/money-format.json +11 -0
  75. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/opacity-switch.json +130 -0
  76. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/progress-bar.json +25 -0
  77. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/tab-billMode.json +15 -0
  78. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +21 -0
  79. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +797 -0
  80. package/dist/vendored/skills/dsl-card-generator/references/rules.md +235 -0
  81. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +177 -0
  82. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +609 -0
  83. package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +131 -0
  84. package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +118 -0
  85. package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +153 -0
  86. package/dist/vendored/skills/dsl-card-generator/scripts/apply_opacity_pattern.mjs +343 -0
  87. package/dist/vendored/skills/dsl-card-generator/scripts/build_card_index.mjs +108 -0
  88. package/dist/vendored/skills/dsl-card-generator/scripts/build_card_templates.mjs +190 -0
  89. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +257 -0
  90. package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +79 -0
  91. package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +98 -0
  92. package/dist/vendored/skills/dsl-card-generator/scripts/describe_card.mjs +265 -0
  93. package/dist/vendored/skills/dsl-card-generator/scripts/diff_bands.mjs +257 -0
  94. package/dist/vendored/skills/dsl-card-generator/scripts/diff_card.mjs +316 -0
  95. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +873 -0
  96. package/dist/vendored/skills/dsl-card-generator/scripts/extract_handcrafted.py +1199 -0
  97. package/dist/vendored/skills/dsl-card-generator/scripts/extract_subtree.mjs +191 -0
  98. package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +389 -0
  99. package/dist/vendored/skills/dsl-card-generator/scripts/inventory_schemas.py +160 -0
  100. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +700 -0
  101. package/dist/vendored/skills/dsl-card-generator/scripts/lib/design_image.mjs +56 -0
  102. package/dist/vendored/skills/dsl-card-generator/scripts/lib/design_sample.mjs +328 -0
  103. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +79 -0
  104. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +103 -0
  105. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scaffold.mjs +575 -0
  106. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +81 -0
  107. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +147 -0
  108. package/dist/vendored/skills/dsl-card-generator/scripts/lint_autofix.mjs +363 -0
  109. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +1399 -0
  110. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +249 -0
  111. package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +208 -0
  112. package/dist/vendored/skills/dsl-card-generator/scripts/render_baseline.mjs +231 -0
  113. package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +801 -0
  114. package/dist/vendored/skills/dsl-card-generator/scripts/render_diff.mjs +239 -0
  115. package/dist/vendored/skills/dsl-card-generator/scripts/resolve_card_template.mjs +73 -0
  116. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +447 -0
  117. package/dist/vendored/skills/dsl-card-generator/scripts/scaffold_card.mjs +81 -0
  118. package/dist/vendored/skills/dsl-card-generator/scripts/slices_normalize.mjs +316 -0
  119. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +462 -0
  120. package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +7735 -0
  121. package/dist/vendored/skills/dsl-card-generator/scripts/spec_tree.mjs +355 -0
  122. package/dist/vendored/skills/dsl-card-generator/scripts/validate_all_schemas.py +71 -0
  123. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/default-image-wzZYQ86K.png +0 -0
  124. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-C9k-07y7.css +1 -0
  125. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-oFRM4f4V.js +55 -0
  126. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/svg-symbols-B2exBrAr.svg +118 -0
  127. package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/component-meta.json +2714 -0
  128. package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/directive-meta.json +40 -0
  129. package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/style-meta.json +1086 -0
  130. package/dist/vendored/{standalone.html → skills/dsl-card-generator/vendor/preview/standalone.html} +2 -1
  131. package/dist/vendored/skills/finmall-card-sync/SKILL.md +262 -0
  132. package/dist/vendored/skills/finmall-session-navigator/SKILL.md +169 -0
  133. package/dist/vendored/skills/finmall-session-navigator/scripts/finmall_login.mjs +882 -0
  134. package/dist/vendored/skills/finmall-session-navigator/scripts/package-lock.json +59 -0
  135. package/dist/vendored/skills/finmall-session-navigator/scripts/package.json +10 -0
  136. package/dist/vendored/skills/finmall-session-navigator/scripts/session-manager.mjs +96 -0
  137. package/dist/vendored/skills/shared/README.md +55 -0
  138. package/dist/vendored/skills/shared/auth-model.md +79 -0
  139. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +329 -0
  140. package/dist/vendored/skills/sketch-spec-to-card/references/boundary.json +14 -0
  141. package/dist/vendored/skills/sketch-spec-to-card/references/card_refs.json +13 -0
  142. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +103 -0
  143. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +797 -0
  144. package/dist/vendored/skills/sketch-spec-to-card/scripts/card_refs.py +94 -0
  145. package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +706 -0
  146. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +1781 -0
  147. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +318 -0
  148. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +1118 -0
  149. package/dist/vendored/templates/helloCard/data.json +10 -0
  150. package/dist/vendored/templates/helloCard/schema.json +238 -0
  151. package/dist/vendored/tools/link-skills.mjs +485 -0
  152. package/package.json +2 -1
  153. package/dist/preview-editor/assets/index-jl4YJzKv.js +0 -180
  154. package/dist/vendored/App.vue +0 -2858
  155. package/dist/vendored/ansi.js +0 -185
  156. package/dist/vendored/assets/apple-touch-icon.png +0 -0
  157. package/dist/vendored/assets/default-image.png +0 -0
  158. package/dist/vendored/assets/favicon-16.png +0 -0
  159. package/dist/vendored/assets/favicon-32.png +0 -0
  160. package/dist/vendored/assets/site-icon.png +0 -0
  161. package/dist/vendored/assets/svgIcons/icons.svg +0 -87
  162. package/dist/vendored/assets/svgIcons/index.js +0 -39
  163. package/dist/vendored/components/AiSettings.vue +0 -392
  164. package/dist/vendored/components/AlertCardInstance.vue +0 -58
  165. package/dist/vendored/components/AlertCardLayer.vue +0 -59
  166. package/dist/vendored/components/ApiCallPanel.vue +0 -127
  167. package/dist/vendored/components/ApiPayloadTree.vue +0 -201
  168. package/dist/vendored/components/AppToast.vue +0 -37
  169. package/dist/vendored/components/CardGallery.vue +0 -291
  170. package/dist/vendored/components/CardPreview.vue +0 -72
  171. package/dist/vendored/components/CardSelect.vue +0 -206
  172. package/dist/vendored/components/ChatComposer.vue +0 -413
  173. package/dist/vendored/components/ChatMessage.vue +0 -383
  174. package/dist/vendored/components/ChatPanel.vue +0 -937
  175. package/dist/vendored/components/ComponentPalette.vue +0 -301
  176. package/dist/vendored/components/ConfirmDialog.vue +0 -54
  177. package/dist/vendored/components/DatePickerOverlay.vue +0 -345
  178. package/dist/vendored/components/DebugPanel.vue +0 -1734
  179. package/dist/vendored/components/DeviceOverlay.vue +0 -70
  180. package/dist/vendored/components/Dropdown.vue +0 -193
  181. package/dist/vendored/components/EventEditor.vue +0 -273
  182. package/dist/vendored/components/HoverGuides.vue +0 -122
  183. package/dist/vendored/components/JsonTree.vue +0 -547
  184. package/dist/vendored/components/LogValue.vue +0 -52
  185. package/dist/vendored/components/MarkdownBody.vue +0 -64
  186. package/dist/vendored/components/NewCardDialog.vue +0 -132
  187. package/dist/vendored/components/NodeInspector.vue +0 -295
  188. package/dist/vendored/components/NumberStepper.vue +0 -125
  189. package/dist/vendored/components/PickerOverlay.vue +0 -131
  190. package/dist/vendored/components/PropControl.vue +0 -245
  191. package/dist/vendored/components/PropTree.vue +0 -128
  192. package/dist/vendored/components/RemotePreview.vue +0 -173
  193. package/dist/vendored/components/SelectionOverlay.vue +0 -105
  194. package/dist/vendored/components/StructureTree.vue +0 -1002
  195. package/dist/vendored/components/Switch.vue +0 -38
  196. package/dist/vendored/components/setters/SetBgPosition.vue +0 -64
  197. package/dist/vendored/components/setters/SetBgSize.vue +0 -90
  198. package/dist/vendored/components/setters/SetCode.vue +0 -198
  199. package/dist/vendored/components/setters/SetColor.vue +0 -244
  200. package/dist/vendored/components/setters/SetCustomSelect.vue +0 -58
  201. package/dist/vendored/components/setters/SetExpr.vue +0 -107
  202. package/dist/vendored/components/setters/SetGradient.vue +0 -125
  203. package/dist/vendored/components/setters/SetGrid9.vue +0 -68
  204. package/dist/vendored/components/setters/SetIconSelect.vue +0 -67
  205. package/dist/vendored/components/setters/SetIfForm.vue +0 -61
  206. package/dist/vendored/components/setters/SetImage.vue +0 -43
  207. package/dist/vendored/components/setters/SetList.vue +0 -139
  208. package/dist/vendored/components/setters/SetNumber.vue +0 -118
  209. package/dist/vendored/components/setters/SetNumberStyle.vue +0 -113
  210. package/dist/vendored/components/setters/SetPadding.vue +0 -77
  211. package/dist/vendored/components/setters/SetSegment.vue +0 -160
  212. package/dist/vendored/components/setters/SetSelect.vue +0 -51
  213. package/dist/vendored/components/setters/SetSwitch.vue +0 -49
  214. package/dist/vendored/components/setters/SetText.vue +0 -52
  215. package/dist/vendored/components/setters/props.js +0 -23
  216. package/dist/vendored/fidelity/baseline-messageOverview.json +0 -87
  217. package/dist/vendored/fidelity/calibration.json +0 -29
  218. package/dist/vendored/fidelity/fidelity-messageOverview-after.html +0 -114
  219. package/dist/vendored/fidelity/fidelity-messageOverview-after.json +0 -272
  220. package/dist/vendored/fidelity/fidelity-messageOverview.html +0 -185
  221. package/dist/vendored/fidelity/fidelity-messageOverview.json +0 -336
  222. package/dist/vendored/fidelity/fidelity-messageOverview_v2.html +0 -262
  223. package/dist/vendored/fidelity/fidelity-messageOverview_v2.json +0 -502
  224. package/dist/vendored/fidelity/fidelity-messageOverview_v3.html +0 -201
  225. package/dist/vendored/fidelity/fidelity-messageOverview_v3.json +0 -417
  226. package/dist/vendored/fidelity/fidelity-messageOverview_visual.html +0 -125
  227. package/dist/vendored/fidelity/fidelity-messageOverview_visual.json +0 -338
  228. package/dist/vendored/index.html +0 -16
  229. package/dist/vendored/main.js +0 -37
  230. package/dist/vendored/runtime/ai-api.js +0 -165
  231. package/dist/vendored/runtime/alert-card.js +0 -125
  232. package/dist/vendored/runtime/app-toast.js +0 -33
  233. package/dist/vendored/runtime/clipboard-node.js +0 -78
  234. package/dist/vendored/runtime/clipboard.js +0 -29
  235. package/dist/vendored/runtime/event-model.js +0 -300
  236. package/dist/vendored/runtime/hoverGuide.js +0 -31
  237. package/dist/vendored/runtime/jsapi-mock.js +0 -256
  238. package/dist/vendored/runtime/markdown.js +0 -140
  239. package/dist/vendored/runtime/node-model.js +0 -217
  240. package/dist/vendored/skills/.manifest.json +0 -1101
  241. package/dist/vendored/standalone-main.js +0 -36
  242. package/dist/vendored/styles/ai-chat.css +0 -1850
  243. package/dist/vendored/styles/app.css +0 -3648
  244. package/dist/vendored/styles/card-gallery.css +0 -285
  245. package/dist/vendored/styles/card-select.css +0 -288
  246. package/dist/vendored/styles/components.css +0 -383
  247. package/dist/vendored/styles/dropdown.css +0 -324
  248. package/dist/vendored/styles/json-tree.css +0 -308
  249. package/dist/vendored/styles/node-inspector.css +0 -800
  250. package/dist/vendored/styles/setters.css +0 -1050
  251. package/dist/vendored/styles/standalone.css +0 -27
  252. package/dist/vendored/styles/structure-tree.css +0 -460
  253. /package/dist/vendored/{api.js → preview/api.js} +0 -0
  254. /package/dist/vendored/{assets → preview/assets}/svg-symbols.svg +0 -0
  255. /package/dist/vendored/{components → preview/components}/AbsoluteLayout.vue +0 -0
  256. /package/dist/vendored/{components → preview/components}/Button.vue +0 -0
  257. /package/dist/vendored/{components → preview/components}/Canvas.vue +0 -0
  258. /package/dist/vendored/{components → preview/components}/CheckGroup.vue +0 -0
  259. /package/dist/vendored/{components → preview/components}/Column.vue +0 -0
  260. /package/dist/vendored/{components → preview/components}/CustomPlaceholder.vue +0 -0
  261. /package/dist/vendored/{components → preview/components}/Divider.vue +0 -0
  262. /package/dist/vendored/{components → preview/components}/GridLayout.vue +0 -0
  263. /package/dist/vendored/{components → preview/components}/Icon.vue +0 -0
  264. /package/dist/vendored/{components → preview/components}/Image.vue +0 -0
  265. /package/dist/vendored/{components → preview/components}/Input.vue +0 -0
  266. /package/dist/vendored/{components → preview/components}/ListLayout.vue +0 -0
  267. /package/dist/vendored/{components → preview/components}/Map.vue +0 -0
  268. /package/dist/vendored/{components → preview/components}/MultiColumn.vue +0 -0
  269. /package/dist/vendored/{components → preview/components}/MultiText.vue +0 -0
  270. /package/dist/vendored/{components → preview/components}/ProgressSlider.vue +0 -0
  271. /package/dist/vendored/{components → preview/components}/RadioButton.vue +0 -0
  272. /package/dist/vendored/{components → preview/components}/RenderTree.vue +0 -0
  273. /package/dist/vendored/{components → preview/components}/Root.vue +0 -0
  274. /package/dist/vendored/{components → preview/components}/Row.vue +0 -0
  275. /package/dist/vendored/{components → preview/components}/SvgCode.vue +0 -0
  276. /package/dist/vendored/{components → preview/components}/SvgSymbols.vue +0 -0
  277. /package/dist/vendored/{components → preview/components}/Text.vue +0 -0
  278. /package/dist/vendored/{components → preview/components}/Video.vue +0 -0
  279. /package/dist/vendored/{components → preview/components}/ViewContainer.vue +0 -0
  280. /package/dist/vendored/{components → preview/components}/base.js +0 -0
  281. /package/dist/vendored/{components → preview/components}/icons.js +0 -0
  282. /package/dist/vendored/{components → preview/components}/index.js +0 -0
  283. /package/dist/vendored/{meta → preview/meta}/component-meta.json +0 -0
  284. /package/dist/vendored/{meta → preview/meta}/directive-meta.json +0 -0
  285. /package/dist/vendored/{meta → preview/meta}/style-meta.json +0 -0
  286. /package/dist/vendored/{runtime → preview/runtime}/StyleParser.js +0 -0
  287. /package/dist/vendored/{runtime → preview/runtime}/adapter.js +0 -0
  288. /package/dist/vendored/{runtime → preview/runtime}/assets.js +0 -0
  289. /package/dist/vendored/{runtime → preview/runtime}/canvas-context.js +0 -0
  290. /package/dist/vendored/{runtime → preview/runtime}/card-runtime.js +0 -0
  291. /package/dist/vendored/{runtime → preview/runtime}/evaluate.js +0 -0
  292. /package/dist/vendored/{runtime → preview/runtime}/property.js +0 -0
  293. /package/dist/vendored/{runtime → preview/runtime}/selectionOverlay.js +0 -0
  294. /package/dist/vendored/{runtime → preview/runtime}/toPreview.js +0 -0
  295. /package/dist/{server-core → vendored/server/core}/assetProxy.js +0 -0
  296. /package/dist/{server-core → vendored/server/core}/bbox.js +0 -0
  297. /package/dist/{server-core → vendored/server/core}/buildDataFromSchema.js +0 -0
  298. /package/dist/{server-core → vendored/server/core}/cardFiles.js +0 -0
  299. /package/dist/{server-core → vendored/server/core}/cardHistory.js +0 -0
  300. /package/dist/{server-core → vendored/server/core}/cardImage.js +0 -0
  301. /package/dist/{server-core → vendored/server/core}/cardImportMeta.js +0 -0
  302. /package/dist/{server-core → vendored/server/core}/cardListCache.js +0 -0
  303. /package/dist/{server-core → vendored/server/core}/cardOpenFolder.js +0 -0
  304. /package/dist/{server-core → vendored/server/core}/cardSidecar.js +0 -0
  305. /package/dist/{server-core → vendored/server/core}/codeBridge.js +0 -0
  306. /package/dist/{server-core → vendored/server/core}/logStream.js +0 -0
  307. /package/dist/{server-core → vendored/server/core}/logger.js +0 -0
  308. /package/dist/{server-core → vendored/server/core}/nodeModels.js +0 -0
  309. /package/dist/{server-core → vendored/server/core}/renderStatus.js +0 -0
  310. /package/dist/{server-core → vendored/server/core}/screenshot.js +0 -0
  311. /package/dist/{server-core → vendored/server/core}/sliceStatic.js +0 -0
  312. /package/dist/vendored/{meta → skills/dsl-card-generator/vendor/preview/meta}/action-meta.json +0 -0
  313. /package/dist/{server-core/_vendor → vendored/tools}/finmall-env.mjs +0 -0
@@ -0,0 +1,1399 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint_card.mjs — 卡片健康度体检(结构合法 ≠ 体验达标)。
4
+ *
5
+ * 与 validate_schema.py 的边界:
6
+ * - validate 管"结构合法、能不能渲染"(CI 硬卡点)。
7
+ * - lint 管"达不达标、会不会让用户看到怪东西"(软提醒)。
8
+ * - 一张卡可以结构完全合法(validate 0 ERROR)但仍有体验问题(lint 报警)。
9
+ *
10
+ * 检查项:
11
+ * 1. 脚手架前缀残留:data.json / 表达式里出现 scaffold 自动生成的占位文本("示例xxx")。
12
+ * 2. 未使用的 inputParams:声明了 $key 但 schema 里没引用(数据字典膨胀)。
13
+ * 3. Button 缺 click:按钮节点没有 click 事件(按钮不能点是常见 bug)。
14
+ * 4. 根节点缺 background:Root.style 没有 background 组(透明卡用户会困惑)。
15
+ * 5. Text 缺 font.color:Text 节点没有 font.color(深色主题下看不见)。
16
+ * 6. Canvas 缺 onMeasured:Canvas 没有 onMeasured 等于画不出来。
17
+ * 7. 空文本节点:Text.value 绑的 $key 在 data.json 是空字符串且无 placeholder。
18
+ * 8. eventKey 与组件类型不匹配:如 Text 接 onMeasured、Canvas 接 click 等。
19
+ * 9. style/properties/directives 缺 itemValueFrom:缺失或空串会被预览器静默丢弃(背景/内边距/对齐失效),与 validate_schema.py 同源 ERROR。
20
+ * 10. style 子项 itemId 错配:子项 itemId 与该组(参考 vendor/preview/meta/style-meta.json)预期不一致(如 flexDirection 误用 002 而非 006),与 validate_schema.py 同源 ERROR。
21
+ * 11. 标题+下划线拓扑:AbsoluteLayout 必须显式 BottomLeft,装饰条先于标题绘制,宽度建议绑定标题长度表达式。
22
+ * 12. 切图命名语义化(r59):引用名沿用设计稿图层名/导出目录前缀/中文名 → 看不出用途,必须按功能重命名。
23
+ * 13. 切图同内容重复(r60):src/slices 里存在 sha256 完全相同的副本 → 应复用同一份,不重复导入/上传。
24
+ * 14. 静态文案 / 字典默认值含疑似运行期数字(r61,提醒级):千分号数字 / ¥+数字 / 数字+单位词写死在静态 Text 或 defaultValue 里 → 提示按 rules.md §十四 做 AI 语义复核(运行期数字应抽变量 + 表达式拼接);固定话术误报可忽略。
25
+ *
26
+ * 用法:
27
+ * node lint_card.mjs --id <cardId> # 体检单张卡
28
+ * node lint_card.mjs --all # 体检所有卡
29
+ * node lint_card.mjs --id <cardId> --json # 机器可读输出
30
+ * node lint_card.mjs --id <cardId> --strict # 有 lint 报警 exit 1(CI 可选)
31
+ *
32
+ * 退出码:
33
+ * - 0:无 lint 报警(或未启用 --strict)
34
+ * - 1:有 lint 报警且 --strict
35
+ * - 2:脚本异常(卡不存在、JSON 解析失败等)
36
+ */
37
+ import fs from 'node:fs'
38
+ import path from 'node:path'
39
+
40
+ import { fileURLToPath } from 'node:url'
41
+ import { cardsDir, cardSchemaPath, cardDataPath, readJson, metaDir, slicesDir } from './lib/paths.mjs'
42
+ import { buildContentIndex, checkSliceName, collectSliceRefStrings, duplicatePeers, sliceNameFromRef } from './lib/slice_dedupe.mjs'
43
+
44
+ // src/slices 内容索引(sha256 → 文件名)进程内只建一次,--all 体检全量卡时复用
45
+ let SLICE_CONTENT_INDEX = null
46
+ function sliceContentIndex() {
47
+ if (!SLICE_CONTENT_INDEX) SLICE_CONTENT_INDEX = buildContentIndex(slicesDir)
48
+ return SLICE_CONTENT_INDEX
49
+ }
50
+
51
+ const RENDER_CONTRACT = JSON.parse(fs.readFileSync(
52
+ path.join(metaDir, '../../../config/render-contract.json'),
53
+ 'utf8',
54
+ ))
55
+
56
+ // Group 指令(如 v-show):必须 itemValueFrom:"0" + itemChildren,裸字符串/“1” 会被渲染器 onPreview 静默丢弃
57
+ const DM = JSON.parse(fs.readFileSync(path.join(metaDir, 'directive-meta.json'), 'utf8'))
58
+ const GROUP_DIRECTIVES = new Set((DM.properties || []).filter((d) => (d.itemChildren || []).length > 0).map((d) => d.itemKey))
59
+
60
+ /* ------------------------------------------------------------------ */
61
+ /* 参数解析 */
62
+ /* ------------------------------------------------------------------ */
63
+ function parseArgs(argv) {
64
+ const out = { id: '', all: false, json: false, strict: false }
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const a = argv[i]
67
+ if (a === '--id') out.id = argv[++i]
68
+ else if (a === '--all') out.all = true
69
+ else if (a === '--json') out.json = true
70
+ else if (a === '--strict') out.strict = true
71
+ else if (a === '-h' || a === '--help') {
72
+ console.log(`Usage: node lint_card.mjs [options]
73
+
74
+ Options:
75
+ --id <cardId> 体检单张卡
76
+ --all 体检 src/cards 下所有卡
77
+ --json 机器可读 JSON 输出
78
+ --strict 有 lint 报警时 exit 1(CI 用)
79
+ -h, --help 显示本帮助`)
80
+ process.exit(0)
81
+ }
82
+ }
83
+ if (!out.id && !out.all) {
84
+ console.error('[error] 必须指定 --id <cardId> 或 --all')
85
+ process.exit(1)
86
+ }
87
+ return out
88
+ }
89
+
90
+ /* ------------------------------------------------------------------ */
91
+ /* 节点遍历 */
92
+ /* ------------------------------------------------------------------ */
93
+ function walk(node, visitor, parent = null) {
94
+ if (!node || typeof node !== 'object') return
95
+ visitor(node, parent)
96
+ if (Array.isArray(node.components)) for (const c of node.components) walk(c, visitor, node)
97
+ for (const s of node.slots || []) for (const c of s.components || []) walk(c, visitor, node)
98
+ }
99
+
100
+ /** 获取默认插槽直接子节点(兼容 Root.components 与普通节点 slots[0].components) */
101
+ function directChildren(node) {
102
+ if (!node) return []
103
+ const slot = (node.slots || [])[0]
104
+ if (slot && Array.isArray(slot.components)) return slot.components
105
+ return Array.isArray(node.components) ? node.components : []
106
+ }
107
+
108
+ /**
109
+ * 收集所有被引用的数据字典 key(粗扫,不过滤 slotScope):
110
+ * 1. 表达式里的 `$key`
111
+ * 2. customAction 脚本里的 `<别名>.state.key` / `<别名>.data.key`
112
+ * 别名 = this / self / that / _this(Canvas 自绘普遍用 self.state.x,
113
+ * 见 previewer_contract.md;闭包里常用 that/_this)
114
+ * 注意不要加 \b 前缀断言:脚本经 JSON.stringify 后换行变成字面量 "\n",
115
+ * `n` 会紧贴 this 导致 \b 失效而漏匹配。
116
+ * —— R51 之后运行时变量只在事件脚本里以 state 读取,view 表达式扫不到,
117
+ * 不补这一路会让「仅事件内使用」的 inputParams 被误报 unused-input-param。
118
+ */
119
+ function collectBindings(schema) {
120
+ const used = new Set()
121
+ const text = JSON.stringify(schema.view || {})
122
+ // 统一扫:$ 后跟合法标识符
123
+ const re = /\$([A-Za-z_$][\w$]*)/g
124
+ let m
125
+ while ((m = re.exec(text))) used.add(m[1])
126
+ // 事件脚本内的 this/self/that/_this 的 .state.x / .data.x
127
+ const reState = /(?:this|self|that|_this)\.(?:state|data)\.([A-Za-z_$][\w$]*)/g
128
+ while ((m = reState.exec(text))) used.add(m[1])
129
+ return used
130
+ }
131
+
132
+ /* ------------------------------------------------------------------ */
133
+ /* style-meta 派生:groupKey -> { childKey: childItemId } */
134
+ /* (与 validate_schema.py::load_style_maps 同源;捕捉 flexDirection */
135
+ /* 误用 002 而非 006 这类「itemId 存在但放错组」的隐形 bug) */
136
+ /* ------------------------------------------------------------------ */
137
+ let _styleGroupChildMap = null
138
+ function loadStyleMaps() {
139
+ if (_styleGroupChildMap) return _styleGroupChildMap
140
+ const map = {}
141
+ const sm = path.join(metaDir, 'style-meta.json')
142
+ try {
143
+ const data = readJson(sm, null)
144
+ if (data && Array.isArray(data.properties)) {
145
+ for (const grp of data.properties) {
146
+ const gk = grp.itemKey
147
+ if (!gk) continue
148
+ const cm = {}
149
+ for (const c of grp.itemChildren || []) {
150
+ const ck = c.itemKey
151
+ const cid = c.itemId
152
+ if (ck != null && cid != null) cm[String(ck)] = String(cid)
153
+ }
154
+ map[String(gk)] = cm
155
+ }
156
+ }
157
+ } catch (e) {
158
+ // style-meta 解析失败则跳过 itemId 校验,不阻塞 lint
159
+ }
160
+ _styleGroupChildMap = map
161
+ return map
162
+ }
163
+
164
+ /**
165
+ * 校验单节点的「itemValueFrom 必填」与「style 子项 itemId 一致性」。
166
+ * 这两类问题在预览器里是静默失效(属性被丢弃 / 被错填到其它属性),
167
+ * validate_schema.py 已把它们设为 ERROR;lint 这里再报一遍,方便体检时一并发现。
168
+ */
169
+ function checkStyleIntegrity(node, cid, out) {
170
+ const styleMaps = loadStyleMaps()
171
+
172
+ // style 子项
173
+ const styleProps = (node.style && node.style.properties) || []
174
+ for (const grp of styleProps) {
175
+ const gk = grp.itemKey
176
+ const children = (grp.itemChildren && grp.itemChildren.properties) || []
177
+ const expectMap = styleMaps[gk]
178
+ for (const child of children) {
179
+ // 9. 缺失/空 itemValueFrom
180
+ const vf = child.itemValueFrom
181
+ if (vf === undefined || vf === null || vf === '') {
182
+ out.push({
183
+ severity: 'error',
184
+ code: 'missing-itemValueFrom',
185
+ componentId: cid,
186
+ message: `style.${gk}.${child.itemKey || '?'} 缺 itemValueFrom —— 预览时该属性会被静默丢弃(背景/内边距/对齐失效),用 autofix_schema.py 可一键补 "0"`,
187
+ })
188
+ }
189
+ // 10. style 子项 itemId 错配
190
+ if (expectMap && child.itemKey != null) {
191
+ const expect = expectMap[String(child.itemKey)]
192
+ const got = child.itemId != null ? String(child.itemId) : undefined
193
+ if (expect && got !== expect) {
194
+ out.push({
195
+ severity: 'error',
196
+ code: 'style-itemId-mismatch',
197
+ componentId: cid,
198
+ message: `style.${gk}.${child.itemKey} 用 itemId "${got}",但该组预期 "${expect}"(如 flexDirection 应 006 而非 002)—— 会被错填到其它属性`,
199
+ })
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ // component properties
206
+ for (const p of node.properties || []) {
207
+ const vf = p.itemValueFrom
208
+ if (vf === undefined || vf === null || vf === '') {
209
+ out.push({
210
+ severity: 'error',
211
+ code: 'missing-itemValueFrom',
212
+ componentId: cid,
213
+ message: `property ${p.itemKey || '?'} 缺 itemValueFrom —— 预览时该属性会被静默丢弃,用 autofix_schema.py 可一键补 "0"`,
214
+ })
215
+ }
216
+ }
217
+
218
+ // directives
219
+ for (const d of node.directives || []) {
220
+ const vf = d.itemValueFrom
221
+ if (vf === undefined || vf === null || vf === '') {
222
+ out.push({
223
+ severity: 'error',
224
+ code: 'missing-itemValueFrom',
225
+ componentId: cid,
226
+ message: `directive ${d.itemKey || '?'} 缺 itemValueFrom —— 预览时该指令会被静默丢弃,用 autofix_schema.py 可一键补 "0"`,
227
+ })
228
+ } else if (GROUP_DIRECTIVES.has(d.itemKey) && String(vf) !== '0') {
229
+ // Group 指令(v-show 等)误用 itemValueFrom:"1"(裸表达式字符串形式)—— onPreview 会丢弃其 itemChildren 条件,指令失效
230
+ out.push({
231
+ severity: 'error',
232
+ code: 'group-directive-bad-form',
233
+ componentId: cid,
234
+ message: `directive ${d.itemKey} 是 Group 指令,必须 itemValueFrom:"0" 且带 itemChildren(showType/condition);当前 itemValueFrom:"${vf}" 会被渲染器 onPreview 静默丢弃条件,指令永远生效/失效。修正:改为对象形式 d:{ "${d.itemKey}": { showType:"01", condition:{ expression:"..." } } }`,
235
+ })
236
+ }
237
+ }
238
+ }
239
+
240
+ /* ------------------------------------------------------------------ */
241
+ /* 检查规则 */
242
+ /* ------------------------------------------------------------------ */
243
+ /**
244
+ * @returns { severity: 'error'|'warn'|'info', code: string, message: string, componentId?: string }
245
+ */
246
+ export function lint(schema, data) {
247
+ const out = []
248
+ const view = schema.view
249
+ if (!view) return [{ severity: 'error', code: 'no-view', message: 'schema 没有 view 字段' }]
250
+
251
+ // 1. 脚手架前缀残留("示例"是 scaffold_card.mjs 默认占位文本)
252
+ if (data && typeof data === 'object') {
253
+ for (const [k, v] of Object.entries(data)) {
254
+ if (typeof v === 'string' && v.startsWith('示例')) {
255
+ out.push({
256
+ severity: 'warn',
257
+ code: 'scaffold-placeholder',
258
+ message: `data.json["${k}"] = "${v.slice(0, 30)}" 是脚手架占位值(来自 scaffold_card.mjs 的 inferInputParams),上线前应替换为真实文案`,
259
+ })
260
+ }
261
+ }
262
+ }
263
+ // 也扫表达式里的字面量
264
+ const viewText = JSON.stringify(view)
265
+ const sampleMatches = viewText.match(/"示例[^"]+"/g) || []
266
+ for (const m of [...new Set(sampleMatches)].slice(0, 3)) {
267
+ out.push({
268
+ severity: 'warn',
269
+ code: 'scaffold-placeholder-expr',
270
+ message: `表达式里发现脚手架占位字面量 ${m},上线前应替换`,
271
+ })
272
+ }
273
+
274
+ // 2. 未使用的 inputParams
275
+ const params = (schema.dictionary && schema.dictionary.inputParams) || []
276
+ if (params.length) {
277
+ const used = collectBindings(schema)
278
+ for (const p of params) {
279
+ if (!used.has(p.key)) {
280
+ out.push({
281
+ severity: 'info',
282
+ code: 'unused-input-param',
283
+ message: `inputParams["${p.key}"] 声明了但在 view 表达式($${p.key})与事件脚本(this.state.${p.key})里均未引用,可考虑删除以瘦身数据字典`,
284
+ })
285
+ }
286
+ }
287
+ }
288
+
289
+ // R43: 卡片背景不透明 —— Root 或首层容器必须有非透明背景色
290
+ // 卡片背景不存在透明的情况:检查到透明时从标注取背景色,无法获取则从截图取色
291
+ function hasOpaqueBg(node) {
292
+ const bgGroup = (node.style?.properties || []).find((p) => p.itemKey === 'background')
293
+ if (!bgGroup) return false
294
+ const bgChildren = bgGroup.itemChildren?.properties || []
295
+ const bgType = bgChildren.find((c) => c.itemKey === 'backgroundType')
296
+ const bgColor = bgChildren.find((c) => c.itemKey === 'backgroundColor')
297
+ if (bgType && bgType.itemValue && bgType.itemValue !== '00' && bgColor && bgColor.itemValue) return true
298
+ if (bgType && ['02', '03'].includes(bgType.itemValue)) return true
299
+ return false
300
+ }
301
+ // Check Root(view) OR first child component (card containers often put bg on inner Column)
302
+ let cardHasBg = hasOpaqueBg(view)
303
+ if (!cardHasBg) {
304
+ const firstChild = (view.components?.[0]) || (view.slots?.[0]?.components?.[0])
305
+ if (firstChild) cardHasBg = hasOpaqueBg(firstChild)
306
+ }
307
+ if (!cardHasBg) {
308
+ out.push({
309
+ severity: 'error',
310
+ code: 'r43-card-transparent-background',
311
+ message: `卡片背景透明 —— 卡片背景不允许透明(R43)。须从设计稿标注 cardFill 取背景色写入 Root/首层容器的 background.backgroundColor + backgroundType:"01";标注缺失时从聚焦截图取色兜底。`,
312
+ })
313
+ }
314
+
315
+ // 4-8. 节点级检查
316
+ // 祖先链映射:Button.vue 的 `$emit('click')` 未加 .stop,原生 click 会冒泡到祖先元素,
317
+ // 祖先带 click handler 时按钮实际可点 —— 这类不算「按钮不能点」。
318
+ const parentOf = new Map()
319
+ walk(view, (n, parent) => { if (parent) parentOf.set(n, parent) })
320
+ const hasOwnClick = (node) => (node.eventactions || []).some((ea) => ea && ea.event && ea.event.eventKey === 'click')
321
+ const findClickCarrier = (node) => {
322
+ for (let p = parentOf.get(node); p; p = parentOf.get(p)) if (hasOwnClick(p)) return p
323
+ return null
324
+ }
325
+
326
+ walk(view, (n) => {
327
+ const cid = n.componentId || '(root)'
328
+ const cls = n.className
329
+
330
+ // 9-10. itemValueFrom 必填 + style 子项 itemId 一致性(与 validate_schema.py 同源)
331
+ checkStyleIntegrity(n, cid, out)
332
+
333
+ // 4. Button 缺 click
334
+ if (cls === 'Button') {
335
+ const carrier = hasOwnClick(n) ? null : findClickCarrier(n)
336
+ if (carrier) {
337
+ out.push({
338
+ severity: 'info',
339
+ code: 'button-click-by-ancestor',
340
+ componentId: cid,
341
+ message: `Button "${cid}" 自身无 click 事件,靠冒泡由祖先 ${carrier.className} "${carrier.componentId || '(root)'}" 的 click 承载 —— 可点但点击热区扩大到整个祖先容器,建议确认是否符合预期`,
342
+ })
343
+ } else if (!hasOwnClick(n)) {
344
+ out.push({
345
+ severity: 'warn',
346
+ code: 'button-no-click',
347
+ componentId: cid,
348
+ message: `Button "${cid}" 没有 click 事件 —— 按钮不能点是常见 bug(如有意做静态按钮可忽略)`,
349
+ })
350
+ }
351
+ }
352
+
353
+ // 5. Text 缺 font.color
354
+ if (cls === 'Text') {
355
+ const fontGroup = (n.style && n.style.properties || []).find((p) => p.itemKey === 'font')
356
+ const colorChild = fontGroup && (fontGroup.itemChildren && fontGroup.itemChildren.properties || []).find((c) => c.itemKey === 'color')
357
+ if (!colorChild) {
358
+ out.push({
359
+ severity: 'info',
360
+ code: 'text-no-color',
361
+ componentId: cid,
362
+ message: `Text "${cid}" 未设 font.color —— 浏览器默认黑色,深色主题下会看不见`,
363
+ })
364
+ }
365
+ // 7. 空文本节点
366
+ const valProp = (n.properties || []).find((p) => p.itemKey === 'value')
367
+ if (valProp && valProp.itemValueFrom === '1') {
368
+ const key = String(valProp.itemValue || '').replace(/^\$/, '')
369
+ if (data && data[key] === '' && !valProp.itemDefaultValue) {
370
+ out.push({
371
+ severity: 'info',
372
+ code: 'text-empty-binding',
373
+ componentId: cid,
374
+ message: `Text "${cid}" 绑定 $${key} 但 data.json["${key}"] 是空字符串且无 itemDefaultValue —— 预览时会显示空白`,
375
+ })
376
+ }
377
+ }
378
+ }
379
+
380
+ // 6. Canvas 缺 onMeasured
381
+ if (cls === 'Canvas') {
382
+ const hasMeasured = (n.eventactions || []).some((ea) => ea && ea.event && ea.event.eventKey === 'onMeasured')
383
+ if (!hasMeasured) {
384
+ out.push({
385
+ severity: 'warn',
386
+ code: 'canvas-no-onmeasured',
387
+ componentId: cid,
388
+ message: `Canvas "${cid}" 没有 onMeasured 事件 —— Canvas 只能在 onMeasured 里画,否则一片空白`,
389
+ })
390
+ }
391
+ }
392
+
393
+ // 8. eventKey 与组件类型不匹配
394
+ const knownEvents = {
395
+ Root: ['onReady', ''],
396
+ Row: ['click'],
397
+ Column: ['click'],
398
+ ListLayout: ['click'],
399
+ GridLayout: ['click'],
400
+ AbsoluteLayout: [],
401
+ Canvas: ['onMeasured'],
402
+ Text: ['click'],
403
+ Button: ['click'],
404
+ Icon: ['click'],
405
+ Image: ['click'],
406
+ MultiText: ['click'],
407
+ Input: [], // Input 无可绑定事件
408
+ Divider: [],
409
+ ViewContainer: ['action'],
410
+ }
411
+ const allowed = knownEvents[cls]
412
+ if (allowed && (n.eventactions || []).length) {
413
+ for (const ea of n.eventactions) {
414
+ const ek = ea && ea.event && ea.event.eventKey
415
+ if (ek && !allowed.includes(ek)) {
416
+ out.push({
417
+ severity: 'warn',
418
+ code: 'event-mismatch',
419
+ componentId: cid,
420
+ message: `${cls} "${cid}" 接了 eventKey="${ek}",但该组件只支持 ${JSON.stringify(allowed)} —— 该事件永远不会触发`,
421
+ })
422
+ }
423
+ }
424
+ }
425
+ })
426
+
427
+ // ── R 规则自动校验(从 generation_pitfalls.md 迁移)─────────────
428
+
429
+ // ── Pre-pass: 构建 parent→children 映射(供 R34 多子容器检查 + R17 单子容器判定)──
430
+ const nodeMap = new Map()
431
+ const parentChildrenMap = new Map()
432
+ function buildMaps(node, parent) {
433
+ if (!node || typeof node !== 'object') return
434
+ if (node.componentId) nodeMap.set(node.componentId, node)
435
+ if (parent && parent.componentId) {
436
+ const pid = parent.componentId
437
+ if (!parentChildrenMap.has(pid)) parentChildrenMap.set(pid, [])
438
+ parentChildrenMap.get(pid).push(node)
439
+ }
440
+ if (Array.isArray(node.components)) for (const c of node.components) buildMaps(c, node)
441
+ for (const s of node.slots || []) for (const c of s.components || []) buildMaps(c, node)
442
+ }
443
+ buildMaps(view, null)
444
+
445
+ function getShrinkVal(n) {
446
+ const sp = (n.style && n.style.properties) || []
447
+ const fg = sp.find(p => p.itemKey === 'flex')
448
+ const fc = (fg && fg.itemChildren && fg.itemChildren.properties) || []
449
+ const sc = fc.find(c => c.itemKey === 'flexShrink')
450
+ return sc ? String(sc.itemValue) : null
451
+ }
452
+
453
+ function getWidthVal(n) {
454
+ const sp = (n.style && n.style.properties) || []
455
+ const rg = sp.find(p => p.itemKey === 'rect')
456
+ const rc = (rg && rg.itemChildren && rg.itemChildren.properties) || []
457
+ const wc = rc.find(c => c.itemKey === 'width')
458
+ return wc ? String(wc.itemValue) : null
459
+ }
460
+
461
+ // ── R35: 多子容器全部子节点 flexShrink:0 → 溢出风险 ──
462
+ for (const [pid, children] of parentChildrenMap) {
463
+ const parent = nodeMap.get(pid)
464
+ if (!parent) continue
465
+ const pcls = parent.className
466
+ if ((pcls === 'Row' || pcls === 'Column') && children.length >= 2) {
467
+ const parentWidth = getWidthVal(parent)
468
+ const isContentSize = !parentWidth || parentWidth === 'auto'
469
+ const allShrinkZero = children.every(child => getShrinkVal(child) === '0')
470
+ // 透明度切换固定范式(R57)的变体容器:子节点各带互斥 v-show,同一时刻只有一个在流里,
471
+ // 定宽不会累加,属 R35 的合法豁免(容器本身已与变体等宽高 + overflow:hidden)。
472
+ const hasVShow = (child) => ((child.directives) || []).some((d) => d && d.itemKey === 'v-show')
473
+ const isExclusiveVariantGroup = children.length >= 2 && children.every(hasVShow)
474
+ if (!isContentSize && allShrinkZero && !isExclusiveVariantGroup) {
475
+ out.push({
476
+ severity: 'error',
477
+ code: 'r35-multi-child-all-shrink0',
478
+ componentId: pid,
479
+ message: `${pcls} "${pid}" 宽度非 auto 且 ${children.length} 个并列子节点全部 flexShrink:0 → 外容器宽度变化时定宽累加溢出(R35)。至少一个子节点应保持 flexShrink:1 可压缩;内容自适应容器(width:auto)可保留固定子元素`,
480
+ })
481
+ }
482
+ }
483
+ }
484
+
485
+ // ── R31: Image 引用路径含空格 → CSS url() 断裂 ──
486
+ walk(view, (n) => {
487
+ if (n.className !== 'Image') return
488
+ for (const p of n.properties || []) {
489
+ if (p.itemKey === 'value' && typeof p.itemValue === 'string') {
490
+ const v = p.itemValue
491
+ if (v.includes('/slices/') && /\s/.test(v.replace('/slices/', ''))) {
492
+ out.push({
493
+ severity: 'error',
494
+ code: 'r31-slice-filename-space',
495
+ componentId: n.componentId || '',
496
+ message: `Image "${n.componentId}" 引用路径含空格: "${v}" → CSS url() 空格断裂 → background-image:none(R31)。重命名为纯 ASCII`,
497
+ })
498
+ }
499
+ }
500
+ }
501
+ })
502
+
503
+ // ── R59/R60: 切图语义化命名 + 同内容去重(slices_convention.md §1.1 / §3.1)──
504
+ for (const ref of collectSliceRefStrings(schema)) {
505
+ const name = sliceNameFromRef(ref)
506
+ const base = path.basename(name)
507
+ const nameCheck = checkSliceName(base)
508
+ if (!nameCheck.semantic) {
509
+ out.push({
510
+ severity: 'warn',
511
+ code: 'r59-slice-semantic-name',
512
+ componentId: '',
513
+ message: `切图名非语义化: "${base}"(${nameCheck.reason})→ 按实际功能/作用重命名(如 iconArrowRight@3x.png / bgCardHeader@3x.png),并同步改写所有卡片引用;禁止沿用设计稿图层名、导出目录前缀或中文名(slices_convention.md §1.1)`,
514
+ })
515
+ }
516
+ const peers = duplicatePeers(sliceContentIndex(), slicesDir, name)
517
+ if (peers.length) {
518
+ out.push({
519
+ severity: 'warn',
520
+ code: 'r60-slice-duplicate-content',
521
+ componentId: '',
522
+ message: `切图 "${base}" 与 ${peers.join('、')} 内容完全相同(sha256 一致)→ 只保留语义名那一份并把引用改写到它,不要重复导入/重复上传(slices_convention.md §3.1 / §4.1)`,
523
+ })
524
+ }
525
+ }
526
+
527
+ // ── R61: 静态文案 / 字典默认值含疑似运行期数字(rules.md §十四,提醒级 WARN)──
528
+ // 只作「提醒 AI 语义复核」的启发式:命中 ≠ 违规(版本号 / 固定营销话术会误报),故 WARN 不阻塞。
529
+ const NUMERIC_LITERAL_PATTERNS = [
530
+ { re: /\d{1,3}(,\d{3})+(\.\d+)?/, label: '千分号数字' },
531
+ { re: /[¥¥]\s*\d+(\.\d+)?/, label: '货币符+数字' },
532
+ { re: /\d+(\.\d+)?\s*(元|笔|次|个|条)/, label: '数字+单位词' },
533
+ ]
534
+ const numericHit = (str) => {
535
+ for (const { re, label } of NUMERIC_LITERAL_PATTERNS) if (re.test(str)) return label
536
+ return null
537
+ }
538
+ walk(view, (n) => {
539
+ if (n.className !== 'Text') return
540
+ for (const p of n.properties || []) {
541
+ if (p.itemKey !== 'value' || p.itemValueFrom === '1') continue // 表达式绑定不查
542
+ const v = typeof p.itemValue === 'string' ? p.itemValue : ''
543
+ if (!v) continue
544
+ const label = numericHit(v)
545
+ if (!label) continue
546
+ out.push({
547
+ severity: 'warn',
548
+ code: 'r61-text-numeric-literal',
549
+ componentId: n.componentId || '',
550
+ message: `静态文案含疑似运行期数字(${label}: "${v.slice(0, 40)}")→ 按 rules.md §十四 先做 AI 语义复核:数字随运行期数据变化则抽 inputParams 变量 + 表达式拼接展示(金额=原始数字+格式化表达式);固定话术(版本号/营销语)可保留,忽略本 WARN`,
551
+ })
552
+ }
553
+ })
554
+ for (const param of params) {
555
+ const dv = param && param.defaultValue
556
+ if (typeof dv !== 'string' || !dv) continue
557
+ const label = numericHit(dv)
558
+ if (!label) continue
559
+ out.push({
560
+ severity: 'warn',
561
+ code: 'r61-text-numeric-literal',
562
+ componentId: param.key || '',
563
+ message: `inputParams["${param.key}"].defaultValue 为含疑似运行期数字的字符串(${label}: "${dv.slice(0, 40)}")→ 金额/计数应 type:"number" + 原始数字,禁止格式化串(12,300.00)/带货币符串进字典(rules.md §十四.2/§十四.4);确为文本字段(地址/话术)可忽略本 WARN`,
564
+ })
565
+ }
566
+
567
+ // ── R32: Image 缺 rect.height → CSS aspect-ratio:16/9 压缩 ──
568
+ walk(view, (n) => {
569
+ if (n.className !== 'Image') return
570
+ const sp = (n.style && n.style.properties) || []
571
+ const rectGroup = sp.find(p => p.itemKey === 'rect')
572
+ const rectChildren = (rectGroup && rectGroup.itemChildren && rectGroup.itemChildren.properties) || []
573
+ const hChild = rectChildren.find(c => c.itemKey === 'height')
574
+ if (!hChild || !String(hChild.itemValue).trim()) {
575
+ out.push({
576
+ severity: 'error',
577
+ code: 'r32-image-no-height',
578
+ componentId: n.componentId || '',
579
+ message: `Image "${n.componentId}" 缺 rect.height → CSS aspect-ratio:16/9 压缩图片高度(R32)。设显式高度`,
580
+ })
581
+ }
582
+ })
583
+
584
+ // ── R25: Image itemValueFrom 三态精确映射 ──
585
+ walk(view, (n) => {
586
+ if (n.className !== 'Image') return
587
+ for (const p of n.properties || []) {
588
+ if (p.itemKey !== 'value') continue
589
+ const from = String(p.itemValueFrom || '')
590
+ const val = String(p.itemValue || '')
591
+ if (from === '0' && !val.startsWith('$')) {
592
+ out.push({
593
+ severity: 'error',
594
+ code: 'r25-image-from-0',
595
+ componentId: n.componentId || '',
596
+ message: `Image "${n.componentId}" itemValueFrom="0"(常量) → 渲染器不走图片逻辑,空白(R25)。静态路径用 "2"`,
597
+ })
598
+ }
599
+ if (from === '1' && !val.startsWith('$')) {
600
+ out.push({
601
+ severity: 'error',
602
+ code: 'r25-image-from-1',
603
+ componentId: n.componentId || '',
604
+ message: `Image "${n.componentId}" itemValueFrom="1"(变量) 但值不以 $ 开头 → 表达式求值失败,空白(R25)`,
605
+ })
606
+ }
607
+ }
608
+ })
609
+
610
+ // ── R23: 百分比表达式缺 Math.round → JS 浮点精度 ──
611
+ walk(view, (n) => {
612
+ const allProps = [...(n.properties || [])]
613
+ for (const sp of n.style?.properties || []) {
614
+ for (const c of sp.itemChildren?.properties || []) allProps.push(c)
615
+ }
616
+ for (const p of allProps) {
617
+ if (String(p.itemValueFrom) === '1' && typeof p.itemValue === 'string') {
618
+ if (/\*\s*100/.test(p.itemValue) && !/Math\.round/.test(p.itemValue)) {
619
+ out.push({
620
+ severity: 'error',
621
+ code: 'r23-no-math-round',
622
+ componentId: n.componentId || '',
623
+ message: `"${n.componentId}" 表达式含 *100 但无 Math.round → JS 浮点精度溢出(R23): ${p.itemValue.slice(0, 50)}`,
624
+ })
625
+ }
626
+ }
627
+ }
628
+ })
629
+
630
+ // R17/R22: flexShrink 铁律
631
+ // R17: 关键 Text(金额/数字/标签/标题/占比/单位)必须 flexShrink:0(防换行)
632
+ // R22: width:100% 节点必须 flexShrink:1(让位防溢出)
633
+ walk(view, (n, parent) => {
634
+ const cid = n.componentId || '(root)'
635
+ const cls = n.className
636
+ const styleProps = (n.style && n.style.properties) || []
637
+ const flexGroup = styleProps.find((p) => p.itemKey === 'flex')
638
+ const flexChildren = (flexGroup && flexGroup.itemChildren && flexGroup.itemChildren.properties) || []
639
+ const rectGroup = styleProps.find((p) => p.itemKey === 'rect')
640
+ const rectChildren = (rectGroup && rectGroup.itemChildren && rectGroup.itemChildren.properties) || []
641
+ const widthChild = rectChildren.find((c) => c.itemKey === 'width')
642
+ const shrinkChild = flexChildren.find((c) => c.itemKey === 'flexShrink')
643
+ const widthVal = widthChild ? String(widthChild.itemValue) : ''
644
+ const heightChild = rectChildren.find((c) => c.itemKey === 'height')
645
+ const childRows = (n.slots || []).some((slot) =>
646
+ (slot.components || []).some((child) => child.className === 'Row'))
647
+ const isProgressBarTrack = cls === 'Row' && childRows
648
+ && Number(heightChild?.itemValue || 0) > 0 && Number(heightChild.itemValue) <= 8
649
+
650
+ // R22: width:100% 且 flexShrink:0 → 锁死协商,固定宽兄弟会被挤出
651
+ if (widthVal === '100%') {
652
+ if (shrinkChild && String(shrinkChild.itemValue) === '0' && !isProgressBarTrack) {
653
+ out.push({
654
+ severity: 'error',
655
+ code: 'r22-width100-flexshrink0',
656
+ componentId: cid,
657
+ message: `${cls} "${cid}" width:100% 且 flexShrink:0 → 协商锁死,固定宽兄弟(图标/金额/按钮)会被挤出或溢出(R22)。width:100% 应配 flexShrink:1 让位`,
658
+ })
659
+ }
660
+ }
661
+
662
+ // R17(修订):仅对「单子容器」中的叶子节点强制 flexShrink:0。
663
+ // 多子容器(2+ 并列子节点)中的叶子由 R34 管辖(至少一个可压缩防溢出),不重复报 R17。
664
+ if (RENDER_CONTRACT.leafClasses.includes(cls) && widthVal !== '100%') {
665
+ const siblings = parent ? (parentChildrenMap.get(parent.componentId) || []) : []
666
+ const isMultiChildParent = siblings.length >= 2
667
+ if (!isMultiChildParent && (!shrinkChild || String(shrinkChild.itemValue) !== '0')) {
668
+ out.push({
669
+ severity: 'error',
670
+ code: 'r17-leaf-no-flexshrink',
671
+ componentId: cid,
672
+ message: `${cls} "${cid}" 未设 flexShrink:0 → 渲染器默认 flexShrink:1 会导致元素被压缩(文本换行/图标变形)(R17)。单子容器中的叶子节点必须显式设 flexShrink:0`,
673
+ })
674
+ }
675
+ }
676
+ })
677
+
678
+ // R8: 圆角归属(纯色卡外圆角放 Root,分层卡放内卡,不重复)
679
+ {
680
+ const rootStyleProps = (view.style && view.style.properties) || []
681
+ const rootRadiusGroup = rootStyleProps.find((p) => p.itemKey === 'borderRadius')
682
+ const rootRadiusChild = rootRadiusGroup && (rootRadiusGroup.itemChildren && rootRadiusGroup.itemChildren.properties || []).find((c) => c.itemKey === 'borderRadius')
683
+ const rootRadius = rootRadiusChild ? parseFloat(rootRadiusChild.itemValue) : 0
684
+
685
+ if (rootRadius > 0) {
686
+ // Root 有圆角 → 检查第一层子容器是否也设了相同圆角(双重圆角)
687
+ const rootSlots = view.slots || []
688
+ let checked = 0
689
+ for (const slot of rootSlots) {
690
+ const comps = (slot.components) || []
691
+ for (const comp of comps.slice(0, 3)) { // 只查前 3 个一级子容器
692
+ const childStyle = (comp.style && comp.style.properties) || []
693
+ const childRadiusGroup = childStyle.find((p) => p.itemKey === 'borderRadius')
694
+ if (childRadiusGroup) {
695
+ const childRadiusChild = (childRadiusGroup.itemChildren && childRadiusGroup.itemChildren.properties || []).find((c) => c.itemKey === 'borderRadius')
696
+ const childRadius = childRadiusChild ? parseFloat(childRadiusChild.itemValue) : 0
697
+ if (childRadius > 0 && Math.abs(childRadius - rootRadius) < 1) {
698
+ out.push({
699
+ severity: 'info',
700
+ code: 'r8-double-border-radius',
701
+ componentId: comp.componentId || '?',
702
+ message: `Root 圆角=${rootRadius},一级子容器 "${comp.componentId}" 也设了相同圆角=${childRadius}(R8)。纯色卡外圆角只放 Root,内层容器应透明且不重复圆角`,
703
+ })
704
+ }
705
+ }
706
+ checked++
707
+ }
708
+ if (checked >= 3) break
709
+ }
710
+ }
711
+ }
712
+
713
+ // R19: Row + justifyContent:space-between 但宽度不确定 → 收缩到内容宽,space-between 失效
714
+ //
715
+ // 判「宽度不确定」需同时看自身 rect.width 与父容器交叉轴行为,否则全是误报:
716
+ // 1. 自身已给确定宽度(100% 或定宽 px,如 transferPlan 两个 100px 日期胶囊并排在横向 Row 里)
717
+ // → 容器宽度可定,space-between 在该宽度内正常生效,不是缺陷;改成 100% 反而会溢出。
718
+ // 2. 自身未设宽但父容器是纵向流且交叉轴 stretch(Column/Root/SingleColumn 的 CSS 已强制
719
+ // flex-direction:column,align-items 缺省即 stretch,见 preview/styles/components.css)
720
+ // → 子 Row 本就被拉伸到父内容宽,space-between 已生效,无需显式补 width:100%。
721
+ // (补了反而撞 r22-width100-flexshrink0 / r35-multi-child-all-shrink0 两条 ERROR。)
722
+ // 只有「未设宽/auto 且父容器不拉伸」——典型是嵌在横向 Row 里——才会收缩到内容宽、
723
+ // 子节点堆在左侧,这才是 R19 要拦的真缺陷。
724
+ const COLUMN_FLOW = new Set(['Root', 'Column', 'SingleColumn'])
725
+ const flexSub = (node, key) => {
726
+ const g = (node?.style?.properties || []).find((p) => p.itemKey === 'flex')
727
+ const c = ((g?.itemChildren?.properties) || []).find((x) => x.itemKey === key)
728
+ return String(c?.itemValue ?? '').trim()
729
+ }
730
+ const stretchedByParent = (parent) => {
731
+ if (!parent) return true // 根层级由卡片宽度决定
732
+ // Column/Root/SingleColumn 由 CSS 强制 column;其余(Row/MultiColumn/GridLayout/ListLayout 项…)
733
+ // 默认横向流,schema 显式写了 flexDirection 才以其为准
734
+ const dir = COLUMN_FLOW.has(parent.className) ? 'column' : (flexSub(parent, 'flexDirection') || 'row')
735
+ if (dir !== 'column') return false
736
+ const align = flexSub(parent, 'alignItems')
737
+ return align === '' || align === 'stretch'
738
+ }
739
+
740
+ walk(view, (n, parent) => {
741
+ const cid = n.componentId || '(root)'
742
+ if (n.className !== 'Row') return
743
+ const styleProps = (n.style && n.style.properties) || []
744
+ const flexGroup = styleProps.find((p) => p.itemKey === 'flex')
745
+ const flexChildren = (flexGroup && flexGroup.itemChildren && flexGroup.itemChildren.properties) || []
746
+ const justifyChild = flexChildren.find((c) => c.itemKey === 'justifyContent')
747
+ const rectGroup = styleProps.find((p) => p.itemKey === 'rect')
748
+ const rectChildren = (rectGroup && rectGroup.itemChildren && rectGroup.itemChildren.properties) || []
749
+ const widthChild = rectChildren.find((c) => c.itemKey === 'width')
750
+
751
+ if (justifyValue(justifyChild) === 'space-between') {
752
+ const widthVal = widthChild ? String(widthChild.itemValue ?? '').trim() : ''
753
+ if ((widthVal === '' || widthVal === 'auto') && !stretchedByParent(parent)) {
754
+ out.push({
755
+ severity: 'warn',
756
+ code: 'r19-space-between-no-width',
757
+ componentId: cid,
758
+ message: `Row "${cid}" 设了 justifyContent:space-between 但 width=${widthVal || '(未设)'},且父容器 ${parent ? `${parent.className} "${parent.componentId || '(root)'}" 不拉伸交叉轴` : ''} —— 渲染器按 auto 处理会收缩到内容宽,space-between 失效、子节点堆在左侧(R19)。须显式 rect.width:"100%"(itemValueFrom:"0")撑满父级主轴。`,
759
+ })
760
+ }
761
+ }
762
+ })
763
+
764
+
765
+ // ── 格式铁律:值格式校验(生成器最常犯的格式错误) ──
766
+
767
+ // R-FMT1: borderRadius 值不得用元组格式 {a,b,c,d}(应为标量 "12" 或单独角 borderTopLeftRadius 等)
768
+ walk(view, (n) => {
769
+ const cid = n.componentId || '(root)'
770
+ const styleProps = (n.style && n.style.properties) || []
771
+ const radiusGroup = styleProps.find((p) => p.itemKey === 'borderRadius')
772
+ if (radiusGroup) {
773
+ const children = (radiusGroup.itemChildren && radiusGroup.itemChildren.properties) || []
774
+ const radiusChild = children.find((c) => c.itemKey === 'borderRadius')
775
+ if (radiusChild && typeof radiusChild.itemValue === 'string'
776
+ && radiusChild.itemValue.includes('{') && radiusChild.itemValue.includes('}')) {
777
+ out.push({
778
+ severity: 'error',
779
+ code: 'fmt-border-radius-tuple',
780
+ componentId: cid,
781
+ message: `${n.className} "${cid}" borderRadius 值用了元组格式 "${radiusChild.itemValue}" —— 应为标量(如 "12")或单独角属性(borderTopLeftRadius/borderTopRightRadius/borderBottomRightRadius/borderBottomLeftRadius)。元组格式渲染器无法解析`,
782
+ })
783
+ }
784
+ }
785
+ })
786
+
787
+ // R-FMT2: padding 值不得用元组格式 {top,right,bottom,left}(应拆为单独属性 paddingTop/Right/Bottom/Left)
788
+ walk(view, (n) => {
789
+ const cid = n.componentId || '(root)'
790
+ const styleProps = (n.style && n.style.properties) || []
791
+ const paddingGroup = styleProps.find((p) => p.itemKey === 'padding')
792
+ if (paddingGroup) {
793
+ const children = (paddingGroup.itemChildren && paddingGroup.itemChildren.properties) || []
794
+ const padChild = children.find((c) => c.itemKey === 'padding')
795
+ if (padChild && typeof padChild.itemValue === 'string'
796
+ && padChild.itemValue.includes('{') && padChild.itemValue.includes('}')) {
797
+ out.push({
798
+ severity: 'error',
799
+ code: 'fmt-padding-tuple',
800
+ componentId: cid,
801
+ message: `${n.className} "${cid}" padding 值用了元组格式 "${padChild.itemValue}" —— 应拆为 paddingTop/paddingRight/paddingBottom/paddingLeft 各设单值(paddingType="1")。元组格式会导致预览器解析失败(文本塌缩/行宽爆裂)`,
802
+ })
803
+ }
804
+ const marginChild = children.find((c) => c.itemKey === 'margin')
805
+ if (marginChild && typeof marginChild.itemValue === 'string'
806
+ && marginChild.itemValue.includes('{') && marginChild.itemValue.includes('}')) {
807
+ out.push({
808
+ severity: 'error',
809
+ code: 'fmt-margin-tuple',
810
+ componentId: cid,
811
+ message: `${n.className} "${cid}" margin 值用了元组格式 "${marginChild.itemValue}" —— 应拆为 marginTop/marginRight/marginBottom/marginLeft 各设单值(marginType="1")`,
812
+ })
813
+ }
814
+ }
815
+ })
816
+
817
+ // R38: 负 marginTop 禁止 —— 标题/文本与装饰条重叠必须用 AbsoluteLayout,不得用负外边距
818
+ walk(view, (n) => {
819
+ const cid = n.componentId || '(root)'
820
+ for (const sg of (n.style?.properties || [])) {
821
+ if (sg.itemKey !== 'margin') continue
822
+ const children = sg.itemChildren?.properties || []
823
+ for (const mc of children) {
824
+ if (mc.itemKey === 'marginTop' && typeof mc.itemValue === 'string' && mc.itemValue.startsWith('-')) {
825
+ out.push({
826
+ severity: 'error',
827
+ code: 'r38-negative-margin',
828
+ componentId: cid,
829
+ message: `${n.className} "${cid}" marginTop=${mc.itemValue} 是负值 —— 文本与下划线/装饰条重叠必须用 AbsoluteLayout + position.top 实现,禁止负外边距(R38)。`,
830
+ })
831
+ }
832
+ }
833
+ }
834
+ })
835
+
836
+ // R44: 类型+值成对检查 —— 有值但缺类型开关的样式组(borderRadius/border/shadow)→ ERROR
837
+ walk(view, (n) => {
838
+ const cid = n.componentId || '(root)'
839
+ for (const sg of (n.style?.properties || [])) {
840
+ const children = sg.itemChildren?.properties || []
841
+ if (sg.itemKey === 'borderRadius') {
842
+ const radius = children.find((c) => c.itemKey === 'borderRadius')
843
+ const radiusType = children.find((c) => c.itemKey === 'borderRadiusType')
844
+ if (radius && radius.itemValue && (!radiusType || !radiusType.itemValue || radiusType.itemValue === '')) {
845
+ out.push({
846
+ severity: 'error',
847
+ code: 'r44-border-radius-no-type',
848
+ componentId: cid,
849
+ message: `${n.className} "${cid}" 设了 borderRadius=${radius.itemValue} 但缺 borderRadiusType —— 渲染器默认类型为空(=无圆角),值被静默忽略。须配 borderRadiusType:"0"(全角)。`,
850
+ })
851
+ }
852
+ }
853
+ if (sg.itemKey === 'border') {
854
+ const borderColor = children.find((c) => c.itemKey === 'borderColor')
855
+ const borderWidth = children.find((c) => c.itemKey === 'borderWidth')
856
+ const isBorder = children.find((c) => c.itemKey === 'isBorder')
857
+ if ((borderColor?.itemValue || borderWidth?.itemValue) && (!isBorder || isBorder.itemValue !== '1')) {
858
+ out.push({
859
+ severity: 'error',
860
+ code: 'r44-border-no-isborder',
861
+ componentId: cid,
862
+ message: `${n.className} "${cid}" 设了 borderColor/borderWidth 但 isBorder≠"1" —— 边框值被渲染器忽略。须配 isBorder:"1"。`,
863
+ })
864
+ }
865
+ }
866
+ // R44 扩展:margin / padding 的「类型开关」—— 设了单边值但 marginType/paddingType≠"1"
867
+ // ⇒ 渲染器按默认类型忽略整组边距。实测事故(all-components-test 07 画板):
868
+ // 6 处 margin 组漏 marginType:"1" → 卡片塌掉 86px(match 卡 73%),模型连调 9 轮
869
+ // iterate 都没定位到,最后靠 diff_bands 才发现。generation_pitfalls.md §「类型开关+值」
870
+ // 早有铁律,但散文在步数压力下不会被读到 —— 这里做成机器门禁。
871
+ if (sg.itemKey === 'margin' || sg.itemKey === 'padding') {
872
+ const typeKey = `${sg.itemKey}Type`
873
+ const typeChild = children.find((c) => c.itemKey === typeKey)
874
+ const sides = ['Top', 'Right', 'Bottom', 'Left']
875
+ .map((d) => children.find((c) => c.itemKey === `${sg.itemKey}${d}`))
876
+ .filter((c) => c && ['', '0', '0px', undefined, null].indexOf(String(c.itemValue ?? '').trim()) < 0)
877
+ if (sides.length && String(typeChild?.itemValue ?? '') !== '1') {
878
+ out.push({
879
+ severity: 'error',
880
+ code: `r44-${sg.itemKey}-no-type`,
881
+ componentId: cid,
882
+ message: `${n.className} "${cid}" 设了 ${sides.map((c) => `${c.itemKey}=${c.itemValue}`).join(' / ')} 但 ${typeKey}="${typeChild?.itemValue ?? '(缺)'}" —— 渲染器按默认类型忽略整组边距(间距静默失效 / 高度塌缩)。须配 ${typeKey}:"1"(IR 写法 s.${sg.itemKey}.${typeKey}:"1";expand_ir 已自动补,出现本条说明是手写 schema 或显式写了错误类型)`,
883
+ })
884
+ }
885
+ }
886
+ // R44 扩展:background 的「类型开关 + 值」配对,以及「渐变塞错字段」
887
+ // 实测事故:模型想还原设计稿顶部渐变,先猜 backgroundType:"02"+gradientColor/gradientDirection
888
+ // (02 是图片类型、gradientColor 根本不是 DSL 键),再把 linear-gradient(...) 塞进 backgroundColor,
889
+ // 三次全部静默失效 → 放弃并编造「顶部蓝条是渲染器标注、属测量噪声」收尾。
890
+ // 正路:backgroundType:"03" + backgroundGradient(references/spec_to_schema_patterns.md §背景类型表)。
891
+ if (sg.itemKey === 'background') {
892
+ const bgType = String(children.find((c) => c.itemKey === 'backgroundType')?.itemValue ?? '')
893
+ const colorChild = children.find((c) => c.itemKey === 'backgroundColor')
894
+ const gradChild = children.find((c) => c.itemKey === 'backgroundGradient')
895
+ const imgChild = children.find((c) => c.itemKey === 'backgroundImage')
896
+ const colorVal = String(colorChild?.itemValue ?? '')
897
+ if (/gradient\s*\(/i.test(colorVal)) {
898
+ out.push({
899
+ severity: 'error',
900
+ code: 'r44-gradient-in-background-color',
901
+ componentId: cid,
902
+ message: `${n.className} "${cid}" 把渐变写进了 backgroundColor="${colorVal.slice(0, 60)}" —— 渲染器只接受颜色值,渐变会被忽略(背景变透明/纯色)。渐变须写 backgroundGradient + backgroundType:"03"`,
903
+ })
904
+ }
905
+ if (gradChild?.itemValue && bgType !== '03') {
906
+ out.push({
907
+ severity: 'error',
908
+ code: 'r44-background-type-mismatch',
909
+ componentId: cid,
910
+ message: `${n.className} "${cid}" 设了 backgroundGradient 但 backgroundType="${bgType || '(缺)'}" —— 渐变被忽略。backgroundType 取值:00 透明 / 01 颜色 / 02 图片 / 03 渐变(渐变必须 03)`,
911
+ })
912
+ }
913
+ if (imgChild?.itemValue && bgType !== '02') {
914
+ out.push({
915
+ severity: 'error',
916
+ code: 'r44-background-type-mismatch',
917
+ componentId: cid,
918
+ message: `${n.className} "${cid}" 设了 backgroundImage 但 backgroundType="${bgType || '(缺)'}" —— 图片背景被忽略。backgroundType 必须为 "02"(02 是图片,不是渐变)`,
919
+ })
920
+ }
921
+ if (bgType === '03' && !gradChild?.itemValue) {
922
+ out.push({
923
+ severity: 'error',
924
+ code: 'r44-background-type-mismatch',
925
+ componentId: cid,
926
+ message: `${n.className} "${cid}" backgroundType="03"(渐变)但没设 backgroundGradient —— 背景渲染为空。补 backgroundGradient:"linear-gradient(...)",或改回 01 + backgroundColor`,
927
+ })
928
+ }
929
+ if (bgType === '02' && !imgChild?.itemValue) {
930
+ out.push({
931
+ severity: 'error',
932
+ code: 'r44-background-type-mismatch',
933
+ componentId: cid,
934
+ message: `${n.className} "${cid}" backgroundType="02"(图片)但没设 backgroundImage —— 背景渲染为空。图片背景补 backgroundImage(远程 URL / @filestore 别名),纯色改 01,渐变改 03`,
935
+ })
936
+ }
937
+ }
938
+ if (sg.itemKey === 'shadow') {
939
+ const shadowColor = children.find((c) => c.itemKey === 'shadowColor')
940
+ const isShadow = children.find((c) => c.itemKey === 'isShadow')
941
+ if (shadowColor?.itemValue && (!isShadow || isShadow.itemValue !== '1')) {
942
+ out.push({
943
+ severity: 'error',
944
+ code: 'r44-shadow-no-isshadow',
945
+ componentId: cid,
946
+ message: `${n.className} "${cid}" 设了 shadowColor 但 isShadow≠"1" —— 阴影值被渲染器忽略。须配 isShadow:"1"。`,
947
+ })
948
+ }
949
+ }
950
+ }
951
+ })
952
+
953
+ // R-KEY: style 组内的「自造键 / 缩写键」—— style-meta 没这个 itemKey ⇒ itemId 落空 ⇒
954
+ // 运行时静默丢弃该属性。validate_schema.py 对子节点已报 ERROR,但 Root(view)历史上不过检,
955
+ // lint 这里对全树(含 Root)统一拦一次,并直接列出该组的合法键,省掉模型再去翻 style-meta。
956
+ {
957
+ const styleMaps = loadStyleMaps()
958
+ walk(view, (n) => {
959
+ const cid = n.componentId || '(root)'
960
+ for (const sg of (n.style?.properties || [])) {
961
+ const allowed = styleMaps[sg.itemKey]
962
+ if (!allowed) continue
963
+ for (const child of (sg.itemChildren?.properties || [])) {
964
+ const ck = child.itemKey
965
+ if (ck == null || allowed[ck] != null) continue
966
+ out.push({
967
+ severity: 'error',
968
+ code: 'unknown-style-child',
969
+ componentId: cid,
970
+ message: `${n.className} "${cid}" 的 style.${sg.itemKey} 写了不存在的键 "${ck}" —— 渲染器静默丢弃(itemId 落空)。该组合法键:${Object.keys(allowed).join(' / ')}(键名以 preview/meta/style-meta.json 原文为准,禁自造 / 缩写)`,
971
+ })
972
+ }
973
+ }
974
+ })
975
+ }
976
+
977
+ // R50: slotScope 内绑定用 $field 而非 $$.field($$ 是整个 item 对象,$$.field 冗余)
978
+ walk(view, (n) => {
979
+ const cid = n.componentId || '(root)'
980
+ for (const p of (n.properties || [])) {
981
+ const val = String(p.itemValue || '')
982
+ if (val.includes('$$.')) {
983
+ out.push({
984
+ severity: 'error',
985
+ code: 'r50-dollar-dollar-field',
986
+ componentId: cid,
987
+ message: `${n.className} "${cid}" 绑定含 "$$.xxx" —— slotScope 内应直接用 $fieldName(当前 item 字段已展开到作用域),$$ 仅引用整个 item 对象,$$.field 是冗余写法。改为 $field。`,
988
+ })
989
+ }
990
+ }
991
+ })
992
+
993
+ // R51: customAction 里 this.data 只允许读 Input 绑定键的实时输入值;运行态变量一律 this.state
994
+ const inputBoundKeys = new Set()
995
+ walk(view, (n) => {
996
+ if (n.className !== 'Input') return
997
+ for (const p of n.properties || []) {
998
+ if (p.itemKey === 'value' && String(p.itemValueFrom) === '1') {
999
+ const m = /^\$([A-Za-z_$][\w$]*)$/.exec(String(p.itemValue || '').trim())
1000
+ if (m) inputBoundKeys.add(m[1])
1001
+ }
1002
+ }
1003
+ })
1004
+ walk(view, (n) => {
1005
+ const cid = n.componentId || '(root)'
1006
+ for (const ea of n.eventactions || []) {
1007
+ const act = ea && ea.action
1008
+ if (!act || act.actionType !== 'customAction') continue
1009
+ const fn = (act.properties || []).find((p) => p.itemKey === 'function') || {}
1010
+ const iv = fn.itemValue || {}
1011
+ const code = String(iv.code || iv.compiled || '')
1012
+ const seen = new Set()
1013
+ for (const m of code.matchAll(/this\.data\.([A-Za-z_$][\w$]*)/g)) {
1014
+ const key = m[1]
1015
+ if (inputBoundKeys.has(key) || seen.has(key)) continue
1016
+ seen.add(key)
1017
+ out.push({
1018
+ severity: 'error',
1019
+ code: 'r51-event-this-data-misuse',
1020
+ componentId: cid,
1021
+ message: `${n.className} "${cid}" customAction 读 this.data.${key} —— this.data 仅允许读 Input 绑定键的实时输入值;运行态变量(选中态/锁定态/派生值等)一律用 this.state.${key}。`,
1022
+ })
1023
+ }
1024
+ }
1025
+ })
1026
+
1027
+ // R52: v-show 显隐设置显示名命名规范(<15 字符与表达式一致;>=15 字符设合适命名,不得抄表达式)
1028
+ walk(view, (n) => {
1029
+ const cid = n.componentId || '(root)'
1030
+ for (const dr of n.directives || []) {
1031
+ if (dr.itemKey !== 'v-show') continue
1032
+ const props = (dr.itemChildren || {}).properties || []
1033
+ const st = (props.find((p) => p.itemKey === 'showType') || {}).itemValue
1034
+ const cond = (props.find((p) => p.itemKey === 'condition') || {}).itemValue
1035
+ if (st !== '01' && st !== '02') continue
1036
+ if (!cond || typeof cond !== 'object' || !cond.expression) continue
1037
+ const expr = String(cond.expression)
1038
+ const name = cond.name == null ? '' : String(cond.name)
1039
+ if (!name.trim()) continue // 缺失/空由 validate_schema.py 报 ERROR
1040
+ if (expr.length < 15 && name !== expr) {
1041
+ out.push({
1042
+ severity: 'warn',
1043
+ code: 'r52-vshow-condition-name-style',
1044
+ componentId: cid,
1045
+ message: `${n.className} "${cid}" v-show 显示名 "${name}" 与表达式不一致 —— 表达式 ${expr.length} 字符(<15)时显示名应与表达式保持一致:${expr}`,
1046
+ })
1047
+ } else if (expr.length >= 15 && name === expr) {
1048
+ out.push({
1049
+ severity: 'warn',
1050
+ code: 'r52-vshow-condition-name-style',
1051
+ componentId: cid,
1052
+ message: `${n.className} "${cid}" v-show 表达式 ${expr.length} 字符(>=15)但显示名抄了表达式 —— 应设置合适的短命名(如「最后一项隐藏分割线」)`,
1053
+ })
1054
+ }
1055
+ }
1056
+ })
1057
+
1058
+ // R53: 特殊节点(MultiColumn/ListLayout/GridLayout/AbsoluteLayout/Canvas/Map/Video/ViewContainer)禁止直接设 v-show —— 外套一层 Row/Column 承载显隐条件(铁律)
1059
+ const VSHOW_WRAP_REQUIRED = new Set(['MultiColumn', 'ListLayout', 'GridLayout', 'AbsoluteLayout', 'Canvas', 'Map', 'Video', 'ViewContainer'])
1060
+ walk(view, (n) => {
1061
+ const cid = n.componentId || '(root)'
1062
+ if (!VSHOW_WRAP_REQUIRED.has(n.className)) return
1063
+ const hasCondVshow = (n.directives || []).some((d) => d.itemKey === 'v-show'
1064
+ && ((d.itemChildren || {}).properties || []).some((p) => p.itemKey === 'showType' && (p.itemValue === '01' || p.itemValue === '02')))
1065
+ if (hasCondVshow) {
1066
+ out.push({
1067
+ severity: 'error',
1068
+ code: 'r53-vshow-needs-wrapper',
1069
+ componentId: cid,
1070
+ message: `${n.className} "${cid}" 直接设置 v-show 显隐条件 —— 铁律:多列/列表/网格/绝对布局、Canvas、Map、Video、自定义节点需设显示条件时,必须外套一层 Row/Column 并把 v-show 设在包裹层;其他节点可直接设置`,
1071
+ })
1072
+ }
1073
+ })
1074
+
1075
+ // R54: ListLayout/GridLayout item 事件修改绑定数据必须走 postMessage('globalSet') + 根节点 onReady addEventListener 监听(铁律,范式见 recipes.md §4 / depositFilter)
1076
+ const codeOf = (ea) => {
1077
+ const fn = ((ea || {}).action || {}).properties || []
1078
+ const f = fn.find((x) => x.itemKey === 'function') || {}
1079
+ return String((f.itemValue || {}).code || (f.itemValue || {}).compiled || '')
1080
+ }
1081
+ const MUTATE_RE = /this\.(setData|setRootData)\(|this\.(state|data)\.[\w$.]+\s*=[^=]/
1082
+ walk(view, (n, parent) => {
1083
+ let scoped = false
1084
+ for (let cur = n; cur; cur = cur.__lintParent) {
1085
+ if (cur.__lintSlotScope) { scoped = true; break }
1086
+ }
1087
+ if (!scoped) return
1088
+ if (n.className === 'ListLayout' || n.className === 'GridLayout') return
1089
+ const cid = n.componentId || '(root)'
1090
+ for (const ea of n.eventactions || []) {
1091
+ if (((ea || {}).action || {}).actionType !== 'customAction') continue
1092
+ const code = codeOf(ea)
1093
+ if (MUTATE_RE.test(code)) {
1094
+ out.push({
1095
+ severity: 'error',
1096
+ code: 'r54-list-item-must-postmessage',
1097
+ componentId: cid,
1098
+ message: `${n.className} "${cid}" 在列表/网格 item 事件里直接修改绑定数据 —— 铁律:应 this.postMessage('globalSet', {key, value}) 透传,由根节点 onReady 的 addEventListener('globalSet') 监听代改(范式见 recipes.md §4 / depositFilter)`,
1099
+ })
1100
+ }
1101
+ }
1102
+ }, true)
1103
+ // R54-b: 用了 globalSet 透传但根节点 onReady 没注册监听 → 消息石沉大海
1104
+ const allCode = []
1105
+ let rootOnReadyCode = ''
1106
+ walk(view, (n) => {
1107
+ for (const ea of n.eventactions || []) {
1108
+ const code = codeOf(ea)
1109
+ if (!code) continue
1110
+ allCode.push(code)
1111
+ if (n.className === 'Root' && ((ea || {}).event || {}).eventKey === 'onReady') rootOnReadyCode += code
1112
+ }
1113
+ })
1114
+ if (allCode.some((c) => /postMessage\(\s*['"]globalSet['"]/.test(c)) && !/addEventListener\(\s*['"]globalSet['"]/.test(rootOnReadyCode)) {
1115
+ out.push({
1116
+ severity: 'error',
1117
+ code: 'r54-globalset-no-root-listener',
1118
+ componentId: view.componentId || '(root)',
1119
+ message: `卡片使用 postMessage('globalSet') 透传,但根节点 onReady 未 addEventListener('globalSet') 注册监听 —— 消息无人消费;先在根节点初始化事件绑定监听(范式见 recipes.md §4)`,
1120
+ })
1121
+ }
1122
+
1123
+ // R55: customAction 事件脚本禁止出现 $$(铁律)—— slotScope item 字段已展开到 this.state,
1124
+ // this.state.<field> 直接可读(这是平台保证,不是需要兜底判断的不确定行为)
1125
+ walk(view, (n) => {
1126
+ const cid = n.componentId || '(root)'
1127
+ for (const ea of n.eventactions || []) {
1128
+ if (((ea || {}).action || {}).actionType !== 'customAction') continue
1129
+ const code = codeOf(ea)
1130
+ if (!code.includes('$$')) continue
1131
+ out.push({
1132
+ severity: 'error',
1133
+ code: 'r55-event-no-dollar-dollar',
1134
+ componentId: cid,
1135
+ message: `${n.className} "${cid}" 事件脚本出现 "$$" —— 铁律:customAction 里 slotScope 当前 item 的字段已展开到 this.state,直接 this.state.<field> 读取(如 this.state.query);禁止 $$ / this.state.$$ / $$.field 及任何 "$$ 兜底判断" 写法`,
1136
+ })
1137
+ }
1138
+ })
1139
+
1140
+ // R56: 事件脚本调 JSAPI 直接 ynet.callJSAPI(...)(铁律)—— ynet 由平台注入为全局,
1141
+ // 禁止 window.ynet 前缀与 typeof window / window.ynet 存在性守卫(导出卡全量裸调为证)
1142
+ walk(view, (n) => {
1143
+ const cid = n.componentId || '(root)'
1144
+ for (const ea of n.eventactions || []) {
1145
+ if (((ea || {}).action || {}).actionType !== 'customAction') continue
1146
+ const code = codeOf(ea)
1147
+ if (!/window\.ynet|typeof window/.test(code)) continue
1148
+ out.push({
1149
+ severity: 'error',
1150
+ code: 'r56-jsapi-no-window-guard',
1151
+ componentId: cid,
1152
+ message: `${n.className} "${cid}" 事件脚本用 window.ynet 前缀或 typeof window 存在性守卫调 JSAPI —— 铁律:ynet 由平台注入为全局,事件里直接 ynet.callJSAPI(name, params, cb),不需要 window. 前缀,也不需要任何存在性判断`,
1153
+ })
1154
+ }
1155
+ })
1156
+
1157
+ // R57: 透明度(other.opacity)禁止用表达式/绑定 —— 移动端渲染不支持透明度表达式,
1158
+ // 必须走「透明度切换固定范式」:按用到的透明度取值生成 N 个静态变体节点,
1159
+ // 统一套进一层与原节点等宽高 + overflow:hidden 的 Row/Column 容器,
1160
+ // 事件挂在容器上,变体用 v-show 互斥显隐实现视觉上的透明度切换。
1161
+ // 见 references/recipes.md §17 + recipe_snippets/opacity-switch.json
1162
+ walk(view, (n) => {
1163
+ const cid = n.componentId || '(root)'
1164
+ const otherGroup = ((n.style || {}).properties || []).find((p) => p.itemKey === 'other')
1165
+ const opacityChild = (((otherGroup || {}).itemChildren || {}).properties || []).find(
1166
+ (c) => c.itemKey === 'opacity'
1167
+ )
1168
+ if (!opacityChild) return
1169
+ const isExpr =
1170
+ opacityChild.itemValueFrom === '1' ||
1171
+ (typeof opacityChild.itemValue === 'string' && opacityChild.itemValue.includes('$'))
1172
+ if (!isExpr) return
1173
+ out.push({
1174
+ severity: 'error',
1175
+ code: 'r57-opacity-expression',
1176
+ componentId: cid,
1177
+ message: `${n.className} "${cid}" 的 other.opacity 用了表达式/绑定("${opacityChild.itemValue}")—— 移动端渲染不支持透明度表达式(R57 铁律)。改用透明度切换固定范式:按用到的透明度取值生成等量静态变体节点(opacity 写常量),外套一层与原节点等宽高且 overflow:hidden 的 Row/Column 容器,点击等事件移到容器上,变体之间用互斥 v-show 显隐切换。详见 references/recipes.md §17`,
1178
+ })
1179
+ })
1180
+
1181
+ // R-FMT3: Icon 必须有 click eventaction // R-FMT3: Icon 必须有 click eventaction(否则 onClick 双触发,见 rules.md §九)
1182
+ walk(view, (n) => {
1183
+ const cid = n.componentId || '(root)'
1184
+ if (n.className === 'Icon') {
1185
+ const hasClick = (n.eventactions || []).some(
1186
+ (ea) => ea && ea.event && ea.event.eventKey === 'click'
1187
+ )
1188
+ if (!hasClick) {
1189
+ out.push({
1190
+ severity: 'error',
1191
+ code: 'icon-no-click',
1192
+ componentId: cid,
1193
+ message: `Icon "${cid}" 没有 click 事件 —— Icon 不绑定 click 会导致 onClick 双触发(rules.md §九)。即使当前无交互也必须声明 click eventaction`,
1194
+ })
1195
+ }
1196
+ }
1197
+ })
1198
+
1199
+ // R-FMT4: MultiText itemList 项的 itemValueFrom:"1" 必须带 itemDefaultValue(渲染 undefined 兜底)
1200
+ walk(view, (n) => {
1201
+ const cid = n.componentId || '(root)'
1202
+ if (n.className === 'MultiText') {
1203
+ const textListProp = (n.properties || []).find((p) => p.itemKey === 'textList')
1204
+ if (textListProp && textListProp.itemList) {
1205
+ for (const item of textListProp.itemList) {
1206
+ const itemProps = (item.properties) || []
1207
+ for (const sp of itemProps) {
1208
+ if (sp.itemValueFrom === '1' && sp.itemKey === 'value' && !('itemDefaultValue' in sp)) {
1209
+ out.push({
1210
+ severity: 'error',
1211
+ code: 'multitext-item-no-default',
1212
+ componentId: cid,
1213
+ message: `MultiText "${cid}" 的 itemList 项 value 绑定(itemValueFrom:"1")缺 itemDefaultValue —— 渲染时 binding undefined 会显示空(rules.md §三)`,
1214
+ })
1215
+ }
1216
+ }
1217
+ }
1218
+ }
1219
+ }
1220
+ })
1221
+
1222
+ // R9/R10: 卡片根节点(view)不得设四周 padding/margin(裁切后间距归零铁律)
1223
+ {
1224
+ const rootStyleProps = (view.style && view.style.properties) || []
1225
+ for (const sp of rootStyleProps) {
1226
+ if (sp.itemKey === 'padding' || sp.itemKey === 'margin') {
1227
+ const children = (sp.itemChildren && sp.itemChildren.properties) || []
1228
+ for (const k of children) {
1229
+ if (k.itemKey !== sp.itemKey + 'Type' && k.itemValue) {
1230
+ const val = String(k.itemValue)
1231
+ if (val !== '0' && val !== '') {
1232
+ out.push({
1233
+ severity: 'error',
1234
+ code: 'r9-root-nonzero-padding',
1235
+ componentId: '(view)',
1236
+ message: `view(Root)设了 ${sp.itemKey}.${k.itemKey}="${val}" —— 裁切后间距归零铁律(R9/R10):卡片根节点不设任何四周 padding/margin,间距下沉到内层内容容器`,
1237
+ })
1238
+ }
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+ }
1244
+
1245
+
1246
+ // R-FMT5: Image 静态路径(不以 $ 开头、不含表达式)用 itemValueFrom:"1" → 应为 "2"(图片值)
1247
+ walk(view, (n) => {
1248
+ const cid = n.componentId || '(root)'
1249
+ if (n.className === 'Image') {
1250
+ const valProp = (n.properties || []).find((p) => p.itemKey === 'value')
1251
+ if (valProp && valProp.itemValueFrom === '1') {
1252
+ const val = String(valProp.itemValue || '')
1253
+ // 不是变量表达式(不以 $ 开头、不含 ? : 三元)→ 静态路径,应该用 "2"
1254
+ if (!val.startsWith('$') && !val.includes('?')) {
1255
+ out.push({
1256
+ severity: 'error',
1257
+ code: 'fmt-image-static-path-valuefrom',
1258
+ componentId: cid,
1259
+ message: `Image "${cid}" value="${val}" 用了 itemValueFrom:"1"(变量/表达式),但这是静态图片路径,应改为 itemValueFrom:"2"(图片/文件值)。itemValueFrom:"1" 会把路径当表达式解析,渲染器找不到变量导致破图`,
1260
+ })
1261
+ }
1262
+ }
1263
+ }
1264
+ })
1265
+
1266
+ // R42: 标题+下划线/装饰条拓扑(export-proven title-underline-overlay)
1267
+ // AbsoluteLayout 必须显式 align:BottomLeft;装饰条先绘制、标题后绘制;宽度建议随标题长度表达式变化。
1268
+ walk(view, (n) => {
1269
+ if (n.className !== 'AbsoluteLayout') return
1270
+ const cid = n.componentId || '(root)'
1271
+ const children = directChildren(n)
1272
+ const textIndex = children.findIndex((c) => c.className === 'Text')
1273
+ const decorationIndex = children.findIndex((c) => {
1274
+ if (!c) return false
1275
+ if (c.className === 'Divider') return true
1276
+ if (c.className !== 'Row') return false
1277
+ const background = (c.style?.properties || []).find((p) => p.itemKey === 'background')
1278
+ return Boolean(background)
1279
+ })
1280
+ if (textIndex === -1 || decorationIndex === -1) return
1281
+
1282
+ const align = (n.properties || []).find((p) => p.itemKey === 'align')
1283
+ if (!align || align.itemValue !== 'BottomLeft') {
1284
+ out.push({
1285
+ severity: 'error',
1286
+ code: 'r42-title-underline-align',
1287
+ componentId: cid,
1288
+ message: `AbsoluteLayout "${cid}" 包含标题+装饰条但未设 align:"BottomLeft" —— 后续兄弟会感知装饰条高度,破坏标题底部节奏。必须复用 title-underline-overlay 配方。`,
1289
+ })
1290
+ }
1291
+
1292
+ if (decorationIndex > textIndex) {
1293
+ out.push({
1294
+ severity: 'error',
1295
+ code: 'r42-title-underline-z-order',
1296
+ componentId: cid,
1297
+ message: `AbsoluteLayout "${cid}" 中装饰条排在标题之后 —— 下划线会绘制在文字上方。子节点顺序必须为 [装饰条, 标题]。`,
1298
+ })
1299
+ }
1300
+
1301
+ const decoration = children[decorationIndex]
1302
+ const rect = (decoration.style?.properties || []).find((p) => p.itemKey === 'rect')
1303
+ const width = ((rect?.itemChildren?.properties || []).find((p) => p.itemKey === 'width') || {}).itemValue
1304
+ if (!String(width || '').includes('.length *')) {
1305
+ out.push({
1306
+ severity: 'warn',
1307
+ code: 'r42-title-underline-fixed-width',
1308
+ componentId: cid,
1309
+ message: `AbsoluteLayout "${cid}" 的装饰条宽度未绑定标题长度表达式 —— 应使用 $titleVar.length * fontSize,避免标题变化后高亮条宽度不同步。`,
1310
+ })
1311
+ }
1312
+ })
1313
+
1314
+ return out
1315
+ }
1316
+
1317
+ function justifyValue(node) {
1318
+ if (!node) return ''
1319
+ return String(node.itemValue || '')
1320
+ }
1321
+
1322
+ /* ------------------------------------------------------------------ */
1323
+ /* 渲染 */
1324
+ /* ------------------------------------------------------------------ */
1325
+ function severityIcon(s) {
1326
+ return s === 'error' ? '❌' : s === 'warn' ? '⚠️ ' : 'ℹ️ '
1327
+ }
1328
+
1329
+ function renderHuman(reports, id) {
1330
+ const lines = []
1331
+ for (const { id: cardId, findings, error } of reports) {
1332
+ lines.push(`[lint] card=${cardId}`)
1333
+ if (error) {
1334
+ lines.push(` ❌ 无法体检:${error}`)
1335
+ lines.push('')
1336
+ continue
1337
+ }
1338
+ if (!findings.length) {
1339
+ lines.push(` ✅ 无 lint 报警`)
1340
+ } else {
1341
+ const bySev = { error: 0, warn: 0, info: 0 }
1342
+ for (const f of findings) bySev[f.severity]++
1343
+ lines.push(` 共 ${findings.length} 条:${bySev.error} error / ${bySev.warn} warn / ${bySev.info} info`)
1344
+ for (const f of findings) {
1345
+ const cid = f.componentId ? ` [${f.componentId}]` : ''
1346
+ lines.push(` ${severityIcon(f.severity)} ${f.code}${cid}: ${f.message}`)
1347
+ }
1348
+ }
1349
+ lines.push('')
1350
+ }
1351
+ return lines.join('\n')
1352
+ }
1353
+
1354
+ /* ------------------------------------------------------------------ */
1355
+ /* main */
1356
+ /* ------------------------------------------------------------------ */
1357
+ function lintOne(id) {
1358
+ const sp = cardSchemaPath(id)
1359
+ const dp = cardDataPath(id)
1360
+ if (!fs.existsSync(sp)) throw new Error(`卡片不存在: ${sp}`)
1361
+ const schema = readJson(sp)
1362
+ const data = fs.existsSync(dp) ? readJson(dp, {}) : {}
1363
+ return { id, findings: lint(schema, data) }
1364
+ }
1365
+
1366
+ function main() {
1367
+ const args = parseArgs(process.argv.slice(2))
1368
+ const ids = args.all
1369
+ ? fs.readdirSync(cardsDir).filter((d) => fs.existsSync(cardSchemaPath(d)))
1370
+ : [args.id]
1371
+
1372
+ const reports = []
1373
+ for (const id of ids) {
1374
+ try {
1375
+ reports.push(lintOne(id))
1376
+ } catch (e) {
1377
+ reports.push({ id, error: e.message })
1378
+ }
1379
+ }
1380
+
1381
+ if (args.json) {
1382
+ console.log(JSON.stringify({ reports }, null, 2))
1383
+ } else {
1384
+ console.log(renderHuman(reports))
1385
+ }
1386
+
1387
+ const hasError = reports.some((r) => r.findings && r.findings.some((f) => f.severity === 'error'))
1388
+ const hasWarn = reports.some((r) => r.findings && r.findings.some((f) => f.severity === 'warn'))
1389
+ // error 永远卡 CI;warn 仅在 --strict 时卡
1390
+ // 用 exitCode 而非 process.exit():stdout 是管道时 exit() 会截断未 flush 的大体积 JSON 输出
1391
+ process.exitCode = hasError ? 1 : (hasWarn && args.strict ? 1 : 0)
1392
+ }
1393
+
1394
+ // 仅当作为 CLI 直接运行时才启动 main()(被 import 时不执行)
1395
+ const _thisScript = fs.realpathSync(fileURLToPath(import.meta.url))
1396
+ const _invokedAs = (process.argv[1] && fs.existsSync(process.argv[1]))
1397
+ ? fs.realpathSync(process.argv[1])
1398
+ : process.argv[1]
1399
+ if (_invokedAs === _thisScript) main()