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
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
* 12. 切图命名语义化(r59):引用名沿用设计稿图层名/导出目录前缀/中文名 → 看不出用途,必须按功能重命名。
|
|
23
23
|
* 13. 切图同内容重复(r60):src/slices 里存在 sha256 完全相同的副本 → 应复用同一份,不重复导入/上传。
|
|
24
24
|
* 14. 静态文案 / 字典默认值含疑似运行期数字(r61,提醒级):千分号数字 / ¥+数字 / 数字+单位词写死在静态 Text 或 defaultValue 里 → 提示按 rules.md §十四 做 AI 语义复核(运行期数字应抽变量 + 表达式拼接);固定话术误报可忽略。
|
|
25
|
+
* 15. 切图引用含 URL 保留字符(r31b):`# ? % & +` 会被浏览器当 fragment/query 截掉 → 图片静默不显示(R31 的补充,R31 只拦空格)。
|
|
26
|
+
* 16. 横向流父容器里的容器子节点未写 rect.width + flexShrink:0(r22b):渲染器按 component-meta 默认「100%」出 → 吃掉整行,同排兄弟被压成 0 宽或被顶出卡外。
|
|
27
|
+
* 17. 切图引用在 src/slices 里没有对应文件(r31c):渲染时 404,图片位置空白且控制台零报错 —— R31(空格)/R31b(保留字符)之外的第三种「引用与磁盘名不同源」。
|
|
28
|
+
* 18. 组件契约(r28 / r28b-*):属性键 / 样式组 / 样式子键必须在 component-meta.json + style-meta.json 白名单内 —— 不存在的键被运行时**整条静默丢弃**(Image 的键写成 image 而非 value ⇒ 图片变破图,而四道闸全绿)。
|
|
25
29
|
*
|
|
26
30
|
* 用法:
|
|
27
31
|
* node lint_card.mjs --id <cardId> # 体检单张卡
|
|
@@ -39,7 +43,14 @@ import path from 'node:path'
|
|
|
39
43
|
|
|
40
44
|
import { fileURLToPath } from 'node:url'
|
|
41
45
|
import { cardsDir, cardSchemaPath, cardDataPath, readJson, metaDir, slicesDir } from './lib/paths.mjs'
|
|
42
|
-
import { buildContentIndex, checkSliceName, collectSliceRefStrings, duplicatePeers, sliceNameFromRef } from './lib/slice_dedupe.mjs'
|
|
46
|
+
import { buildContentIndex, buildSliceFileIndex, checkSliceName, collectSliceRefStrings, duplicatePeers, findUrlReservedChars, isRemoteSliceRef, resolveSliceRefOnDisk, sliceNameFromRef } from './lib/slice_dedupe.mjs'
|
|
47
|
+
import { collectRowChildNoWidth } from './lib/layout_rules.mjs'
|
|
48
|
+
import { collectFullwidthFixedPx } from './lib/rules/adaptive-width.mjs'
|
|
49
|
+
import { loadContract, collectContractViolations, describeViolation } from './lib/component_contract.mjs'
|
|
50
|
+
import { collectImageValueFrom, collectImageNoHeight } from './lib/rules/image.mjs'
|
|
51
|
+
import { collectExprNoMathRound, numericHint } from './lib/rules/expr.mjs'
|
|
52
|
+
import { collectRepeaterDefects, collectStaticTileExpansion, collectArrayConsumption } from './lib/rules/bindings.mjs'
|
|
53
|
+
import { collectSlotMultiChildren, collectNullChildren, collectDuplicateStyleChildren, collectAlertDataWholeState, collectIconNoRect, collectRootFixedHeight } from './lib/rules/structure.mjs'
|
|
43
54
|
|
|
44
55
|
// src/slices 内容索引(sha256 → 文件名)进程内只建一次,--all 体检全量卡时复用
|
|
45
56
|
let SLICE_CONTENT_INDEX = null
|
|
@@ -248,6 +259,25 @@ export function lint(schema, data) {
|
|
|
248
259
|
const view = schema.view
|
|
249
260
|
if (!view) return [{ severity: 'error', code: 'no-view', message: 'schema 没有 view 字段' }]
|
|
250
261
|
|
|
262
|
+
// 0. 组件契约:属性键 / 样式组 / 样式子键必须落在白名单内(唯一来源 component-meta.json + style-meta.json)。
|
|
263
|
+
//
|
|
264
|
+
// 2026-09-24 事故:模型给 Image 写 itemKey:"image",而该组件元数据里图片地址的键**只有 value**。
|
|
265
|
+
// 运行时按 itemKey 绑定组件 props,不存在的键被**整条静默丢弃** ⇒ value 为空 ⇒ 图片渲染成破图。
|
|
266
|
+
// 而 validate / lint / preflight / elements.missing 当时**全部放行**(preflight 的 Check 3/5 都用
|
|
267
|
+
// getProp(n,'value'),取不到就 continue —— 键写错反而把检查链短路了)。
|
|
268
|
+
// 全仓 82 处 Image 里 81 处写 value,唯一写 image 的就是这张卡 ⇒ 模型会偶发编造键名,必须机械兜底。
|
|
269
|
+
{
|
|
270
|
+
const contract = loadContract({ metaDir })
|
|
271
|
+
for (const v of collectContractViolations(schema, contract)) {
|
|
272
|
+
out.push({
|
|
273
|
+
severity: v.severity,
|
|
274
|
+
code: v.code === 'unknown-class' ? 'r28-unknown-class' : `r28b-${v.code}`,
|
|
275
|
+
componentId: v.componentId,
|
|
276
|
+
message: describeViolation(v),
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
251
281
|
// 1. 脚手架前缀残留("示例"是 scaffold_card.mjs 默认占位文本)
|
|
252
282
|
if (data && typeof data === 'object') {
|
|
253
283
|
for (const [k, v] of Object.entries(data)) {
|
|
@@ -308,7 +338,7 @@ export function lint(schema, data) {
|
|
|
308
338
|
out.push({
|
|
309
339
|
severity: 'error',
|
|
310
340
|
code: 'r43-card-transparent-background',
|
|
311
|
-
message:
|
|
341
|
+
message: `卡片背景完全透明 —— 卡片背景不允许无背景/全透明(R43;半透明底色合法)。须从设计稿标注 cardFill 取背景色(可含 alpha)写入 Root/首层容器的 background.backgroundColor + backgroundType:"01";标注缺失时从聚焦截图取色兜底。`,
|
|
312
342
|
})
|
|
313
343
|
}
|
|
314
344
|
|
|
@@ -450,6 +480,12 @@ export function lint(schema, data) {
|
|
|
450
480
|
return sc ? String(sc.itemValue) : null
|
|
451
481
|
}
|
|
452
482
|
|
|
483
|
+
/** '311' / '311px' → 311;'100%' / 'auto' / 空 → null(比例宽无法静态判定溢出) */
|
|
484
|
+
function toPx(v) {
|
|
485
|
+
const m = /^(-?\d+(?:\.\d+)?)(?:px)?$/.exec(String(v ?? '').trim())
|
|
486
|
+
return m ? Number(m[1]) : null
|
|
487
|
+
}
|
|
488
|
+
|
|
453
489
|
function getWidthVal(n) {
|
|
454
490
|
const sp = (n.style && n.style.properties) || []
|
|
455
491
|
const rg = sp.find(p => p.itemKey === 'rect')
|
|
@@ -472,11 +508,21 @@ export function lint(schema, data) {
|
|
|
472
508
|
const hasVShow = (child) => ((child.directives) || []).some((d) => d && d.itemKey === 'v-show')
|
|
473
509
|
const isExclusiveVariantGroup = children.length >= 2 && children.every(hasVShow)
|
|
474
510
|
if (!isContentSize && allShrinkZero && !isExclusiveVariantGroup) {
|
|
511
|
+
// 真实溢出 vs 稳健性提示:只有「子节点显式定宽之和 > 父容器定宽」才真的会溢出。
|
|
512
|
+
// 其余情形(父容器按比例定宽 + 子节点内容自适应 / 百分比配比)当前渲染不会溢出,
|
|
513
|
+
// 报 error 会把模型逼去改结构换宽度 —— 实测(2026-09-21 demo 建卡回合)模型为消除这条
|
|
514
|
+
// error 把 Row 改成 width:auto,match 从 87.5% 掉到 62%,再改回来,来回震荡 40+ 步。
|
|
515
|
+
// 规则的本意是「防溢出」,不是「禁止定宽子节点」:降为 warning,信号不丢、门禁不再被它卡死。
|
|
516
|
+
const parentPx = toPx(parentWidth)
|
|
517
|
+
const fixedSum = children.reduce((a, c) => a + (toPx(getWidthVal(c)) || 0), 0)
|
|
518
|
+
const realOverflow = parentPx != null && fixedSum > parentPx
|
|
475
519
|
out.push({
|
|
476
|
-
severity: 'error',
|
|
520
|
+
severity: realOverflow ? 'error' : 'warn',
|
|
477
521
|
code: 'r35-multi-child-all-shrink0',
|
|
478
522
|
componentId: pid,
|
|
479
|
-
message:
|
|
523
|
+
message: realOverflow
|
|
524
|
+
? `${pcls} "${pid}" 定宽 ${parentPx}px 装不下 ${children.length} 个子节点的定宽之和 ${fixedSum}px(全部 flexShrink:0,压不动)→ 真溢出(R35)。把最宽的那个子节点改成 flexShrink:1,或按设计稿把父容器改宽/改 auto`
|
|
525
|
+
: `${pcls} "${pid}" 宽度非 auto 且 ${children.length} 个并列子节点全部 flexShrink:0 —— 当前宽度下不会溢出(子节点定宽之和 ${fixedSum}px${parentPx != null ? ` ≤ 父 ${parentPx}px` : ',父容器按比例定宽'}),仅稳健性提示:想消除就给最宽的子节点 flexShrink:1(不溢出时视觉零影响),**不要为此改 width:auto 或删节点**`,
|
|
480
526
|
})
|
|
481
527
|
}
|
|
482
528
|
}
|
|
@@ -500,6 +546,76 @@ export function lint(schema, data) {
|
|
|
500
546
|
}
|
|
501
547
|
})
|
|
502
548
|
|
|
549
|
+
// ── R31b: 切图引用含 URL 保留字符(# ? % & +)→ 浏览器截断请求 → 图片静默不显示 ──
|
|
550
|
+
// 事故(2026-09-21 demo/helloCard):引用 `/slices/警示-#999.png`,`#` 之后被当 fragment,
|
|
551
|
+
// 实际只请求到 `/slices/警示-`;又因切图未命中落到 SPA 兜底返回 200+HTML ⇒ 图空白、网络面板 200、
|
|
552
|
+
// 零报错、门禁全绿。R31 只拦空格,这里补上真正的元凶(保留字符)。
|
|
553
|
+
walk(view, (n) => {
|
|
554
|
+
if (n.className !== 'Image') return
|
|
555
|
+
for (const p of n.properties || []) {
|
|
556
|
+
if (p.itemKey !== 'value' || typeof p.itemValue !== 'string') continue
|
|
557
|
+
if (!p.itemValue.includes('/slices/')) continue
|
|
558
|
+
const hit = findUrlReservedChars(sliceNameFromRef(p.itemValue))
|
|
559
|
+
if (hit) {
|
|
560
|
+
out.push({
|
|
561
|
+
severity: 'error',
|
|
562
|
+
code: 'r31b-slice-url-reserved-char',
|
|
563
|
+
componentId: n.componentId || '',
|
|
564
|
+
message: `Image "${n.componentId}" 引用路径含 URL 保留字符 ${hit.map((c) => `"${c}"`).join(' ')}: "${p.itemValue}" → 浏览器会把保留字符之后当 fragment/query 截掉,请求的不是这个文件 → background-image 加载失败,且**不报错**(R31b)。把切图重命名为纯 ASCII 无语义字符名(如 iconWarning@2x.png)并同步改写引用`,
|
|
565
|
+
})
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
// ── R31b(兜底):style 背景图 / 表达式里内嵌的 /slices/ 引用同样要查(R31 只扫 Image.value)──
|
|
571
|
+
for (const ref of collectSliceRefStrings(schema)) {
|
|
572
|
+
const name = sliceNameFromRef(ref)
|
|
573
|
+
const hit = findUrlReservedChars(name)
|
|
574
|
+
if (!hit) continue
|
|
575
|
+
// Image.value 已被上面那条覆盖,避免同一处报两条
|
|
576
|
+
const already = out.some((f) => f.code === 'r31b-slice-url-reserved-char' && f.message.includes(`"${ref}"`))
|
|
577
|
+
if (already) continue
|
|
578
|
+
out.push({
|
|
579
|
+
severity: 'error',
|
|
580
|
+
code: 'r31b-slice-url-reserved-char',
|
|
581
|
+
componentId: '',
|
|
582
|
+
message: `切图引用含 URL 保留字符 ${hit.map((c) => `"${c}"`).join(' ')}: "${ref}" → 浏览器截断请求,图片会静默不显示(R31b)。重命名切图为纯 ASCII 名并同步改写引用`,
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ── R31c: 引用的切图在工程 src/slices 里根本不存在 → 渲染 404 → 图片空白(R31 家族的第三种)──
|
|
587
|
+
// R31 拦「名字带空格」、R31b 拦「名字带 URL 保留字符」,都是「名字写了但用不了」;
|
|
588
|
+
// R31c 补「这个名字在磁盘上就没有」。2026-09-21 helloCard 同源事故:
|
|
589
|
+
// 审计报 `警示-#999.png`、落盘被消毒成 `警示-999.png`,引用与磁盘名不同源 ⇒ 请求 404,
|
|
590
|
+
// 而切片服务先前会落 SPA 兜底返回 200+HTML ⇒ 图空白、网络面板 200、零报错、门禁全绿。
|
|
591
|
+
// 远端引用(@filestore / http / data:)天然无本地文件,不参与判定。
|
|
592
|
+
if (fs.existsSync(slicesDir)) {
|
|
593
|
+
const sliceIndex = buildSliceFileIndex(slicesDir)
|
|
594
|
+
for (const ref of collectSliceRefStrings(schema)) {
|
|
595
|
+
if (isRemoteSliceRef(ref)) continue
|
|
596
|
+
const { resolved, name } = resolveSliceRefOnDisk(sliceIndex, ref)
|
|
597
|
+
if (resolved) continue
|
|
598
|
+
out.push({
|
|
599
|
+
severity: 'error',
|
|
600
|
+
code: 'r31c-slice-ref-missing-file',
|
|
601
|
+
componentId: '',
|
|
602
|
+
message: `切图引用 "${ref}" 在 src/slices 里不存在(期望文件名 ${name})→ 渲染时 404,图片位置一片空白且控制台不报错(R31c)。两条修法:① 把切图按引用名落盘(\`crop_slices.mjs --from-audit\` 会自动收养审计判定存在的资产并按消毒名落盘);② 把引用改成磁盘上真实存在的文件名 —— 两者必须同源,别只改一边`,
|
|
603
|
+
})
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ── R22b: 横向流父容器里的容器子节点「未写 rect.width + flexShrink:0」→ 吃掉整行 ──
|
|
608
|
+
// 渲染契约:容器不写 rect.width ⇒ 落到 component-meta 默认 "100%" ⇒ 独占整行;再叠 flexShrink:0
|
|
609
|
+
// 就是锁死不让位。R22 只拦「显式写 width:"100%"」那一半,**不写 width**(渲染等价)完全没人管。
|
|
610
|
+
for (const hit of collectRowChildNoWidth(view)) {
|
|
611
|
+
out.push({
|
|
612
|
+
severity: 'error',
|
|
613
|
+
code: 'r22b-row-child-container-no-width',
|
|
614
|
+
componentId: hit.componentId,
|
|
615
|
+
message: hit.message,
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
|
|
503
619
|
// ── R59/R60: 切图语义化命名 + 同内容去重(slices_convention.md §1.1 / §3.1)──
|
|
504
620
|
for (const ref of collectSliceRefStrings(schema)) {
|
|
505
621
|
const name = sliceNameFromRef(ref)
|
|
@@ -525,16 +641,9 @@ export function lint(schema, data) {
|
|
|
525
641
|
}
|
|
526
642
|
|
|
527
643
|
// ── R61: 静态文案 / 字典默认值含疑似运行期数字(rules.md §十四,提醒级 WARN)──
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
}
|
|
644
|
+
// 形态表共享 lib/rules/expr.mjs(NUMERIC_HINT_PATTERNS + numericHint,测试直测):
|
|
645
|
+
// 只作「提醒 AI 语义复核」的启发式:命中 ≠ 违规(版本号 / 固定营销话术会误报),故 WARN 不阻塞。
|
|
646
|
+
const numericHit = numericHint
|
|
538
647
|
walk(view, (n) => {
|
|
539
648
|
if (n.className !== 'Text') return
|
|
540
649
|
for (const p of n.properties || []) {
|
|
@@ -564,68 +673,65 @@ export function lint(schema, data) {
|
|
|
564
673
|
})
|
|
565
674
|
}
|
|
566
675
|
|
|
567
|
-
// ── R32: Image
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
676
|
+
// ── R32 / R25: Image 硬契约(缺 rect.height、itemValueFrom 三态)──
|
|
677
|
+
// 判定与文案共享 lib/rules/image.mjs(与 preflight 同一实现,避免「同一张卡两处措辞/结论不同」)。
|
|
678
|
+
// 这里显式挑选字段构造 finding,保持 lint 输出形状与重构前逐字一致。
|
|
679
|
+
for (const f of collectImageNoHeight(view)) {
|
|
680
|
+
out.push({ severity: f.severity, code: f.code, componentId: f.componentId, message: f.message })
|
|
681
|
+
}
|
|
682
|
+
for (const f of collectImageValueFrom(view)) {
|
|
683
|
+
out.push({ severity: f.severity, code: f.code, componentId: f.componentId, message: f.message })
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ── R23: 百分比表达式缺 Math.round → JS 浮点精度 ──(共享 lib/rules/expr.mjs)
|
|
687
|
+
for (const f of collectExprNoMathRound(view)) {
|
|
688
|
+
out.push({ severity: f.severity, code: f.code, componentId: f.componentId, message: f.message })
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ── 数据绑定完备:列表数组化三件套 ──(共享 lib/rules/bindings.mjs,2026-09-25)
|
|
692
|
+
// 复现器硬伤是 error(渲染 0 项 / 空壳,必然错);未消费数组与静态 tile 是 warn
|
|
693
|
+
// (像素可以很高但数据维度缺失;硬拦在 readiness 的 bindings/lists 门——有视觉提名背书才拦)。
|
|
694
|
+
for (const f of collectRepeaterDefects(view)) {
|
|
695
|
+
out.push({ severity: f.severity, code: f.code, componentId: f.componentId, message: f.message })
|
|
696
|
+
}
|
|
697
|
+
for (const f of collectStaticTileExpansion(view)) {
|
|
698
|
+
out.push({ severity: f.severity, code: f.code, componentId: f.componentId, message: f.message })
|
|
699
|
+
}
|
|
700
|
+
{
|
|
701
|
+
const { unconsumed } = collectArrayConsumption(schema, data || {})
|
|
702
|
+
for (const u of unconsumed) {
|
|
575
703
|
out.push({
|
|
576
|
-
severity: '
|
|
577
|
-
code: '
|
|
578
|
-
componentId:
|
|
579
|
-
message: `
|
|
704
|
+
severity: 'warn',
|
|
705
|
+
code: 'binding-array-unconsumed',
|
|
706
|
+
componentId: '',
|
|
707
|
+
message: `data["${u.name}"](${u.len} 项)没有任何列表消费(复现器 value / slotScope 模板 / 事件代码 / 表达式都找不到引用)→ ` +
|
|
708
|
+
(u.semantic
|
|
709
|
+
? '名字带列表语义却没被任何列表使用:设计稿若有重复数据行,应 ListLayout/GridLayout + $' + u.name + ' + slotScope 模板(ir-format.md §2e);若该数组确无视图用途应从 data.json 删除'
|
|
710
|
+
: '若为 Canvas 图表 / 运行时配置数据可忽略本预警'),
|
|
580
711
|
})
|
|
581
712
|
}
|
|
582
|
-
}
|
|
713
|
+
}
|
|
583
714
|
|
|
584
|
-
// ──
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
})
|
|
715
|
+
// ── 宽度自适应契约(ir-format.md §2f;判定共享 lib/rules/adaptive-width.mjs,2026-09-25)──
|
|
716
|
+
// 容器类在纵向流父级里把占满语义写死成 px(98%~103% 占满父内容盒)→ error。
|
|
717
|
+
// meta 默认 rect.width 就是 100%,覆盖成 px = 显式锁死;改 100% 与设计宽逐像素等价且自适应。
|
|
718
|
+
for (const f of collectFullwidthFixedPx(view)) {
|
|
719
|
+
out.push({ severity: f.severity, code: f.code, componentId: f.componentId, message: f.message })
|
|
720
|
+
}
|
|
609
721
|
|
|
610
|
-
// ──
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
message: `"${n.componentId}" 表达式含 *100 但无 Math.round → JS 浮点精度溢出(R23): ${p.itemValue.slice(0, 50)}`,
|
|
624
|
-
})
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
})
|
|
722
|
+
// ── 结构与事件代码:pitfalls 教训 → 机器 gate ──(共享 lib/rules/structure.mjs,2026-09-25)
|
|
723
|
+
// §27 slot 平铺 / R34 null 子节点 / §20.4 style 重复子项是 error(必然错、存量零命中);
|
|
724
|
+
// §10 整体回传 / R20 Icon 缺 rect / R38 Root 定高是 warn(存量有命中或语义复核兜底)。
|
|
725
|
+
for (const f of [
|
|
726
|
+
...collectSlotMultiChildren(view),
|
|
727
|
+
...collectNullChildren(view),
|
|
728
|
+
...collectDuplicateStyleChildren(view),
|
|
729
|
+
...collectAlertDataWholeState(view),
|
|
730
|
+
...collectIconNoRect(view),
|
|
731
|
+
...collectRootFixedHeight(view),
|
|
732
|
+
]) {
|
|
733
|
+
out.push({ severity: f.severity, code: f.code, componentId: f.componentId, message: f.message })
|
|
734
|
+
}
|
|
629
735
|
|
|
630
736
|
// R17/R22: flexShrink 铁律
|
|
631
737
|
// R17: 关键 Text(金额/数字/标签/标题/占比/单位)必须 flexShrink:0(防换行)
|
|
@@ -762,6 +868,66 @@ export function lint(schema, data) {
|
|
|
762
868
|
})
|
|
763
869
|
|
|
764
870
|
|
|
871
|
+
/* ── R63: width:100% 撞水平 margin —— 交叉轴定宽不可缩,节点被推出父容器右缘 ────────
|
|
872
|
+
* 渲染器语义(vendor/preview 的 StyleParser + adapter.doSize):"100%" 原样透传成 width:100%,
|
|
873
|
+
* 而 width:100% 已经等于**父容器内容盒**宽度;水平 margin 再叠加 = 元素外框超出父容器 → 向右溢出、
|
|
874
|
+
* 被 overflow:hidden 裁掉(Root / 组件默认 hidden)。实测事故:模型把「父容器水平内缩」写成
|
|
875
|
+
* 「子节点 width:100% + marginLeft/Right」,节点右侧一整块内容消失。
|
|
876
|
+
*
|
|
877
|
+
* 为什么只报交叉轴(父容器按列排布):
|
|
878
|
+
* 主轴(父容器 flex-direction:row)里 flexShrink 默认 1,会把这个节点缩回可用宽 —— margin 被吸收,
|
|
879
|
+
* 不溢出;只有交叉轴(column:Root / Column / SingleColumn / flexDirection:"column" /
|
|
880
|
+
* AbsoluteLayout 的 abspos 子节点——定宽对定位包含块解析、无 shrink 参与)才是**必溢出**。
|
|
881
|
+
* 实测:全仓 205 份 schema 里 4 处「width:100% + marginLeft」全在 Row 父容器(accountCreditedlist /
|
|
882
|
+
* memberLevelNone / pointsDetail / assetOverview),全靠 shrink 吃掉间距——故本规则不报主轴,零误报。
|
|
883
|
+
*/
|
|
884
|
+
const laysOutVertically = (parent) => {
|
|
885
|
+
if (!parent) return false
|
|
886
|
+
// AbsoluteLayout 子节点是 position:absolute 脱标:100% 对定位包含块(容器 padding box)取宽,
|
|
887
|
+
// margin 不参与任何收缩 → 与 column 同款必溢出。
|
|
888
|
+
if (parent.className === 'AbsoluteLayout') return true
|
|
889
|
+
const explicit = flexSub(parent, 'flexDirection')
|
|
890
|
+
if (explicit) return explicit === 'column' // 显式 flexDirection(内联样式)覆盖 CSS 类的默认方向
|
|
891
|
+
return COLUMN_FLOW.has(parent.className)
|
|
892
|
+
}
|
|
893
|
+
walk(view, (n, parent) => {
|
|
894
|
+
if (!parent) return
|
|
895
|
+
const cid = n.componentId || ''
|
|
896
|
+
const styleProps = (n.style && n.style.properties) || []
|
|
897
|
+
const rectGroup = styleProps.find((p) => p.itemKey === 'rect')
|
|
898
|
+
const widthChild = ((rectGroup && rectGroup.itemChildren && rectGroup.itemChildren.properties) || [])
|
|
899
|
+
.find((c) => c.itemKey === 'width')
|
|
900
|
+
if (!widthChild || String(widthChild.itemValue ?? '').trim() !== '100%') return
|
|
901
|
+
if (String(widthChild.itemValueFrom ?? '0') === '1') return // 表达式绑定(值不是字面 100%)
|
|
902
|
+
const marginGroup = styleProps.find((p) => p.itemKey === 'margin')
|
|
903
|
+
const marginChildren = (marginGroup && marginGroup.itemChildren && marginGroup.itemChildren.properties) || []
|
|
904
|
+
const constNum = (key) => {
|
|
905
|
+
const c = marginChildren.find((x) => x.itemKey === key)
|
|
906
|
+
if (!c || String(c.itemValueFrom ?? '0') === '1') return null // 表达式绑定:值不定,不判
|
|
907
|
+
const v = parseFloat(String(c.itemValue).replace(/[^0-9.\-]/g, ''))
|
|
908
|
+
return Number.isFinite(v) ? v : null
|
|
909
|
+
}
|
|
910
|
+
const mtype = String((marginChildren.find((c) => c.itemKey === 'marginType') || {}).itemValue ?? '')
|
|
911
|
+
const hmargin = []
|
|
912
|
+
if (mtype === '1') { // 逐边模式:只看左右
|
|
913
|
+
const ml = constNum('marginLeft')
|
|
914
|
+
const mr = constNum('marginRight')
|
|
915
|
+
if (ml !== null && ml > 0) hmargin.push(`marginLeft:${ml}`)
|
|
916
|
+
if (mr !== null && mr > 0) hmargin.push(`marginRight:${mr}`)
|
|
917
|
+
} else if (mtype === '0') { // 全边模式:margin 四边同值,左右也吃
|
|
918
|
+
const m = constNum('margin')
|
|
919
|
+
if (m !== null && m > 0) hmargin.push(`margin(全边):${m}`)
|
|
920
|
+
}
|
|
921
|
+
if (!hmargin.length || !laysOutVertically(parent)) return
|
|
922
|
+
out.push({
|
|
923
|
+
severity: 'error',
|
|
924
|
+
code: 'r63-width100-with-hmargin',
|
|
925
|
+
componentId: cid,
|
|
926
|
+
message: `${n.className} "${cid}" 同时写了 rect.width:"100%" 与水平 margin(${hmargin.join(' / ')})—— width:100% 已经等于父容器内容宽,水平 margin 只会把节点推出父容器右缘(父容器 ${parent.className} 按列排布,水平方向是交叉轴,flexShrink 救不回来)→ 右侧内容被 overflow:hidden 裁掉(R63)。正路:① 水平间距挪到父容器 paddingLeft/Right(或由父容器 gap 承担);② 流式父容器(Column/Root,alignItems 为空或 stretch)把 rect.width 由 "100%" 改 "auto"(显式写 auto;Text/MultiText 有组件样式 width:fit-content,只删 width 不会撑满,实测 86.67px vs 369px),靠 stretch 补宽、保留 margin(AbsoluteLayout 的 abspos 子节点 width:auto 是 fit-content、无 stretch —— 走 ① 或改显式像素宽);③ 并排分栏用「左 width:100%+flexShrink:1,右 auto+flexShrink:0」`,
|
|
927
|
+
})
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
|
|
765
931
|
// ── 格式铁律:值格式校验(生成器最常犯的格式错误) ──
|
|
766
932
|
|
|
767
933
|
// R-FMT1: borderRadius 值不得用元组格式 {a,b,c,d}(应为标量 "12" 或单独角 borderTopLeftRadius 等)
|
|
@@ -833,6 +999,55 @@ export function lint(schema, data) {
|
|
|
833
999
|
}
|
|
834
1000
|
})
|
|
835
1001
|
|
|
1002
|
+
// R64: 装饰条不得靠流式布局定位 —— 「标题+下划线」没按范式做的形态。
|
|
1003
|
+
// 为什么单开一条:R42 只在「已经是 AbsoluteLayout」的节点上校验,用流式堆叠的写法可以整条绕过它;
|
|
1004
|
+
// R38 又只禁**负** margin,模型改用**正** marginTop 造间隙,形式上完全合规。实测(2026-09-21
|
|
1005
|
+
// transferRecord)Column > [Text, Row 42x5 marginTop:3] 的卡 lint 报 0 error / 0 warning,
|
|
1006
|
+
// 而设计稿是 68x8 的条子压在标题底部(band 底对齐)——结构、尺寸、重叠关系、位置四处都不对,
|
|
1007
|
+
// 装饰条还占掉 8px 流式高度把兄弟节点整体下推。
|
|
1008
|
+
// 判定:流式容器里紧跟 Text 之后、有实色背景、高 ≤10、宽为像素或 `$变量.length * 字号` 的 Row
|
|
1009
|
+
// (即「本应绝对定位的装饰条」)。已带 position 的(复用了范式)与 Divider 一律放过。
|
|
1010
|
+
walk(view, (n) => {
|
|
1011
|
+
if (n.className === 'AbsoluteLayout') return
|
|
1012
|
+
const kids = directChildren(n)
|
|
1013
|
+
if (kids.length < 2) return
|
|
1014
|
+
for (let i = 1; i < kids.length; i += 1) {
|
|
1015
|
+
const bar = kids[i]
|
|
1016
|
+
const prev = kids[i - 1]
|
|
1017
|
+
if (!bar || !prev || bar.className !== 'Row' || prev.className !== 'Text') continue
|
|
1018
|
+
const child = (node, groupKey, childKey) => {
|
|
1019
|
+
const g = (node.style?.properties || []).find((x) => x.itemKey === groupKey)
|
|
1020
|
+
const c = (g?.itemChildren?.properties || []).find((x) => x.itemKey === childKey)
|
|
1021
|
+
return c ? c.itemValue : ''
|
|
1022
|
+
}
|
|
1023
|
+
const bgType = child(bar, 'background', 'backgroundType')
|
|
1024
|
+
const bgColor = child(bar, 'background', 'backgroundColor')
|
|
1025
|
+
if (String(bgType) !== '01' || !bgColor) continue
|
|
1026
|
+
const barHeight = Number(child(bar, 'rect', 'height'))
|
|
1027
|
+
if (!(barHeight > 0 && barHeight <= 10)) continue
|
|
1028
|
+
const barWidth = String(child(bar, 'rect', 'width') || '')
|
|
1029
|
+
const pixelOrExpr = /^\d+(\.\d+)?$/.test(barWidth.trim()) || /\.length \*/.test(barWidth)
|
|
1030
|
+
if (!pixelOrExpr) continue
|
|
1031
|
+
const posTop = child(bar, 'position', 'top')
|
|
1032
|
+
const posLeft = child(bar, 'position', 'left')
|
|
1033
|
+
if (String(posTop).trim() || String(posLeft).trim()) continue
|
|
1034
|
+
const cid = bar.componentId || '(no id)'
|
|
1035
|
+
// 标题文案:常量在 text.text,变量绑定在 properties[].value(如 $title)——拿不到就退回节点 id
|
|
1036
|
+
const boundValue = (prev.properties || []).find((x) => x.itemKey === 'value')?.itemValue
|
|
1037
|
+
const title = String(child(prev, 'text', 'text') || boundValue || prev.componentId || '标题')
|
|
1038
|
+
out.push({
|
|
1039
|
+
severity: 'error',
|
|
1040
|
+
code: 'r64-title-underline-flow',
|
|
1041
|
+
componentId: cid,
|
|
1042
|
+
message: `Row "${cid}"(高 ${barHeight} / 宽 ${barWidth})是标题「${title}」的装饰条,却排在流式布局里 —— `
|
|
1043
|
+
+ `它会占掉流式高度把后续兄弟整体下推,且拿不到设计稿的「条子压在标题底部」的重叠关系。`
|
|
1044
|
+
+ `必须复用 title-underline-overlay 范式:父容器改 AbsoluteLayout(align:"BottomLeft", rect.height=标题行高),`
|
|
1045
|
+
+ `装饰条与标题各自设 style.position.left/top(条 top = 标注条.y − 标注标题.y,不是公式推算),`
|
|
1046
|
+
+ `子节点顺序 [装饰条, 标题]。IR 可照抄 references/ir-format.md §2d(脚本 expand_ir.mjs --ir 展开,禁正/负 margin 顶位)。`,
|
|
1047
|
+
})
|
|
1048
|
+
}
|
|
1049
|
+
})
|
|
1050
|
+
|
|
836
1051
|
// R44: 类型+值成对检查 —— 有值但缺类型开关的样式组(borderRadius/border/shadow)→ ERROR
|
|
837
1052
|
walk(view, (n) => {
|
|
838
1053
|
const cid = n.componentId || '(root)'
|
|
@@ -990,6 +1205,42 @@ export function lint(schema, data) {
|
|
|
990
1205
|
}
|
|
991
1206
|
})
|
|
992
1207
|
|
|
1208
|
+
// R50b: 未归一化的列表作用域别名(哨兵,**不是**「禁止 $item」的规则)。
|
|
1209
|
+
//
|
|
1210
|
+
// 2026-09-24 定论:`$item.xxx` 不是模型的问题 —— 我们自己把它手写进了系统提示词
|
|
1211
|
+
// (提交 56e9894)与 ir-format.md,模型只是照做。所以处理方式是**编译器归一化**:
|
|
1212
|
+
// · IR → schema:expand_ir.mjs 的 expandIR() 开头调用 lib/scope_alias.mjs 改写;
|
|
1213
|
+
// · schema 级:autofix_schema.py 规则 5 改写(gate 第 1 步 / iterate 内建 autofix 都会跑到)。
|
|
1214
|
+
// 规范形只有三种:`$<字段名>` / `$$` / `$_index`(运行时 makeSlotScope 的作用域没有 `item` 键)。
|
|
1215
|
+
//
|
|
1216
|
+
// 那这条规则还拦什么:**未走过归一化管线的 schema**(外部手改、老产物、绕过 expand/autofix
|
|
1217
|
+
// 直接写盘)。此时 lint 是最后一道可机械判定的闸 —— 命中说明该 schema 该跑一次
|
|
1218
|
+
// `autofix_schema.py`,而不是说作者写错了字。CI 对已提交 schema 跑 lint 时它是唯一防线。
|
|
1219
|
+
walk(view, (n) => {
|
|
1220
|
+
const cid = n.componentId || '(root)'
|
|
1221
|
+
const hit = []
|
|
1222
|
+
const scan = (where, val) => {
|
|
1223
|
+
const s = String(val || '')
|
|
1224
|
+
if (!/\$item(?![A-Za-z0-9_$])/.test(s)) return
|
|
1225
|
+
hit.push(`${where}: ${s.length > 90 ? s.slice(0, 90) + '…' : s}`)
|
|
1226
|
+
}
|
|
1227
|
+
for (const p of (n.properties || [])) if (String(p.itemValueFrom) === '1') scan(p.itemKey || '?', p.itemValue)
|
|
1228
|
+
for (const g of (n.style?.properties || [])) {
|
|
1229
|
+
for (const c of (g.itemChildren?.properties || [])) if (String(c.itemValueFrom) === '1') scan(`style.${g.itemKey}.${c.itemKey}`, c.itemValue)
|
|
1230
|
+
}
|
|
1231
|
+
for (const d of (n.directives || [])) if (String(d.itemValueFrom) === '1') scan(`directive.${d.itemKey}`, d.itemValue)
|
|
1232
|
+
for (const ea of (n.eventactions || [])) {
|
|
1233
|
+
for (const p of (ea?.action?.properties || [])) if (p?.itemValue?.code) scan(`action.${p.itemKey}.code`, p.itemValue.code)
|
|
1234
|
+
}
|
|
1235
|
+
if (!hit.length) return
|
|
1236
|
+
out.push({
|
|
1237
|
+
severity: 'error',
|
|
1238
|
+
code: 'r50b-unnormalized-scope-alias',
|
|
1239
|
+
componentId: cid,
|
|
1240
|
+
message: `${n.className} "${cid}" 的绑定里含未归一化的列表作用域别名 $item —— 运行时 makeSlotScope 的作用域里**没有 item 这个键**(它是「item 字段逐个展开 ∪ {$$: 整项, _index: 索引}」),求值恒为 undefined 且不抛错(validate/preflight/render 全绿,只在 /__render-status 的 bindingErrors 留痕)。规范形:字段写 \`$<字段名>\`、整项写 \`$$\`、索引写 \`$_index\`。**这不是要你手改**:跑一次 \`autofix_schema.py <schema>\`(或 \`expand_ir.mjs\` 重新展开 IR)即自动归一化;命中说明该 schema 绕过了归一化管线。命中:${hit.join(' | ')}`,
|
|
1241
|
+
})
|
|
1242
|
+
})
|
|
1243
|
+
|
|
993
1244
|
// R51: customAction 里 this.data 只允许读 Input 绑定键的实时输入值;运行态变量一律 this.state
|
|
994
1245
|
const inputBoundKeys = new Set()
|
|
995
1246
|
walk(view, (n) => {
|