cardbuddy 0.14.3 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -0
- package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
- package/dist/preview-editor/assets/index-Bro07DTa.js +313 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/standalone.js +3 -0
- package/dist/vendored/card-editor/runtime/adapter.js +2 -2
- package/dist/vendored/mcp/client.mjs +9 -2
- package/dist/vendored/server/core/ai-chat.js +1702 -134
- package/dist/vendored/server/core/browser.js +12 -3
- package/dist/vendored/server/core/cardFiles.js +3 -0
- package/dist/vendored/server/core/cardHistory.js +1 -0
- package/dist/vendored/server/core/cardImportMeta.js +7 -6
- package/dist/vendored/server/core/cardOpenFolder.js +1 -0
- package/dist/vendored/server/core/cardSave.js +1 -0
- package/dist/vendored/server/core/cardSync.js +3 -0
- package/dist/vendored/server/core/cardValidate.js +16 -5
- package/dist/vendored/server/core/codeBridge.js +5 -1
- package/dist/vendored/server/core/logStream.js +1 -0
- package/dist/vendored/server/core/sliceStatic.js +24 -4
- package/dist/vendored/server/core/sliceUpload.js +1 -0
- package/dist/vendored/server/core/turnJournal.js +6 -0
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +257 -201
- package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
- package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
- package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
- package/dist/vendored/skills/dsl-card-ai/SKILL.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +208 -380
- package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
- package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +233 -61
- package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +202 -4
- package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +87 -0
- package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +42 -33
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +59 -11
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +44 -8
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +26 -6
- package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +54 -0
- package/dist/vendored/skills/dsl-card-generator/references/semantic_adjudication.md +23 -0
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
- package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +33 -4
- package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +13 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
- package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +133 -2
- package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +226 -19
- package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +6 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
- package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +92 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +277 -27
- package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +147 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +12 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/ir_merge.mjs +288 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/ir_restore.mjs +175 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +1015 -54
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/geometry_audit.mjs +131 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout_rules.mjs +160 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +17 -5
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +102 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +5727 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +4214 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +1110 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +322 -71
- package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +87 -43
- package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +91 -43
- package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +7 -11
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +272 -46
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +43 -10499
- package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +188 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
- package/dist/vendored/skills/shared/exit-codes.md +79 -0
- package/dist/vendored/skills/shared/spec-contract.md +118 -0
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +43 -20
- package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +4 -4
- package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +47 -9
- package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
- package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +546 -91
- package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +51 -5
- package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +217 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +64 -34
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-Cm0USikc.js +0 -307
- package/dist/preview-editor/assets/index-cbTCHp8J.css +0 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
- package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* component_contract.mjs — 组件契约(className 白名单 + 属性键白名单 + 样式组/子键白名单)单一来源。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它(2026-09-24 gen-1 实测事故):
|
|
5
|
+
* 模型给 Image 节点写了 `itemKey: "image"`,而 Image 组件元数据里**只有一个属性键 `value`**
|
|
6
|
+
* (itemName「图片地址」,itemTypeRange "image,var")。运行时按 itemKey 绑定组件 props,
|
|
7
|
+
* 不存在的键**被整条静默丢弃** ⇒ value 为空 ⇒ 图片走 DEFAULT_IMAGE_URL ⇒ 渲染成破图。
|
|
8
|
+
* 而当时的四道闸**全部放行**:
|
|
9
|
+
* - validate_schema.py:只查结构,属性键不是它的管辖范围
|
|
10
|
+
* - lint_card.mjs:没有属性键规则
|
|
11
|
+
* - preflight.mjs:Check 3 / Check 5 都用 `getProp(n,'value')`,取不到就直接 `continue`
|
|
12
|
+
* —— 键名写错反而把整条检查链**短路**了
|
|
13
|
+
* - iterate 的 elements.missing:只比对文本,不比对图片
|
|
14
|
+
* 全仓 82 处 Image 里 81 处写 `value`,唯一写 `image` 的就是这一处 ⇒ 这是模型偶发编造键名,
|
|
15
|
+
* 必须有机械兜底,不能指望它每次都对。
|
|
16
|
+
*
|
|
17
|
+
* 口径同源:preflight / lint_card / readiness / gate 一律 import 本模块,
|
|
18
|
+
* **禁止各写一份白名单**(否则出现「文档教 A、门禁判 A 违规」的死结)。
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 渲染器内建、但不在 component-meta.json 里的类。
|
|
25
|
+
*
|
|
26
|
+
* `Root` = 卡片根容器。card-editor/runtime/node-model.js 明写:
|
|
27
|
+
* 「根节点 className "Root" 不在组件 meta 里,这里合成一份:无属性、无插槽、样式全开」。
|
|
28
|
+
* 所以:className 合法、样式组照常校验、**属性键不校验**(ROOT_META 里根本没有 properties 概念,
|
|
29
|
+
* 也就不存在「非法属性键」)。全仓 75 张卡的根节点实测 0 张带 properties。
|
|
30
|
+
*/
|
|
31
|
+
const BUILTIN_CLASSES = new Set(['Root'])
|
|
32
|
+
|
|
33
|
+
/** 不做属性键校验的类(见 BUILTIN_CLASSES 说明)。 */
|
|
34
|
+
const NO_PROP_CHECK = new Set(['Root'])
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 复现器(repeater)判定:**由元数据推导,不写类名**。
|
|
38
|
+
*
|
|
39
|
+
* 为什么需要:`slots[].slotKey` 实测**恒为 `default`**(全仓 1010 个 slot 无一例外,
|
|
40
|
+
* Row 597 / Column 328 / ListLayout 54 / AbsoluteLayout 20 / GridLayout 11),
|
|
41
|
+
* 所以「子节点存在 slot.components 里」**不等于**「处在列表项作用域里」——
|
|
42
|
+
* 按 slotKey 或按「有没有 slot」判断会把整棵树的引用都当成列表项字段(实测 gen-1 21 条引用
|
|
43
|
+
* 被全部误判进槽位,变量绑定门禁因此整条失效)。
|
|
44
|
+
*
|
|
45
|
+
* 判据 = 同时满足两条:① 元数据声明了 `value` 属性(数组数据源);② 有 `default` 插槽。
|
|
46
|
+
* 命中者即「按数据源循环渲染子节点」的容器,其插槽内的 `$field` 才是 item 字段。
|
|
47
|
+
*/
|
|
48
|
+
function deriveRepeaterClasses(cm) {
|
|
49
|
+
const out = new Set()
|
|
50
|
+
for (const [cls, def] of Object.entries(cm?.components || {})) {
|
|
51
|
+
const hasValueProp = (def?.properties || []).some((p) => p && p.itemKey === 'value')
|
|
52
|
+
const slots = def?.slots || def?.slotList || []
|
|
53
|
+
const hasDefaultSlot = slots.some((s) => String(s?.slotKey || s?.key || s) === 'default')
|
|
54
|
+
if (hasValueProp && hasDefaultSlot) out.add(cls)
|
|
55
|
+
}
|
|
56
|
+
return out
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 读取并编译契约。
|
|
61
|
+
*
|
|
62
|
+
* @param {object} opts
|
|
63
|
+
* @param {string} opts.metaDir 元数据目录(含 component-meta.json / style-meta.json)
|
|
64
|
+
* @returns {{
|
|
65
|
+
* classes: Set<string>,
|
|
66
|
+
* propKeys: Map<string, Set<string>>,
|
|
67
|
+
* propMeta: Map<string, Map<string, object>>,
|
|
68
|
+
* styleGroups: Map<string, Set<string>>,
|
|
69
|
+
* repeaterClasses: Set<string>,
|
|
70
|
+
* loaded: { components: boolean, styles: boolean },
|
|
71
|
+
* }}
|
|
72
|
+
*/
|
|
73
|
+
export function loadContract({ metaDir }) {
|
|
74
|
+
const out = {
|
|
75
|
+
classes: new Set(BUILTIN_CLASSES),
|
|
76
|
+
propKeys: new Map(),
|
|
77
|
+
propMeta: new Map(),
|
|
78
|
+
styleGroups: new Map(),
|
|
79
|
+
repeaterClasses: new Set(),
|
|
80
|
+
loaded: { components: false, styles: false },
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 组件契约 ──
|
|
84
|
+
const cmPath = path.join(metaDir, 'component-meta.json')
|
|
85
|
+
try {
|
|
86
|
+
const cm = JSON.parse(fs.readFileSync(cmPath, 'utf8'))
|
|
87
|
+
for (const [cls, def] of Object.entries(cm?.components || {})) {
|
|
88
|
+
out.classes.add(cls)
|
|
89
|
+
const keys = new Set()
|
|
90
|
+
const metaByKey = new Map()
|
|
91
|
+
for (const p of def?.properties || []) {
|
|
92
|
+
const k = p?.itemKey
|
|
93
|
+
// 关键:**跳过没有 itemKey 的属性项**。
|
|
94
|
+
// Text / Button 的元数据里有「常用样式」这类 UI 预置项,只有 itemId + itemType,没有 itemKey
|
|
95
|
+
// —— 它们不参与运行时绑定。全仓实测 443(Text) + 21(Button) 条这种键,若不跳过会造成大面积误报。
|
|
96
|
+
if (typeof k !== 'string' || !k) continue
|
|
97
|
+
keys.add(k)
|
|
98
|
+
metaByKey.set(k, p)
|
|
99
|
+
}
|
|
100
|
+
out.propKeys.set(cls, keys)
|
|
101
|
+
out.propMeta.set(cls, metaByKey)
|
|
102
|
+
}
|
|
103
|
+
out.repeaterClasses = deriveRepeaterClasses(cm)
|
|
104
|
+
out.loaded.components = out.classes.size > 0
|
|
105
|
+
} catch {
|
|
106
|
+
/* 拿不到组件元数据:契约退化为「空」,调用方据此跳过检查而不是误报 */
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 样式契约(13 组 + 每组合法子键)──
|
|
110
|
+
const smPath = path.join(metaDir, 'style-meta.json')
|
|
111
|
+
try {
|
|
112
|
+
const sm = JSON.parse(fs.readFileSync(smPath, 'utf8'))
|
|
113
|
+
for (const g of sm?.properties || []) {
|
|
114
|
+
const gk = g?.itemKey
|
|
115
|
+
if (typeof gk !== 'string' || !gk) continue
|
|
116
|
+
const childKeys = new Set()
|
|
117
|
+
for (const c of g?.itemChildren || []) {
|
|
118
|
+
const ck = c?.itemKey
|
|
119
|
+
if (typeof ck === 'string' && ck) childKeys.add(ck)
|
|
120
|
+
}
|
|
121
|
+
out.styleGroups.set(gk, childKeys)
|
|
122
|
+
}
|
|
123
|
+
out.loaded.styles = out.styleGroups.size > 0
|
|
124
|
+
} catch {
|
|
125
|
+
/* 同上 */
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 该组件的合法属性键集合(未知组件返回 null,表示「无从判断」而非「全部非法」)。 */
|
|
132
|
+
export function legalPropKeys(contract, className) {
|
|
133
|
+
if (NO_PROP_CHECK.has(className)) return null
|
|
134
|
+
return contract.propKeys.has(className) ? contract.propKeys.get(className) : null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 编辑距离(只用于给「你是不是想写 X」的建议,不是判定依据)。
|
|
139
|
+
* 上限 2:键名普遍很短,距离 >2 的建议没有参考价值。
|
|
140
|
+
*/
|
|
141
|
+
function nearestKey(target, candidates) {
|
|
142
|
+
const t = String(target || '')
|
|
143
|
+
let best = null
|
|
144
|
+
let bestD = Infinity
|
|
145
|
+
for (const c of candidates) {
|
|
146
|
+
const d = levenshtein(t, c)
|
|
147
|
+
if (d < bestD) {
|
|
148
|
+
bestD = d
|
|
149
|
+
best = c
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return bestD <= 2 ? best : null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function levenshtein(a, b) {
|
|
156
|
+
if (a === b) return 0
|
|
157
|
+
const m = a.length
|
|
158
|
+
const n = b.length
|
|
159
|
+
if (!m) return n
|
|
160
|
+
if (!n) return m
|
|
161
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j)
|
|
162
|
+
for (let i = 1; i <= m; i++) {
|
|
163
|
+
const cur = [i]
|
|
164
|
+
for (let j = 1; j <= n; j++) {
|
|
165
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1))
|
|
166
|
+
}
|
|
167
|
+
prev = cur
|
|
168
|
+
}
|
|
169
|
+
return prev[n]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 遍历 schema 里所有节点(含 slots 子树,与渲染器一致)。
|
|
174
|
+
*
|
|
175
|
+
* @param {object} schema 卡片 schema
|
|
176
|
+
* @param {(node: object, where: string, parent: object|null) => void} cb
|
|
177
|
+
*/
|
|
178
|
+
export function walkSchemaNodes(schema, cb) {
|
|
179
|
+
const visit = (node, where, parent) => {
|
|
180
|
+
if (!node || typeof node !== 'object') return
|
|
181
|
+
cb(node, where, parent)
|
|
182
|
+
const kids = node.components || []
|
|
183
|
+
kids.forEach((c, i) => visit(c, `${where}>c${i}`, node))
|
|
184
|
+
;(node.slots || []).forEach((s, si) => {
|
|
185
|
+
;(s?.components || []).forEach((c, ci) => visit(c, `${where}>slot:${s?.slotKey || si}[${ci}]`, node))
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
if (schema?.view) visit(schema.view, 'view', null)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 采集组件契约违规。
|
|
193
|
+
*
|
|
194
|
+
* 返回项字段:
|
|
195
|
+
* code 'unknown-class' | 'unknown-prop-key' | 'unknown-style-group' | 'unknown-style-key'
|
|
196
|
+
* severity 'error' | 'warn'
|
|
197
|
+
* componentId 节点 id(缺省 '(root)')
|
|
198
|
+
* className 节点类型
|
|
199
|
+
* where 节点在树中的路径(定位用)
|
|
200
|
+
* key 违规键名
|
|
201
|
+
* legal 该处合法键数组(供调用方直接展示)
|
|
202
|
+
* suggestion 近似合法键(编辑距离 ≤2 才有;用于「你是不是想写 value」)
|
|
203
|
+
*
|
|
204
|
+
* @param {object} schema
|
|
205
|
+
* @param {ReturnType<typeof loadContract>} contract
|
|
206
|
+
* @returns {Array<object>}
|
|
207
|
+
*/
|
|
208
|
+
export function collectContractViolations(schema, contract) {
|
|
209
|
+
const out = []
|
|
210
|
+
if (!contract || !contract.loaded.components) return out
|
|
211
|
+
|
|
212
|
+
walkSchemaNodes(schema, (node, where) => {
|
|
213
|
+
const cls = node.className
|
|
214
|
+
if (!cls) return
|
|
215
|
+
const cid = node.componentId || '(root)'
|
|
216
|
+
|
|
217
|
+
// 1) className 本身不在元数据里(渲染空白)——与 preflight Check 2 同源,这里也给一份,
|
|
218
|
+
// 让 readiness 能独立跑出完整结论。
|
|
219
|
+
if (!contract.classes.has(cls)) {
|
|
220
|
+
out.push({ code: 'unknown-class', severity: 'error', componentId: cid, className: cls, where, key: cls, legal: [...contract.classes].sort() })
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 2) 属性键白名单 —— 本次事故的正主
|
|
225
|
+
const legal = legalPropKeys(contract, cls)
|
|
226
|
+
if (legal) {
|
|
227
|
+
for (const p of node.properties || []) {
|
|
228
|
+
const k = p?.itemKey
|
|
229
|
+
// 无 itemKey 的属性项是「样式预置」类 UI 载体,不参与绑定,跳过(全仓实测 464 条)
|
|
230
|
+
if (typeof k !== 'string' || !k) continue
|
|
231
|
+
if (legal.has(k)) continue
|
|
232
|
+
out.push({
|
|
233
|
+
code: 'unknown-prop-key',
|
|
234
|
+
severity: 'error',
|
|
235
|
+
componentId: cid,
|
|
236
|
+
className: cls,
|
|
237
|
+
where,
|
|
238
|
+
key: k,
|
|
239
|
+
legal: [...legal].sort(),
|
|
240
|
+
suggestion: nearestKey(k, legal),
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 3) 样式组 / 组内子键白名单
|
|
246
|
+
if (contract.loaded.styles) {
|
|
247
|
+
for (const g of node.style?.properties || []) {
|
|
248
|
+
const gk = g?.itemKey
|
|
249
|
+
if (typeof gk !== 'string' || !gk) continue
|
|
250
|
+
if (!contract.styleGroups.has(gk)) {
|
|
251
|
+
// 空组(没有任何带值的子键)运行时本来就什么也不做 ⇒ 只是噪音,报 warn;
|
|
252
|
+
// 带值的未知组才是真丢样式,报 error。这条区分避免「模型随手写错一个空组」也把门禁打红。
|
|
253
|
+
const carrying = (g?.itemChildren?.properties || []).filter((c) => {
|
|
254
|
+
const v = c?.itemValue
|
|
255
|
+
if (v === undefined || v === null || v === '') return false
|
|
256
|
+
if (typeof v === 'object' && !Array.isArray(v)) return Object.keys(v).length > 0
|
|
257
|
+
return true
|
|
258
|
+
})
|
|
259
|
+
out.push({
|
|
260
|
+
code: 'unknown-style-group',
|
|
261
|
+
severity: carrying.length ? 'error' : 'warn',
|
|
262
|
+
componentId: cid,
|
|
263
|
+
className: cls,
|
|
264
|
+
where,
|
|
265
|
+
key: gk,
|
|
266
|
+
legal: [...contract.styleGroups.keys()].sort(),
|
|
267
|
+
suggestion: nearestKey(gk, [...contract.styleGroups.keys()]),
|
|
268
|
+
carrying: carrying.length,
|
|
269
|
+
})
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
const childLegal = contract.styleGroups.get(gk)
|
|
273
|
+
for (const c of g?.itemChildren?.properties || []) {
|
|
274
|
+
const ck = c?.itemKey
|
|
275
|
+
if (typeof ck !== 'string' || !ck) continue
|
|
276
|
+
if (childLegal.has(ck)) continue
|
|
277
|
+
out.push({
|
|
278
|
+
code: 'unknown-style-key',
|
|
279
|
+
severity: 'error',
|
|
280
|
+
componentId: cid,
|
|
281
|
+
className: cls,
|
|
282
|
+
where: `${where}>style.${gk}`,
|
|
283
|
+
key: ck,
|
|
284
|
+
legal: [...childLegal].sort(),
|
|
285
|
+
suggestion: nearestKey(ck, childLegal),
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
return out
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 把一条违规渲染成一行可执行的中文说明(脚本输出是程序文本,不放具体卡片/业务字段名)。 */
|
|
296
|
+
export function describeViolation(v) {
|
|
297
|
+
const at = v.where ? `${v.where} ` : ''
|
|
298
|
+
const suffix = v.suggestion ? `(是不是想写 "${v.suggestion}"?)` : ''
|
|
299
|
+
switch (v.code) {
|
|
300
|
+
case 'unknown-class':
|
|
301
|
+
return `${at}${v.className} "${v.componentId}" 不在组件元数据白名单里 → 渲染空白。合法类型:${v.legal.join(' / ')}`
|
|
302
|
+
case 'unknown-prop-key':
|
|
303
|
+
return (
|
|
304
|
+
`${at}${v.className} "${v.componentId}" 的属性键 "${v.key}" 不在该组件的合法键里 → ` +
|
|
305
|
+
`运行时按 itemKey 绑定,**不存在的键被整条静默丢弃**,组件拿到空值(如 Image 会渲染成破图)。` +
|
|
306
|
+
`该组件合法键:${v.legal.length ? v.legal.join(' / ') : '(无:该组件不接受任何属性)'}${suffix}`
|
|
307
|
+
)
|
|
308
|
+
case 'unknown-style-group':
|
|
309
|
+
return `${at}${v.className} "${v.componentId}" 的样式组 "${v.key}" 不是 13 个合法组名之一 → 整组被丢弃。合法组:${v.legal.join(' / ')}${suffix}`
|
|
310
|
+
case 'unknown-style-key':
|
|
311
|
+
return `${at}${v.className} "${v.componentId}" 在样式组 "${String(v.where).split('.').pop()}" 下写了非法子键 "${v.key}" → 该条属性被静默丢弃。该组合法子键:${v.legal.join(' / ')}${suffix}`
|
|
312
|
+
default:
|
|
313
|
+
return `${at}${v.className} "${v.componentId}" 契约违规:${v.key}`
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* geometry_audit.mjs —— 用**纯几何事实**判「节点是不是看不见 / 跑出画布 / 被折成一堆行」。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它(2026-09-21 复盘):上一轮建卡跑了 30 分钟不收敛,直接原因是模型**看不见渲染结果**
|
|
5
|
+
* (card_screenshot 只给人看、不进模型上下文),可观测输入只剩 match / heightDelta / bands 三个数字,
|
|
6
|
+
* 于是全程盲调参。而那两个致命缺陷(Row 内横向容器默认 100% 吃掉整行 → 文本被压成 0 宽、
|
|
7
|
+
* 兄弟被顶到 x=356 卡外)其实**不需要看图就能判定**:它们全是几何事实。
|
|
8
|
+
*
|
|
9
|
+
* 本模块只判可由 `getBoundingClientRect()` + computedStyle 直接证实的三类,不做任何猜测:
|
|
10
|
+
* 1. collapsed(**error**):在流里、有可见内容、却被压成零尺寸 → 用户看不见它,千真万确。
|
|
11
|
+
* 2. outOfCard(**error**):部分或全部落在卡片矩形之外 → 卡片根节 overflow:hidden,必然被裁。
|
|
12
|
+
* 3. overWrapped(**warn**):文本高度 > 2 行(h / lineHeight > 2)→ 多半是被挤窄后逐字换行,
|
|
13
|
+
* 也可能是设计稿本来就是多行文本,故只提示不拦门禁。
|
|
14
|
+
*
|
|
15
|
+
* 判定阈值都用「1x 设计像素」,与 iterate 的 match/heightDelta 同一坐标系。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 被视为「图形内容」的组件类:零尺寸时一定是缺陷(文本另有 text 判据)。 */
|
|
19
|
+
const GRAPHIC_CLASSES = new Set(['Image', 'Icon', 'Video', 'Map', 'Svgsymbols', 'SvgSymbols', 'MultiText'])
|
|
20
|
+
|
|
21
|
+
/** 文本节点折行阈值:h / lineHeight 超过这个倍数即提示(2 = 两行,>2 即三行以上)。 */
|
|
22
|
+
const OVERWRAP_RATIO = 2
|
|
23
|
+
|
|
24
|
+
const round1 = (n) => Math.round(Number(n) * 10) / 10
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} opts
|
|
28
|
+
* @param {Array<object>} opts.elements render_card 产出的 elementBoxes(正常尺寸节点)
|
|
29
|
+
* @param {Array<object>} [opts.degenerate] render_card 产出的 elementBoxesDegenerate(零尺寸节点)
|
|
30
|
+
* @param {number} opts.cardWidth 卡片逻辑宽(1x,= `.phone__screen` 实测宽)
|
|
31
|
+
* @param {number} [opts.tolerance] 可接受误差(px),默认 1
|
|
32
|
+
* @returns {{collapsed:Array,outOfCard:Array,overWrapped:Array,counts:object}}
|
|
33
|
+
*/
|
|
34
|
+
export function auditGeometry({ elements, degenerate, cardWidth, tolerance = 1 }) {
|
|
35
|
+
const collapsed = []
|
|
36
|
+
const outOfCard = []
|
|
37
|
+
const overWrapped = []
|
|
38
|
+
const width = Number(cardWidth) || 0
|
|
39
|
+
|
|
40
|
+
for (const el of degenerate || []) {
|
|
41
|
+
const hasContent = Boolean(String(el.text || '').trim()) || GRAPHIC_CLASSES.has(el.cls)
|
|
42
|
+
if (!hasContent) continue // 空容器被压扁不报警(它本来就不显示东西)
|
|
43
|
+
collapsed.push({
|
|
44
|
+
cid: el.cid,
|
|
45
|
+
cls: el.cls,
|
|
46
|
+
text: String(el.text || '').slice(0, 24),
|
|
47
|
+
x: round1(el.x),
|
|
48
|
+
y: round1(el.y),
|
|
49
|
+
w: round1(el.w),
|
|
50
|
+
h: round1(el.h),
|
|
51
|
+
reason: Number(el.w) < tolerance
|
|
52
|
+
? '宽被压成 0(同排兄弟把整行吃光了)→ 文本每字一行、实际不可读'
|
|
53
|
+
: '高被压成 0(父容器或兄弟把高度挤没了)',
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const el of elements || []) {
|
|
58
|
+
const w = Number(el.w) || 0
|
|
59
|
+
const h = Number(el.h) || 0
|
|
60
|
+
if (w < tolerance || h < tolerance) {
|
|
61
|
+
// 正常集合里出现零尺寸(例如被 1px 容差漏过的边界情况)→ 同样按压扁处理
|
|
62
|
+
const hasContent = Boolean(String(el.text || '').trim()) || GRAPHIC_CLASSES.has(el.cls)
|
|
63
|
+
if (hasContent) collapsed.push({ cid: el.cid, cls: el.cls, text: String(el.text || '').slice(0, 24), x: round1(el.x), y: round1(el.y), w: round1(w), h: round1(h), reason: '尺寸被压到 1px 以下' })
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
if (width > 0) {
|
|
67
|
+
const overRight = round1(el.x + w - width)
|
|
68
|
+
const overLeft = round1(-el.x)
|
|
69
|
+
if (overRight > tolerance || overLeft > tolerance) {
|
|
70
|
+
outOfCard.push({
|
|
71
|
+
cid: el.cid,
|
|
72
|
+
cls: el.cls,
|
|
73
|
+
text: String(el.text || '').slice(0, 24),
|
|
74
|
+
x: round1(el.x),
|
|
75
|
+
y: round1(el.y),
|
|
76
|
+
w: round1(w),
|
|
77
|
+
h: round1(h),
|
|
78
|
+
overflowRight: overRight > tolerance ? overRight : 0,
|
|
79
|
+
overflowLeft: overLeft > tolerance ? overLeft : 0,
|
|
80
|
+
reason: overRight > tolerance
|
|
81
|
+
? `右缘超出卡片 ${overRight}px(卡宽 ${width})→ 被根节点 overflow:hidden 裁掉`
|
|
82
|
+
: `左缘超出卡片 ${overLeft}px → 被裁掉`,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const lh = Number(el.lh) || 0
|
|
87
|
+
const text = String(el.text || '').trim()
|
|
88
|
+
if (text && lh > 0 && h / lh > OVERWRAP_RATIO) {
|
|
89
|
+
overWrapped.push({
|
|
90
|
+
cid: el.cid,
|
|
91
|
+
cls: el.cls,
|
|
92
|
+
text: text.slice(0, 24),
|
|
93
|
+
x: round1(el.x),
|
|
94
|
+
y: round1(el.y),
|
|
95
|
+
w: round1(w),
|
|
96
|
+
h: round1(h),
|
|
97
|
+
lines: Math.round(h / lh),
|
|
98
|
+
reason: `约占 ${Math.round(h / lh)} 行(h=${round1(h)} / 行高 ${round1(lh)})→ 检查是否被挤窄后逐字换行;设计稿本来就是多行文本可忽略`,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
collapsed,
|
|
105
|
+
outOfCard,
|
|
106
|
+
overWrapped,
|
|
107
|
+
counts: { collapsed: collapsed.length, outOfCard: outOfCard.length, overWrapped: overWrapped.length },
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 把审计结果压成一行给人/模型看的结论;无异常返回 null。 */
|
|
112
|
+
export function describeGeometry(audit) {
|
|
113
|
+
if (!audit) return null
|
|
114
|
+
const parts = []
|
|
115
|
+
if (audit.collapsed.length) {
|
|
116
|
+
parts.push(
|
|
117
|
+
`被压成零尺寸(用户完全看不见)${audit.collapsed.length} 个:` +
|
|
118
|
+
audit.collapsed.slice(0, 4).map((c) => `${c.cls} ${c.cid}${c.text ? ` "${c.text}"` : ''}`).join('、'),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
if (audit.outOfCard.length) {
|
|
122
|
+
parts.push(
|
|
123
|
+
`跑出卡片被裁掉 ${audit.outOfCard.length} 个:` +
|
|
124
|
+
audit.outOfCard.slice(0, 4).map((c) => `${c.cls} ${c.cid}${c.text ? ` "${c.text}"` : ''}(x=${c.x} w=${c.w})`).join('、'),
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
if (audit.overWrapped.length) {
|
|
128
|
+
parts.push(`疑似折行异常 ${audit.overWrapped.length} 个:` + audit.overWrapped.slice(0, 3).map((c) => `${c.cls} ${c.cid} ≈${c.lines} 行`).join('、'))
|
|
129
|
+
}
|
|
130
|
+
return parts.length ? parts.join(';') : null
|
|
131
|
+
}
|
|
@@ -54,7 +54,7 @@ export const TOKEN = {
|
|
|
54
54
|
COLOR: {
|
|
55
55
|
pageBg: '#F5F6F8', // 页面/画布背景
|
|
56
56
|
cardBg: '#FFFFFF', // 卡片背景
|
|
57
|
-
primary: '#00B578', // 品牌主色(绿,
|
|
57
|
+
primary: '#00B578', // 品牌主色(绿,v3)
|
|
58
58
|
primaryBlue: '#0079F2', // legacy 生成器历史主色(蓝)
|
|
59
59
|
fallbackBg: '#F5F5F5', // pickBg 兜底色
|
|
60
60
|
danger: '#F6434F', // 危险/未读红点
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* layout_rules.mjs —— 布局类硬契约的共享判定(lint_card.mjs / preflight.mjs 单一来源)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有这个模块:同一条布局规则在 lint(迭代门禁)与 preflight(生成前检查)里各写一遍,
|
|
5
|
+
* 迟早会打架——一处改了、另一处没改,模型就会看到「同一张卡两个门禁结论不同」。R25/R31/R32 已
|
|
6
|
+
* 经是双写的,这里不再复制第三份。
|
|
7
|
+
*
|
|
8
|
+
* 本模块只放**从渲染器契约推导出来的、可静态判定**的事实,不猜设计意图:
|
|
9
|
+
* ① 宽度基线:component-meta.json 里声明了 slots 的组件(=容器)默认 `rect.width:"100%"`,
|
|
10
|
+
* 且该默认**只在 rect 组整体缺失(或没有 width 子项)时生效**;写了 `width:""` 就是 auto;
|
|
11
|
+
* ② 主轴方向:CSS 强制 `.ce_root/.ce_column/.ce_singlecolumn` 为 column,其余默认 row。
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
|
|
16
|
+
import { metaDir } from './paths.mjs'
|
|
17
|
+
|
|
18
|
+
/** CSS 强制 flex-direction:column 的容器类(card-editor/styles/components.css)。 */
|
|
19
|
+
export const COLUMN_FLOW_CLASSES = new Set(['Root', 'Column', 'SingleColumn'])
|
|
20
|
+
|
|
21
|
+
let _metaCache = null
|
|
22
|
+
function componentMeta() {
|
|
23
|
+
if (_metaCache === null) {
|
|
24
|
+
try {
|
|
25
|
+
_metaCache = JSON.parse(fs.readFileSync(path.join(metaDir, 'component-meta.json'), 'utf8')).components || {}
|
|
26
|
+
} catch {
|
|
27
|
+
_metaCache = {}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return _metaCache
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 容器类 = component-meta 里声明了 slots(能装子节点)的组件。 */
|
|
34
|
+
export function containerClasses() {
|
|
35
|
+
const out = new Set()
|
|
36
|
+
for (const [cls, def] of Object.entries(componentMeta())) {
|
|
37
|
+
if ((def?.slots || []).length > 0) out.add(cls)
|
|
38
|
+
}
|
|
39
|
+
// meta 读不到时的兜底(保持规则可用,不至于静默失效)
|
|
40
|
+
if (!out.size) for (const c of ['Row', 'Column', 'MultiColumn', 'ListLayout', 'GridLayout', 'AbsoluteLayout', 'Canvas', 'ViewContainer']) out.add(c)
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 容器的宽度基线是不是「填满父容器」(渲染器默认 100%)。
|
|
46
|
+
* 未写 rect.width 的容器会拿到 component-meta 的默认值,实测渲染为 100%。
|
|
47
|
+
*/
|
|
48
|
+
export function defaultWidth100Classes() {
|
|
49
|
+
const out = new Set()
|
|
50
|
+
for (const cls of containerClasses()) {
|
|
51
|
+
const w = componentMeta()[cls]?.styleState?.rect?.width
|
|
52
|
+
if (w === '100%') out.add(cls)
|
|
53
|
+
}
|
|
54
|
+
if (!out.size) for (const c of ['Row', 'Column', 'MultiColumn', 'ListLayout', 'GridLayout', 'AbsoluteLayout', 'Canvas', 'ViewContainer']) out.add(c)
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 取 style.<group>.<key> 的字面值与来源开关(itemValueFrom)。 */
|
|
59
|
+
export function styleChild(node, group, key) {
|
|
60
|
+
for (const g of node?.style?.properties || []) {
|
|
61
|
+
if (g.itemKey !== group) continue
|
|
62
|
+
for (const c of g.itemChildren?.properties || []) {
|
|
63
|
+
if (c.itemKey === key) return { value: String(c.itemValue ?? '').trim(), from: String(c.itemValueFrom ?? '0') }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 父容器的主轴是横向(row)吗?CSS 默认按类定,schema 显式写 flexDirection 才覆盖。 */
|
|
70
|
+
export function isHorizontalFlow(parent) {
|
|
71
|
+
if (!parent) return false
|
|
72
|
+
const explicit = styleChild(parent, 'flex', 'flexDirection')?.value
|
|
73
|
+
if (COLUMN_FLOW_CLASSES.has(parent.className)) return explicit === 'row'
|
|
74
|
+
return (explicit || 'row') === 'row'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* R22b:横向流父容器里的**容器类子节点**,`rect.width` 缺失且 `flexShrink:0`。
|
|
79
|
+
*
|
|
80
|
+
* 渲染契约(2026-09-21 实测定标,见下):
|
|
81
|
+
* | rect 写法 | 容器实测渲染宽 | 同排兄弟 |
|
|
82
|
+
* |---------------------|------------------|-----------------------|
|
|
83
|
+
* | rect 组缺失 / 无 width 子项 | **100%(吃满整行)** | 长文本被压成 w=0 → 每字一行、不可见 ❌ |
|
|
84
|
+
* | `width: ""` | auto(自然宽) | 正常 ✓ |
|
|
85
|
+
* | `width: "100%"` | 100% | 被压成 0 ❌(R22 管这一半) |
|
|
86
|
+
* | `width: "60"` | 60px | 正常 ✓ |
|
|
87
|
+
* 叠加 `flexShrink:0` = 锁死不让位,于是「吃满整行」那个必然把兄弟挤没或顶出卡外。
|
|
88
|
+
*
|
|
89
|
+
* 与 R22 的关系:R22 拦的是 schema **显式写了** `width:"100%"` 那一半;本规则补的是
|
|
90
|
+
* **rect.width 整个缺失**(渲染等价于 100%)却完全没人管的另一半 ——
|
|
91
|
+
* 实测 demo/helloCard 的 6 个致命节点全落在这个盲区。
|
|
92
|
+
*
|
|
93
|
+
* 注意 `width: ""` **不算缺失**:它是这套 schema 的「auto」表示法,实测渲染为自然宽,安全。
|
|
94
|
+
*
|
|
95
|
+
* 仅在父容器有 ≥2 个子节点时成立(独子占满整行是合法用法)。
|
|
96
|
+
*
|
|
97
|
+
* @returns {{className:string,widthVal:null,shrinkVal:string}|null} 命中时返回证据
|
|
98
|
+
*/
|
|
99
|
+
export function rowChildContainerNoWidth({ node, parent, siblingCount }) {
|
|
100
|
+
if (!node || !parent) return null
|
|
101
|
+
if (siblingCount < 2) return null
|
|
102
|
+
if (!containerClasses().has(node.className)) return null
|
|
103
|
+
if (!defaultWidth100Classes().has(node.className)) return null
|
|
104
|
+
if (!isHorizontalFlow(parent)) return null
|
|
105
|
+
// width 子项「存在」就算已显式指定(含空串 "":auto 表示法,实测安全);
|
|
106
|
+
// 只有整组缺失 / 没有 width 子项才会落到 component-meta 默认 "100%"。
|
|
107
|
+
if (styleChild(node, 'rect', 'width')) return null
|
|
108
|
+
const shrink = styleChild(node, 'flex', 'flexShrink')
|
|
109
|
+
if (shrink?.value !== '0') return null
|
|
110
|
+
return { className: node.className, widthVal: null, shrinkVal: shrink.value }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 默认插槽直接子节点(兼容 Root.components 与普通节点 slots[0].components)。 */
|
|
114
|
+
export function directChildren(node) {
|
|
115
|
+
if (!node) return []
|
|
116
|
+
const slot = (node.slots || [])[0]
|
|
117
|
+
if (slot && Array.isArray(slot.components)) return slot.components
|
|
118
|
+
return Array.isArray(node.components) ? node.components : []
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 在 schema 树里收集全部 R22b 命中(供 lint / preflight 共用)。
|
|
123
|
+
* @param {object} view schema.view
|
|
124
|
+
* @returns {Array<{componentId:string,className:string,parentId:string,parentClass:string,siblingCount:number,message:string}>}
|
|
125
|
+
*/
|
|
126
|
+
export function collectRowChildNoWidth(view) {
|
|
127
|
+
const hits = []
|
|
128
|
+
// Root(view)自身是 column 流,其直接子节点不参与本规则 ⇒ 传 null 当作「非横向父容器」。
|
|
129
|
+
const visit = (node, isRoot) => {
|
|
130
|
+
const siblings = directChildren(node)
|
|
131
|
+
for (const child of siblings) {
|
|
132
|
+
const result = rowChildContainerNoWidth({
|
|
133
|
+
node: child,
|
|
134
|
+
parent: isRoot ? null : node,
|
|
135
|
+
siblingCount: siblings.length,
|
|
136
|
+
})
|
|
137
|
+
if (result) {
|
|
138
|
+
const parentLabel = `${node.className || 'Root'} "${node.componentId || '(root)'}"`
|
|
139
|
+
hits.push({
|
|
140
|
+
componentId: child.componentId || '',
|
|
141
|
+
className: result.className,
|
|
142
|
+
parentId: node.componentId || '(root)',
|
|
143
|
+
parentClass: node.className || 'Root',
|
|
144
|
+
siblingCount: siblings.length,
|
|
145
|
+
message:
|
|
146
|
+
`${result.className} "${child.componentId || '?'}" 是横向流 ${parentLabel} 的并列子节点(共 ${siblings.length} 个),` +
|
|
147
|
+
'它的 rect.width 整个缺失 —— 渲染器对「没有 width 子项」会回落到 component-meta 默认「100%」(吃满整行),' +
|
|
148
|
+
'又叠了 flexShrink:0 锁死不让位:同排兄弟会被压成 0 宽(文本每字一行、高度爆炸)或被顶出卡片右缘裁掉(R22b)。' +
|
|
149
|
+
'按设计稿给这个容器补一个明确宽度:实测 px(如 rect.width:"146")、或 `rect.width:"60%"`;' +
|
|
150
|
+
'要「按内容自适应」就写 rect.width:""(空串 = auto,实测渲染为自然宽);' +
|
|
151
|
+
'要「占满剩余空间」用 flexGrow:1 并把 flexShrink 改 1,不要靠缺失 + 锁死。',
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
visit(child, false)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
visit(view, true)
|
|
158
|
+
return hits
|
|
159
|
+
}
|
|
160
|
+
|
|
@@ -49,6 +49,29 @@ export const slicesDir =
|
|
|
49
49
|
export const baselinesDir =
|
|
50
50
|
process.env.DSL_CARD_BASELINES_DIR || path.join(projectRoot, '.card-baselines')
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 机器产物目录 —— **门禁只认这里的落盘产物,不认模型自述**。
|
|
54
|
+
*
|
|
55
|
+
* 三类文件同住于此,路径必须单源(历史事故:`check_fidelity` 自己拼了
|
|
56
|
+
* `<cardDir>/.audit.json`,而真实位置是 `.cache/ai-scratch/<id>.audit.json` ⇒
|
|
57
|
+
* 切图覆盖数恒为 null,`.audit.json` 从未存在过却没人发现):
|
|
58
|
+
* - `.cache/ai-scratch/<id>.audit.json` S2 审计全文(crop_slices --from-audit 与 readiness 都吃它)
|
|
59
|
+
* - `.cache/ai-scratch/<id>.iterate.json` 最近一轮迭代报告(--gate 的唯一依据)
|
|
60
|
+
* - `.cache/ai-scratch/<id>.ir.rNN.json` 每轮 IR 快照(回退用)
|
|
61
|
+
*/
|
|
62
|
+
export const scratchDir =
|
|
63
|
+
process.env.DSL_CARD_SCRATCH_DIR || path.join(projectRoot, '.cache', 'ai-scratch')
|
|
64
|
+
|
|
65
|
+
/** S2 审计落盘路径(spec_audit.mjs --save 的默认目标)。 */
|
|
66
|
+
export function cardAuditPath(id) {
|
|
67
|
+
return path.join(scratchDir, `${id}.audit.json`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 最近一轮迭代报告路径。 */
|
|
71
|
+
export function cardIteratePath(id) {
|
|
72
|
+
return path.join(scratchDir, `${id}.iterate.json`)
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
/** 兼容旧脚本的别名:宿主项目根目录。 */
|
|
53
76
|
export const repoRoot = projectRoot
|
|
54
77
|
|