cardbuddy 0.14.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
  3. package/dist/preview-editor/assets/index-Bro07DTa.js +313 -0
  4. package/dist/preview-editor/index.html +2 -2
  5. package/dist/standalone.js +3 -0
  6. package/dist/vendored/card-editor/runtime/adapter.js +2 -2
  7. package/dist/vendored/mcp/client.mjs +9 -2
  8. package/dist/vendored/server/core/ai-chat.js +1702 -134
  9. package/dist/vendored/server/core/browser.js +12 -3
  10. package/dist/vendored/server/core/cardFiles.js +3 -0
  11. package/dist/vendored/server/core/cardHistory.js +1 -0
  12. package/dist/vendored/server/core/cardImportMeta.js +7 -6
  13. package/dist/vendored/server/core/cardOpenFolder.js +1 -0
  14. package/dist/vendored/server/core/cardSave.js +1 -0
  15. package/dist/vendored/server/core/cardSync.js +3 -0
  16. package/dist/vendored/server/core/cardValidate.js +16 -5
  17. package/dist/vendored/server/core/codeBridge.js +5 -1
  18. package/dist/vendored/server/core/logStream.js +1 -0
  19. package/dist/vendored/server/core/sliceStatic.js +24 -4
  20. package/dist/vendored/server/core/sliceUpload.js +1 -0
  21. package/dist/vendored/server/core/turnJournal.js +6 -0
  22. package/dist/vendored/skills/card-skill-trainer/SKILL.md +257 -201
  23. package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
  24. package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
  25. package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
  26. package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
  27. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
  28. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
  29. package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
  30. package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
  31. package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
  32. package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
  33. package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
  34. package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
  35. package/dist/vendored/skills/dsl-card-ai/SKILL.md +1 -1
  36. package/dist/vendored/skills/dsl-card-generator/SKILL.md +208 -380
  37. package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
  38. package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
  39. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +233 -61
  40. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +202 -4
  41. package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +87 -0
  42. package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +1 -1
  43. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +42 -33
  44. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +59 -11
  45. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +44 -8
  46. package/dist/vendored/skills/dsl-card-generator/references/rules.md +26 -6
  47. package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +54 -0
  48. package/dist/vendored/skills/dsl-card-generator/references/semantic_adjudication.md +23 -0
  49. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
  50. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +33 -4
  51. package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +13 -1
  52. package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
  53. package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +133 -2
  54. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +226 -19
  55. package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +6 -1
  56. package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
  57. package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +92 -6
  58. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +277 -27
  59. package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +147 -0
  60. package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +12 -8
  61. package/dist/vendored/skills/dsl-card-generator/scripts/ir_merge.mjs +288 -0
  62. package/dist/vendored/skills/dsl-card-generator/scripts/ir_restore.mjs +175 -0
  63. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +1015 -54
  64. package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
  65. package/dist/vendored/skills/dsl-card-generator/scripts/lib/geometry_audit.mjs +131 -0
  66. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
  67. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout_rules.mjs +160 -0
  68. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
  69. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
  70. package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
  71. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
  72. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
  73. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
  74. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
  75. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
  76. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
  77. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
  78. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
  79. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +17 -5
  80. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +102 -0
  81. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +5727 -0
  82. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +4214 -0
  83. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +1110 -0
  84. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +322 -71
  85. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +87 -43
  86. package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
  87. package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
  88. package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +91 -43
  89. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +7 -11
  90. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +272 -46
  91. package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +43 -10499
  92. package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +188 -0
  93. package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
  94. package/dist/vendored/skills/shared/exit-codes.md +79 -0
  95. package/dist/vendored/skills/shared/spec-contract.md +118 -0
  96. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +43 -20
  97. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +4 -4
  98. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +47 -9
  99. package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
  100. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +546 -91
  101. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +51 -5
  102. package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +217 -0
  103. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +64 -34
  104. package/package.json +1 -1
  105. package/dist/preview-editor/assets/index-Cm0USikc.js +0 -307
  106. package/dist/preview-editor/assets/index-cbTCHp8J.css +0 -1
  107. package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
  108. package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
@@ -15,15 +15,11 @@ paths.py — skill 内部路径与宿主项目路径单一来源(Python 侧)
15
15
  DSL_CARD_PREVIEW_DIR 自包含预览目录(默认 <skill>/vendor/preview)
16
16
  DSL_CARD_BASELINES_DIR 渲染基线目录(默认 <project>/.card-baselines)
17
17
  """
18
- import json
19
18
  import os
20
19
  from pathlib import Path
21
20
 
22
21
  _HERE = Path(__file__).resolve().parent
23
22
 
24
- # 占位 sentinel:与任何用户传入的 fallback 都不会相同的内部对象。
25
- _MISSING = object()
26
-
27
23
  # 当前 skill 根目录:scripts/lib -> scripts -> dsl-card-generator
28
24
  skill_root = _HERE.parent.parent
29
25
 
@@ -93,36 +89,3 @@ host_skills_dir = _host_skills_dir()
93
89
  def card_dir(card_id: str) -> Path:
94
90
  """解析卡片目录的绝对路径:`<cards_dir>/<id>`。"""
95
91
  return cards_dir / card_id
96
-
97
-
98
- def card_schema_path(card_id: str) -> Path:
99
- """解析卡片 schema.json 的绝对路径。"""
100
- return cards_dir / card_id / "schema.json"
101
-
102
-
103
- def card_data_path(card_id: str) -> Path:
104
- """解析卡片 data.json 的绝对路径。"""
105
- return cards_dir / card_id / "data.json"
106
-
107
-
108
- def read_json(p: Path, fallback=_MISSING):
109
- """
110
- 安全读取 JSON(不存在或解析失败时返回 fallback)。
111
-
112
- Args:
113
- p: 路径
114
- fallback: 不存在/解析失败时的返回值。默认抛错。
115
- Returns:
116
- 解析后的 Python 值
117
- """
118
- p = Path(p)
119
- if not p.is_file():
120
- if fallback is _MISSING:
121
- raise FileNotFoundError(f"File not found: {p}")
122
- return fallback
123
- try:
124
- return json.loads(p.read_text(encoding="utf-8"))
125
- except Exception as e:
126
- if fallback is _MISSING:
127
- raise RuntimeError(f"Cannot parse JSON {p}: {e}") from e
128
- return fallback
@@ -0,0 +1,99 @@
1
+ /**
2
+ * python.mjs — 跨平台 Python 解释器解析的**单一来源**(Node 侧)。
3
+ *
4
+ * 为什么必须真跑一次 `--version`(而不是只看「能不能 spawn」):Windows 上 `python3` 常是
5
+ * Microsoft Store 的**应用执行别名** —— 进程能启动,却立刻以 9009 退出,这不是 ENOENT。
6
+ * 只按 spawn 异常判断就会钉死这个假解释器,autofix / validate 全部静默不执行,调用方
7
+ * 收到的是一个**永远修不好**的 validate error(实测模型为它空转 20 轮,每轮重跑 iterate)。
8
+ *
9
+ * 为什么集中到本文件:同一件事此前在 5 个脚本里各写一遍,能力互不相同 ——
10
+ * · `expand_ir.mjs` 真探测 + `py`(原本最完整,本文件的基准)
11
+ * · `route_input.mjs` 真探测 + `py`,但不认 `WORKBUDDY_PYTHON`
12
+ * · `add_component.mjs` 不真探测、不认 `py`
13
+ * · `gate.mjs` / `v3_regression.mjs` 只读 `process.env.PYTHON || 'python3'`(**完全不探测**)
14
+ * · `register_card_template.mjs` 只读 `DSL_PYTHON`、不认 `py`
15
+ * 后果:同一台机器上 `gate.mjs` 判定「没有 Python」而 `expand_ir.mjs` 跑得通。
16
+ *
17
+ * 环境变量链(前者优先,命中即停止回退到自动探测):
18
+ * `DSL_CARD_PYTHON`(权威)> `WORKBUDDY_PYTHON` > `DSL_PYTHON` > `PYTHON`
19
+ * 后两项为历史兼容(分别在 register_card_template / gate / v3_regression 里出现过),
20
+ * **新代码一律用 `DSL_CARD_PYTHON`**。
21
+ */
22
+ import { spawnSync } from 'node:child_process'
23
+
24
+ /** 显式指定的解释器,按优先级排列;命中即不再做自动探测。 */
25
+ export const PYTHON_ENV_KEYS = ['DSL_CARD_PYTHON', 'WORKBUDDY_PYTHON', 'DSL_PYTHON', 'PYTHON']
26
+
27
+ /** 权威环境变量名(文档 / 报错提示里要用户设置的那个)。 */
28
+ export const PYTHON_ENV_PRIMARY = 'DSL_CARD_PYTHON'
29
+
30
+ /** 自动探测顺序:`py` 是 Windows 官方启动器,`python3` 可能是 Store 假别名故不设优先级。 */
31
+ export const PYTHON_AUTO_BINS = ['python3', 'python', 'py']
32
+
33
+ /**
34
+ * 真跑一次 `--version` 判断解释器可用(识别 Windows Store 假别名)。
35
+ * @param {string} bin
36
+ * @returns {boolean}
37
+ */
38
+ export function probePython(bin) {
39
+ const b = String(bin || '').trim()
40
+ if (!b) return false
41
+ try {
42
+ const r = spawnSync(b, ['--version'], {
43
+ encoding: 'utf8',
44
+ stdio: ['ignore', 'pipe', 'pipe'],
45
+ timeout: 5000,
46
+ })
47
+ if (r.status !== 0) return false
48
+ return /Python \d/i.test(`${r.stdout || ''}${r.stderr || ''}`)
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 候选解释器列表(去重、保持优先级)。
56
+ *
57
+ * 语义与 `expand_ir.mjs` 原实现一致:显式指定且探测通过 → 只返回它;显式指定但探测不过
58
+ * → 仍保留一次机会(可能是带参数的包装器);全探测失败 → 回落到常见名字,让报错里能带出
59
+ * 真实 stderr,而不是一句笼统的「python 不可用」。
60
+ *
61
+ * @returns {string[]}
62
+ */
63
+ export function pythonCandidates() {
64
+ const out = []
65
+ const seen = new Set()
66
+ const push = (bin) => {
67
+ const b = String(bin || '').trim()
68
+ if (!b || seen.has(b)) return
69
+ seen.add(b)
70
+ out.push(b)
71
+ }
72
+
73
+ for (const key of PYTHON_ENV_KEYS) {
74
+ const val = process.env[key]
75
+ if (!val) continue
76
+ if (probePython(val)) {
77
+ push(val)
78
+ return out // 显式指定且可用:不再回退到其它解释器
79
+ }
80
+ push(val) // 探测不过也留一次机会(包装器 / 路径含参数)
81
+ }
82
+
83
+ for (const bin of PYTHON_AUTO_BINS) if (probePython(bin)) push(bin)
84
+ for (const bin of ['python3', 'python']) push(bin)
85
+ return out
86
+ }
87
+
88
+ /**
89
+ * 最优先的可用解释器(总是返回可用的命令名,探测全失败时返回 `python3` 以便报错原样带出)。
90
+ * @returns {string}
91
+ */
92
+ export function resolvePython() {
93
+ return pythonCandidates()[0] || 'python3'
94
+ }
95
+
96
+ /** 当前环境是否**有**可用 Python(供「没装就别跑」的早退分支用)。 */
97
+ export function hasPython() {
98
+ return probePython(resolvePython())
99
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * rules/adaptive-width.mjs —— 宽度自适应契约的机械判据(ir-format.md §2f)。
3
+ *
4
+ * 背景(2026-09-25 汇率卡事故):容器类节点把「设计稿测量宽」写死成 px
5
+ * (如第一层主容器 rect.width="343"),卡片在非设计宽度的宿主容器里
6
+ * 无法自适应——Root 撑满了,内容层却锁死在 343px。
7
+ *
8
+ * 渲染契约(component-meta.json / components.css 实证):
9
+ * - 全部容器组件的 meta 默认 rect.width 就是 "100%"(覆盖默认 = 显式声明固定);
10
+ * - adapter.doSize 对 '%' 值原样透传(不做 px 换算);
11
+ * - 纵向流(Root/Column/AbsoluteLayout)里子项交叉轴默认 stretch。
12
+ * 因此「占满语义」永远不该写成 px:写 100%(显式)或不写(默认)都自适应,
13
+ * 且在以卡面宽为基准的渲染/截图环境下与原 px 值逐像素等价(父内容盒宽 == 原 px)。
14
+ *
15
+ * 判定口径(全部是格式/几何/结构特征,与任何业务无关):
16
+ * 1. 节点类型 ∈ 容器白名单(meta 默认 100% 的那批);
17
+ * 2. rect.width 是纯数字(px 字面量;'100%'/'auto'/表达式不在此列);
18
+ * 3. 父内容盒宽可计算(Root 直系 = view.width;父定宽 = 父宽 − 父水平 padding;
19
+ * 父 100%/无宽则透传,透传链不可达时跳过);
20
+ * 4. 宽度占满父内容盒(98% ~ 103%,容差 2px 量级的标注/取整误差);
21
+ * **超出型不报**(ratio > 1.03:绝对层故意越出卡面的形态,属另一话题);
22
+ * 5. 父为纵向流(Root/Column/AbsoluteLayout 或显式 flexDirection=column)——
23
+ * Row/MultiColumn 主轴语境里并列子项定宽是 space-between 等范式的合法组成
24
+ * (ir-format.md R-FIXEDW / R-COMPARE),不在此规则射程。
25
+ */
26
+ import { hit } from './walk.mjs'
27
+
28
+ const FULLWIDTH_CONTAINERS = new Set([
29
+ 'Row', 'Column', 'MultiColumn', 'ListLayout', 'GridLayout', 'ViewContainer',
30
+ ])
31
+
32
+ /** 纵向流父级:显式 flexDirection=column 优先,否则按渲染器默认方向推断。 */
33
+ function isColumnAxis(node) {
34
+ const fl = (node?.style?.properties || []).find((g) => g.itemKey === 'flex')
35
+ const dir = (fl?.itemChildren?.properties || []).find((c) => c.itemKey === 'flexDirection')?.itemValue
36
+ if (dir === 'column') return true
37
+ if (dir === 'row') return false
38
+ const cls = node?.className
39
+ return cls === 'Root' || cls === 'Column' || cls === 'AbsoluteLayout'
40
+ }
41
+
42
+ /** rect.width 纯数字(px 字面量)→ 数值;无/百分比/auto/表达式 → null。 */
43
+ function fixedPxWidth(node) {
44
+ const rect = (node?.style?.properties || []).find((g) => g.itemKey === 'rect')
45
+ const child = (rect?.itemChildren?.properties || []).find((c) => c.itemKey === 'width')
46
+ if (!child) return null
47
+ const raw = String(child.itemValue ?? '').trim()
48
+ if (!/^-?\d+(\.\d+)?$/.test(raw)) return null
49
+ const n = Number(raw)
50
+ return Number.isFinite(n) && n > 0 ? n : null
51
+ }
52
+
53
+ /** 水平 padding 合计(单值 padding 或 paddingLeft+paddingRight)。 */
54
+ function horizontalPadding(node) {
55
+ const g = (node?.style?.properties || []).find((x) => x.itemKey === 'padding')
56
+ let all = 0
57
+ let left = 0
58
+ let right = 0
59
+ for (const c of g?.itemChildren?.properties || []) {
60
+ if (c.itemKey === 'padding') all = Number(c.itemValue) || 0
61
+ if (c.itemKey === 'paddingLeft') left = Number(c.itemValue) || 0
62
+ if (c.itemKey === 'paddingRight') right = Number(c.itemValue) || 0
63
+ }
64
+ return (left || all) + (right || all)
65
+ }
66
+
67
+ export function collectFullwidthFixedPx(view) {
68
+ const out = []
69
+ if (!view || typeof view !== 'object') return out
70
+ const viewW = Number(String(view.width ?? '').replace(/[^0-9.\-]/g, '')) || 0
71
+
72
+ /**
73
+ * @param node 当前节点
74
+ * @param parentContent 父节点的内容盒宽(父解析宽 − 父水平 padding;Root = view.width − Root padding)
75
+ * —— 子节点 rect.width:"100%" 的解析基准就是这个值(doSize 对 % 原样透传)。
76
+ * 父为 '100%'/无宽时解析宽 = 透传 parentContent;父定宽 px 时 = 定宽值。
77
+ */
78
+ const walk = (node, parentContent, isRoot) => {
79
+ if (!node || typeof node !== 'object') return
80
+ const selfResolved = isRoot ? viewW : (() => {
81
+ const w = fixedPxWidth(node)
82
+ return w !== null ? w : parentContent
83
+ })()
84
+ const selfContent = Math.max(0, selfResolved - horizontalPadding(node))
85
+ const kids = [
86
+ ...(node.components || []),
87
+ ...((node.slots || []).flatMap((s) => (s && s.components) || [])),
88
+ ]
89
+ for (const child of kids) {
90
+ if (!child || typeof child !== 'object') continue
91
+ const w = fixedPxWidth(child)
92
+ if (selfContent > 0 && w !== null && FULLWIDTH_CONTAINERS.has(child.className)
93
+ && isColumnAxis(node)) {
94
+ const ratio = w / selfContent
95
+ if (ratio >= 0.98 && ratio <= 1.03) {
96
+ out.push(hit('r66-fullwidth-px', 'error', child.componentId || '',
97
+ `${child.className} "${child.componentId || child.name || ''}" 在纵向流父级里把占满语义写死成 rect.width="${w}"(父内容盒宽 ≈ ${Math.round(selfContent)})——宿主容器宽度一旦偏离设计基准,内容层锁死不自适应(ir-format.md §2f)。修法:改 width:"100%"(或不写 width,meta 默认即 100%),与设计宽逐像素等价且随容器自适应;真固定宽元素(图标/头像/徽章/图表画布)保留 px。注意 100% 节点禁叠水平 margin(rules.md §十六 R63),水平内缩写父容器 padding。`))
98
+ }
99
+ }
100
+ walk(child, selfContent, false)
101
+ }
102
+ }
103
+ walk(view, 0, true)
104
+ return out
105
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * rules/bindings.mjs —— 列表数组化与数据绑定完备的共享判定(lint_card / readiness 单一来源)。
3
+ *
4
+ * 为什么必须有它(2026-09-25 用户反馈):
5
+ * 「数据绑定齐全(尤其是列表化的数据,应该在视觉分析层面就明确用数组来绑定列表)」
6
+ * 三个实测疏漏形态,此前没有任何门禁能发现:
7
+ * ① 列表数据建了、视图零消费 —— transferPlan.payeeList / dreamPlan.planTimeList /
8
+ * creditCardPayment.myCardList / branchdata.businessScopeList /
9
+ * financialProductDetail.tradeRules(全仓 75 卡对账实测):data.json 里数组静置,
10
+ * schema 整段静态展开或根本没建,像素分照样高,数据维度完全缺失;
11
+ * ② 同构 tile 静态展开 —— ≥3 个结构相同的兄弟子树、值全是字面量(评分星 / tab 标签 /
12
+ * 固定按钮组是**合法**静态形态,tile 特征「值逐行互异」才能区分真数据行);
13
+ * ③ ListLayout/GridLayout 没写 item 模板(slotScope 插槽为空)或 value 没绑数组
14
+ * —— 渲染 0 项,列表整段空白。
15
+ *
16
+ * ── 消费判定(宽收口防误报;判据经全仓 75 卡实测校准)────────────────
17
+ * 数组变量被以下任一途径引用即视为「被消费」:
18
+ * a) 复现器(ListLayout/GridLayout)value 的 $表达式 引用(含 `$$` 整项与三元/切片复合式)
19
+ * b) 复现器 slotScope 插槽内的 $<字段> 引用 —— 字段名 ∈ 数组元素键集合
20
+ * c) 事件代码(setData / this.state|data 赋值 / 任意代码文本)中出现变量名
21
+ * —— monthlyBill 的 $renderListData 由 customAction 运行时转造、$listData 是其源,
22
+ * 不扫代码会把这对合法形态误判成「一悬空一未消费」(readiness.mjs 已有同款结论)
23
+ * d) 任意属性 / 样式 / 指令表达式中引用(v-show 条件、bindHints 等)
24
+ * 未消费且**名字带列表语义**(list|items|rows|records|details|rules|entries)→ 硬伤;
25
+ * 其余未消费(radarData / donutData 这类 Canvas 图表数据、theme 配置数组)→ 只统计不拦
26
+ * —— 名单语义来自「数据建的什么」,拦不拦看「视觉提名了什么」(readiness 的 lists 门)。
27
+ */
28
+ import { walkNodes, hit } from './walk.mjs'
29
+
30
+ /** 复现器类名(slotScope 只出现在这两类;与 component-meta 的插槽声明一致)。 */
31
+ export const REPEATER_CLASSES = new Set(['ListLayout', 'GridLayout'])
32
+
33
+ /** 列表语义名:命中即「未消费 = 硬伤」(拦像素循环)。名单来自全仓疏漏实测。 */
34
+ export const LIST_SEMANTIC_RE = /list|items|rows|records|details|rules|entries/i
35
+
36
+ /** 从 $表达式 抽根变量名(与 readiness.collectBindingRefs 同口径)。 */
37
+ export function extractRoots(value) {
38
+ const roots = new Set()
39
+ for (const m of String(value || '').matchAll(/\$([A-Za-z_][A-Za-z0-9_]*)/g)) roots.add(m[1])
40
+ return roots
41
+ }
42
+
43
+ /**
44
+ * 顶层数组变量表:data.json(渲染真值)→ mocks → inputParams.defaultValue 三来源合并。
45
+ * @returns {Map<string, {len:number, fields:Set<string>, source:string}>}
46
+ */
47
+ export function collectArrayVars(schema, data) {
48
+ const arrays = new Map()
49
+ const add = (name, v, source) => {
50
+ if (!name || !Array.isArray(v)) return
51
+ const fields = v[0] && typeof v[0] === 'object' ? new Set(Object.keys(v[0])) : new Set()
52
+ arrays.set(name, { len: v.length, fields, source })
53
+ }
54
+ for (const [k, v] of Object.entries(data || {})) add(k, v, 'data.json')
55
+ for (const [k, v] of Object.entries(schema?.dictionary?.mocks || {})) if (!arrays.has(k)) add(k, v, 'mocks')
56
+ for (const p of schema?.dictionary?.inputParams || []) if (p?.key && !arrays.has(p.key)) add(p.key, p.defaultValue, 'inputParams')
57
+ return arrays
58
+ }
59
+
60
+ /** 收集 schema 全部事件动作代码文本(自 readiness.mjs collectActionCode 单向收敛到此处)。 */
61
+ export function collectActionCode(schema) {
62
+ const out = []
63
+ const push = (v) => {
64
+ if (typeof v === 'string') {
65
+ if (v) out.push(v)
66
+ return
67
+ }
68
+ if (v && typeof v === 'object') {
69
+ for (const k of ['code', 'compiled', 'expression']) if (typeof v[k] === 'string' && v[k]) out.push(v[k])
70
+ }
71
+ }
72
+ const walk = (n) => {
73
+ if (!n || typeof n !== 'object') return
74
+ for (const ea of n.eventactions || []) {
75
+ const a = ea?.action
76
+ if (!a) continue
77
+ for (const k of ['code', 'compiled', 'expression', 'actionValue']) push(a[k])
78
+ for (const p of a.properties || []) push(p?.itemValue)
79
+ }
80
+ for (const c of n.components || []) walk(c)
81
+ for (const s of n.slots || []) for (const c of s.components || []) walk(c)
82
+ }
83
+ walk(schema?.view || schema)
84
+ return out
85
+ }
86
+
87
+ /** 节点全部属性值(properties + style 组子键),供表达式扫描。 */
88
+ function* propValues(node) {
89
+ for (const p of node.properties || []) if (p && typeof p.itemValue === 'string') yield p.itemValue
90
+ for (const g of node.style?.properties || []) {
91
+ for (const c of g.itemChildren?.properties || []) if (c && typeof c.itemValue === 'string') yield c.itemValue
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 数组消费对账。
97
+ * @returns {{ consumed:Set<string>, arrays:Map, unconsumed:Array<{name,len,semantic,source}>,
98
+ * repeaterCount:number, scopedListCount:number }}
99
+ * scopedListCount = 「value 绑定了数组」的复现器个数(readiness lists 门的对账基数)。
100
+ */
101
+ export function collectArrayConsumption(schema, data) {
102
+ const arrays = collectArrayVars(schema, data)
103
+ const consumed = new Set()
104
+ let repeaterCount = 0
105
+ let scopedListCount = 0
106
+ const view = schema?.view || schema
107
+
108
+ walkNodes(view, (n) => {
109
+ const isRepeater = REPEATER_CLASSES.has(n.className)
110
+ if (isRepeater) {
111
+ repeaterCount++
112
+ const vp = (n.properties || []).find((p) => p?.itemKey === 'value')
113
+ const roots = extractRoots(vp?.itemValue)
114
+ let hitsArray = false
115
+ for (const r of roots) if (arrays.has(r)) { consumed.add(r); hitsArray = true }
116
+ if (hitsArray) scopedListCount++
117
+ }
118
+ for (const val of propValues(n)) {
119
+ for (const r of extractRoots(val)) if (arrays.has(r)) consumed.add(r)
120
+ }
121
+ })
122
+ // 事件代码:变量名出现在代码里即消费(含 setData 转造源 / state 赋值)
123
+ for (const code of collectActionCode(schema)) {
124
+ for (const name of arrays.keys()) if (code.includes(name)) consumed.add(name)
125
+ }
126
+ // slotScope 插槽内的 $<字段> 引用:字段名命中某数组的元素键 ⇒ 该数组被模板消费
127
+ const markSlotFields = (n) => {
128
+ for (const val of propValues(n)) {
129
+ for (const r of extractRoots(val)) {
130
+ for (const [name, a] of arrays) if (a.fields.has(r)) consumed.add(name)
131
+ }
132
+ }
133
+ }
134
+ walkNodes(view, (n) => {
135
+ const isRepeater = REPEATER_CLASSES.has(n.className)
136
+ if (!isRepeater) return
137
+ for (const s of n.slots || []) {
138
+ if (!s?.slotScope) continue
139
+ for (const c of s.components || []) walkNodes(c, markSlotFields)
140
+ }
141
+ })
142
+
143
+ const unconsumed = [...arrays.keys()]
144
+ .filter((name) => !consumed.has(name))
145
+ .map((name) => ({ name, len: arrays.get(name).len, semantic: LIST_SEMANTIC_RE.test(name), source: arrays.get(name).source }))
146
+ return { consumed, arrays, unconsumed, repeaterCount, scopedListCount }
147
+ }
148
+
149
+ /**
150
+ * 复现器硬伤:value 没绑数组(渲染 0 项)或 slotScope 插槽无模板组件(渲染 N 个空壳)。
151
+ * 两条都必然是错的 —— 定 error。
152
+ */
153
+ export function collectRepeaterDefects(view) {
154
+ const out = []
155
+ walkNodes(view, (n) => {
156
+ if (!REPEATER_CLASSES.has(n.className)) return
157
+ const cid = n.componentId || ''
158
+ const vp = (n.properties || []).find((p) => p?.itemKey === 'value')
159
+ if (!vp || !String(vp.itemValue ?? '').trim()) {
160
+ out.push(hit(
161
+ 'binding-repeater-no-scope',
162
+ 'error',
163
+ cid,
164
+ `${n.className} "${cid}" 的 value 没绑数组表达式 → 渲染 0 项,列表整段空白。` +
165
+ 'value 用 itemValueFrom:"1" 绑 $数组变量(范式 ir-format.md §2e / recipe_snippets/list-expand-collapse.json)',
166
+ ))
167
+ return
168
+ }
169
+ const scoped = (n.slots || []).some((s) => s?.slotScope && (s.components || []).length)
170
+ if (!scoped) {
171
+ out.push(hit(
172
+ 'binding-repeater-no-scope',
173
+ 'error',
174
+ cid,
175
+ `${n.className} "${cid}" 没有带 item 模板的 slotScope 插槽(slots[].slotScope=true 且 components 非空)→ ` +
176
+ '数组有多少条就渲染多少个空壳。把一行的完整结构写进 slotScope 插槽,字段用 $<字段名> / $$ / $_index',
177
+ ))
178
+ }
179
+ })
180
+ return out
181
+ }
182
+
183
+ /** 同构签名:className 链(深度 ≤4),与子树内容无关的纯结构指纹。 */
184
+ function structuralSignature(root) {
185
+ const ks = []
186
+ const g = (nd, d) => {
187
+ if (!nd || typeof nd !== 'object' || d > 4) return
188
+ ks.push(nd.className || '?')
189
+ for (const c of nd.components || []) g(c, d + 1)
190
+ for (const s of nd.slots || []) for (const c of s.components || []) g(c, d + 1)
191
+ }
192
+ g(root, 0)
193
+ return ks.join('>')
194
+ }
195
+
196
+ /** 子树是否含 $ 绑定属性。 */
197
+ function hasBinding(root) {
198
+ let found = false
199
+ walkNodes(root, (n) => {
200
+ if (found) return
201
+ for (const val of propValues(n)) if (val.includes('$')) { found = true; return }
202
+ for (const ea of n.eventactions || []) if (String(ea?.action?.properties?.map((p) => p?.itemValue).join('') || '').includes('$')) found = true
203
+ })
204
+ return found
205
+ }
206
+
207
+ /** 子树的文本值序列(Text.value 字面量;绑定值不算 —— 全静态是前提)。 */
208
+ function textValues(root) {
209
+ const out = []
210
+ walkNodes(root, (n) => {
211
+ if (n.className !== 'Text') return
212
+ for (const p of n.properties || []) {
213
+ if (p?.itemKey === 'value' && typeof p.itemValue === 'string' && !p.itemValue.includes('$')) out.push(p.itemValue.trim())
214
+ }
215
+ })
216
+ return out
217
+ }
218
+
219
+ /**
220
+ * 同构 tile 静态展开(warn):复现器外 ≥3 个结构相同的兄弟子树、全静态、
221
+ * 且文本值逐行互异(「值互异」正是数据行特征 —— 评分星/同款图标行值全同,被此判据排除;
222
+ * 与 spec_audit.mjs pushCandidate 的装饰重复排除判据同源)。
223
+ */
224
+ export function collectStaticTileExpansion(view) {
225
+ const out = []
226
+ const visit = (node) => {
227
+ if (!node || typeof node !== 'object') return
228
+ const kids = []
229
+ for (const s of node.slots || []) for (const c of s.components || []) kids.push(c)
230
+ for (const c of node.components || []) kids.push(c)
231
+ const inRepeater = REPEATER_CLASSES.has(node.className)
232
+ if (!inRepeater && kids.length >= 3) {
233
+ const bySig = new Map()
234
+ for (const k of kids) {
235
+ const sig = structuralSignature(k)
236
+ if (!bySig.has(sig)) bySig.set(sig, [])
237
+ bySig.get(sig).push(k)
238
+ }
239
+ for (const [sig, group] of bySig) {
240
+ if (group.length < 3) continue
241
+ if (group.some(hasBinding)) continue
242
+ const rows = group.map(textValues).filter((r) => r.length >= 2)
243
+ if (rows.length < group.length) continue // tile 特征:每行 ≥2 条文本(单文本 = tab/按钮组,合法静态)
244
+ const distinct = new Set(rows.map((r) => JSON.stringify(r)))
245
+ if (distinct.size < 2) continue // 值全同 = 装饰性重复(同款图标行),不拦
246
+ out.push(hit(
247
+ 'binding-list-static-expansion',
248
+ 'warn',
249
+ node.componentId || '',
250
+ `${node.className || '?'} "${node.componentId || ''}" 下有 ${group.length} 个同构静态 tile(签名 ${sig.slice(0, 60)}…,值逐行互异)→ ` +
251
+ '疑似列表数据被静态展开:设计稿若是重复数据行,应改 ListLayout/GridLayout + 数组变量 + slotScope 模板' +
252
+ '(范式 ir-format.md §2e)。若确认是装饰性重复可忽略本预警',
253
+ ))
254
+ }
255
+ }
256
+ for (const c of kids) visit(c)
257
+ }
258
+ visit(view)
259
+ return out
260
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * rules/expr.mjs —— 表达式的可静态判定缺陷(lint_card / preflight 单一来源)。
3
+ */
4
+ import { hit, walkNodes } from './walk.mjs'
5
+
6
+ /**
7
+ * R61/R12 的「疑似运行期数据」启发式形态表(导出供测试直测;lint 消费方为 r61 提醒级 WARN)。
8
+ * 命中 ≠ 违规(版本号 / 固定营销话术会误报),语义复核交 AI。日期为完整日期才命中
9
+ * ("2026.09" 这类年月段不匹配),点分日期(2025.10.10)为 §11.11 高发形态。
10
+ */
11
+ export const NUMERIC_HINT_PATTERNS = [
12
+ { re: /\d{1,3}(,\d{3})+(\.\d+)?/, label: '千分号数字' },
13
+ { re: /[¥¥]\s*\d+(\.\d+)?/, label: '货币符+数字' },
14
+ { re: /\d+(\.\d+)?\s*(元|笔|次|个|条)/, label: '数字+单位词' },
15
+ { re: /\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}/, label: '日期' },
16
+ ]
17
+
18
+ /** 文案/inputParams 默认值里的「疑似运行期数字」标签(r61 消费;无命中返回 null)。 */
19
+ export function numericHint(str) {
20
+ if (!str) return null
21
+ for (const { re, label } of NUMERIC_HINT_PATTERNS) if (re.test(str)) return label
22
+ return null
23
+ }
24
+
25
+ /**
26
+ * R23:百分比表达式(含 `*100`)必须配 `Math.round`,否则 JS 浮点精度会产出 33.333333%。
27
+ *
28
+ * 扫描范围:component properties + 全部 style 子项(两处都可能是表达式值)。
29
+ * @param {object} view schema.view
30
+ * @returns {Array<{code:string,severity:string,componentId:string,message:string,detail:string}>}
31
+ */
32
+ export function collectExprNoMathRound(view) {
33
+ const out = []
34
+ walkNodes(view, (n) => {
35
+ const props = [...(n.properties || [])]
36
+ for (const g of n.style?.properties || []) {
37
+ for (const c of g.itemChildren?.properties || []) props.push(c)
38
+ }
39
+ for (const p of props) {
40
+ if (String(p.itemValueFrom) !== '1' || typeof p.itemValue !== 'string') continue
41
+ if (!/\*\s*100/.test(p.itemValue)) continue
42
+ if (/Math\.round/.test(p.itemValue)) continue
43
+ out.push(hit(
44
+ 'r23-no-math-round',
45
+ 'error',
46
+ n.componentId || '',
47
+ `"${n.componentId || ''}" 表达式含 *100 但无 Math.round → JS 浮点精度溢出(R23): ${p.itemValue.slice(0, 50)}`,
48
+ ))
49
+ }
50
+ })
51
+ return out
52
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * rules/image.mjs —— Image 组件的「静默失效」类硬契约(lint_card / preflight 单一来源)。
3
+ *
4
+ * 为什么必须共享:这两条是模型最高频踩的坑(`itemValueFrom` 三态 + `rect.height`),
5
+ * 原先在 lint(迭代门禁)与 preflight(生成前检查)各写一遍,文案已经漂移
6
+ * ("渲染器不走图片逻辑,空白" vs "但值是路径 → 渲染器不走图片逻辑,空白";
7
+ * "压缩图片高度" vs "会压缩图片高度")—— 同一张卡两处报告措辞不同,模型会当成两件事。
8
+ *
9
+ * 文案以 lint 版为准(lint 是迭代门禁主力,被模型逐轮消费)。
10
+ */
11
+ import { hit, walkNodes, findProp, findStyleChild } from './walk.mjs'
12
+
13
+ /**
14
+ * R25:Image 的图片地址属性(键固定为 `value`)三态精确映射。
15
+ *
16
+ * from="0"(常量)—— 渲染器不走图片逻辑 ⇒ 静态路径必须用 "2"
17
+ * from="1"(变量)—— 值必须是 `$` 开头的表达式
18
+ * from="2"(图片值)—— 值必须是静态路径,写成 `$表达式` 会被**原样透传**进 url(...)
19
+ *
20
+ * @param {object} view schema.view
21
+ * @param {{reportMissingProp?: boolean}} [opts]
22
+ * reportMissingProp=true 时额外报「没有 itemKey="value" 属性」(preflight 侧独有:
23
+ * 键名写错如 `image` 会让下游检查全部短路,必须显式拦)。lint 侧由 R28b 契约覆盖,
24
+ * 默认 false 以保持 lint 输出不变。
25
+ * @returns {Array<{code:string,severity:string,componentId:string,message:string,detail:string}>}
26
+ */
27
+ export function collectImageValueFrom(view, { reportMissingProp = false } = {}) {
28
+ const out = []
29
+ walkNodes(view, (n) => {
30
+ if (n.className !== 'Image') return
31
+ const cid = n.componentId || ''
32
+ const p = reportMissingProp ? findProp(n, 'value') : null
33
+ if (reportMissingProp && !p) {
34
+ out.push(hit(
35
+ 'r25-image-no-value-prop',
36
+ 'error',
37
+ cid,
38
+ `Image "${cid}" 没有 itemKey="value" 的属性 → 运行时拿不到图片地址,渲染成破图(R25)。该组件元数据里图片地址的键固定为 value`,
39
+ ))
40
+ return
41
+ }
42
+ for (const prop of n.properties || []) {
43
+ if (prop.itemKey !== 'value') continue
44
+ const from = String(prop.itemValueFrom || '')
45
+ const val = String(prop.itemValue || '')
46
+ if (from === '0' && !val.startsWith('$')) {
47
+ out.push(hit(
48
+ 'r25-image-from-0',
49
+ 'error',
50
+ cid,
51
+ `Image "${cid}" itemValueFrom="0"(常量) → 渲染器不走图片逻辑,空白(R25)。静态路径用 "2"`,
52
+ ))
53
+ }
54
+ if (from === '1' && !val.startsWith('$')) {
55
+ out.push(hit(
56
+ 'r25-image-from-1',
57
+ 'error',
58
+ cid,
59
+ `Image "${cid}" itemValueFrom="1"(变量) 但值不以 $ 开头 → 表达式求值失败,空白(R25)`,
60
+ ))
61
+ }
62
+ if (from === '2' && val.startsWith('$')) {
63
+ out.push(hit(
64
+ 'r25-image-from-2',
65
+ 'error',
66
+ cid,
67
+ `Image "${cid}" itemValueFrom="2"(图片/文件值) 但值是表达式 "${val}" → ` +
68
+ `运行时对 "2" 是原样透传(property.js onPreview),该字符串直接进 background-image:url(${val}),` +
69
+ `画布必然空白(R25)。变量绑定必须改 "1"`,
70
+ ))
71
+ }
72
+ }
73
+ })
74
+ return out
75
+ }
76
+
77
+ /**
78
+ * R32:Image 必须有显式 `rect.height`(否则 CSS aspect-ratio:16/9 压缩图片高度)。
79
+ * @returns {Array} 同 collectImageValueFrom
80
+ */
81
+ export function collectImageNoHeight(view) {
82
+ const out = []
83
+ walkNodes(view, (n) => {
84
+ if (n.className !== 'Image') return
85
+ const h = findStyleChild(n, 'rect', 'height')
86
+ if (!h || !String(h.itemValue).trim()) {
87
+ out.push(hit(
88
+ 'r32-image-no-height',
89
+ 'error',
90
+ n.componentId || '',
91
+ `Image "${n.componentId || ''}" 缺 rect.height → CSS aspect-ratio:16/9 压缩图片高度(R32)。设显式高度`,
92
+ ))
93
+ }
94
+ })
95
+ return out
96
+ }