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,634 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* readiness.mjs — S4.9 **内容就绪门禁**:像素自循环的前置条件。
|
|
4
|
+
*
|
|
5
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
* 为什么必须存在(2026-09-24 gen-1 实机事故,用户原话):
|
|
7
|
+
* 「只有在确保内容和要素的齐全和准备再做像素自循环,不然跟个傻逼一样浪费时间在
|
|
8
|
+
* 像素循环里打转。这次卡片连基本的切图引入跑了半天都没为我添加上去」
|
|
9
|
+
*
|
|
10
|
+
* 实测证据:gen-1 两个回合合计 **623 万 prompt token** 全部作废。
|
|
11
|
+
* 回合 tmuf9c4h1ml778e:61 步 / 67 次工具调用 / 5,254,936 promptTokens / aborted;
|
|
12
|
+
* iterate 历史 r4–r13 十轮 match 死死卡在 87–90.46(门禁要 95),
|
|
13
|
+
* `gate.stage` 全程 = 'pixel' —— 结构早就过了,纯在像素上空转。
|
|
14
|
+
* 而同一时间:Image 节点的图片地址因为属性键写成 `image`(合法键只有 `value`)
|
|
15
|
+
* 被运行时静默丢弃,渲染出来是个破图。**要素不全却已经开始刷像素。**
|
|
16
|
+
*
|
|
17
|
+
* 根因不是模型偷懒,是**流程里没有这道门禁**:SKILL.md 的阶段编号从 2 直接跳到 4,
|
|
18
|
+
* iterate 每轮无条件 render + pixelDiff(最贵的一步),没有任何一步会先问
|
|
19
|
+
* 「内容齐了吗?变量接上了吗?切图到位了吗?」
|
|
20
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* 六道内容门禁(前五道是 **schema 侧**声明性判定,不需要渲染 ⇒ 可以挡在 render 之前;
|
|
23
|
+
* 第六道是 **spec 输入侧**契约检查,只在你传了 `--spec` 时才跑):
|
|
24
|
+
*
|
|
25
|
+
* A components 组件契约:className / 属性键 / 样式组 / 样式子键必须在元数据白名单内。
|
|
26
|
+
* 不存在的键被运行时**整条静默丢弃**(Image 写 `image` ⇒ 破图且零报错)。
|
|
27
|
+
* B elements 要素齐全:设计稿的每条文案都要在 schema 里找得到(缺失即拦)。
|
|
28
|
+
* C bindings 变量绑定完备:`$var` 引用的根变量必须真的存在。
|
|
29
|
+
* **只在槽位外做硬判定**——槽位内的 `$label`/`$name` 是列表项字段,
|
|
30
|
+
* 无法静态推导,硬判会大面积误报(实测全仓 24 个「未解析」root 全在槽位内)。
|
|
31
|
+
* D images 图片地址已配置:每个 Image 节点都必须有非空的 value。
|
|
32
|
+
* —— gen-1 「连图片地址都没配置」的**直接对应项**。
|
|
33
|
+
* E slices 切图路径已配置:schema 里每个 `/slices/...` 引用都必须在 src/slices 落盘
|
|
34
|
+
* (R31c:悬空引用渲染 404、图空白、控制台零报错)。
|
|
35
|
+
* F spec 上游产物契约完整:spec.json 的 `cards[<i>].mask` / `cards[<i>].rect` 必须存在
|
|
36
|
+
* (契约 `skills/shared/spec-contract.md`)。缺失多为**旧版 spec 产物**
|
|
37
|
+
* (mask/focusMetrics 批次字段引入前生成)⇒ 重跑上游补全,**不要在下游加兼容分支**
|
|
38
|
+
* (兼容分支会让「上游漏产」永远不被发现)。
|
|
39
|
+
*
|
|
40
|
+
* ── 判据选型:先量测再定门禁(全仓 75 张卡 / 754 条 `$` 引用)──────────────
|
|
41
|
+
* 槽位外未解析 root(可硬判)............. 0 处 → 0 误报,定 ERROR
|
|
42
|
+
* 槽位内未解析 root(不可硬判)........... 24 个不同名 → 全是列表项字段/运行时 setData,只统计不拦
|
|
43
|
+
* 非法 itemValueFrom(非 0/1/2).......... 0 处 → 0 误报,定 ERROR
|
|
44
|
+
* from=1 但值不含 $ ...................... 0 处 → 0 误报,定 ERROR
|
|
45
|
+
* from=2 但值是 $ 表达式.................. 0 处 → 0 误报,定 ERROR(url($icon) 画布空白类)
|
|
46
|
+
* Image 无 value / value 为空 ............. 0 处 → 0 误报,定 ERROR
|
|
47
|
+
* 引用在 src/slices 无对应文件 ............ 4 处 → 真实存量债(r31c),定 ERROR
|
|
48
|
+
*
|
|
49
|
+
* ── 明确**不**纳入本门禁的(附理由,避免以后有人「顺手补上」)──────────────
|
|
50
|
+
* · 「设计稿声明的切图是否逐张被引用」——名字匹配不可靠。实测 all-components-test:
|
|
51
|
+
* 设计稿资产名 `编组 4`,schema 里是 `iconArrowRightOrange.png`(同位置、模型语义重命名);
|
|
52
|
+
* 按名字对账会把一张好卡判死。而**几何对账也走不通**:style.rect 只有 width/height
|
|
53
|
+
* (style-meta.json 实测),本 DSL 根本不支持声明绝对定位 ⇒ 无法判断「这个位置有没有图」。
|
|
54
|
+
* 故降级为 `checks.slices.unreferenced` **警告**,只点名不拦。
|
|
55
|
+
* · 字号 / 颜色保真度 —— 那些是**像素层**问题,正是本门禁要拦在门外的东西。
|
|
56
|
+
* · layout / geometry(collapsed / outOfCard)—— 需要 render 的 elementBoxes,属结构层,由 iterate 的 gate 管。
|
|
57
|
+
*
|
|
58
|
+
* ── 用法 ────────────────────────────────────────────────────────────────────
|
|
59
|
+
* node readiness.mjs --id <cardId> [--spec <spec.json>] [--design <card-N.png>] [--json]
|
|
60
|
+
* node readiness.mjs --schema <schema.json> --id <cardId> [--spec ...] [--json]
|
|
61
|
+
*
|
|
62
|
+
* 退出码:0 = 就绪(可以进像素循环);3 = 未就绪(**不是**脚本报错,是内容没准备好);1 = 用法/IO 错误。
|
|
63
|
+
* 3 与 iterate 的 2(gate-fail)分开,便于调用方区分「内容没齐」与「像素没达标」。
|
|
64
|
+
*
|
|
65
|
+
* 环境变量逃生门:DSL_CARD_SKIP_READINESS=1 或 --no-readiness ⇒ 完全跳过(debug 用)。
|
|
66
|
+
*/
|
|
67
|
+
import fs from 'node:fs'
|
|
68
|
+
import path from 'node:path'
|
|
69
|
+
import { pathToFileURL } from 'node:url'
|
|
70
|
+
|
|
71
|
+
import { metaDir as DEFAULT_META_DIR, slicesDir as DEFAULT_SLICES_DIR, cardAuditPath, cardSchemaPath, cardDataPath } from './lib/paths.mjs'
|
|
72
|
+
import { loadContract, collectContractViolations, describeViolation } from './lib/component_contract.mjs'
|
|
73
|
+
import { buildSliceFileIndex, collectSliceRefStrings, resolveSliceRefOnDisk, isRemoteSliceRef, sliceNameMatches } from './lib/slice_dedupe.mjs'
|
|
74
|
+
import { buildFidelityReport } from './check_fidelity.mjs'
|
|
75
|
+
import { collectArrayConsumption } from './lib/rules/bindings.mjs'
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 表达式里天然可用的名字(渲染器注入,不需要卡片声明)。
|
|
79
|
+
* `$$` = 当前列表项整体、`_index` = 列表下标 —— 见 card-editor/runtime/evaluate.js `makeSlotScope`。
|
|
80
|
+
*/
|
|
81
|
+
const BUILTIN_ROOTS = new Set(['$$', '_index', 'item', 'index', 'globalSet', 'state'])
|
|
82
|
+
|
|
83
|
+
/* ────────────────────────────────────────────────────────────────────────
|
|
84
|
+
* 根变量表:schema 里 `$name` 的 name 必须能在这里找到出处。
|
|
85
|
+
*
|
|
86
|
+
* 三个来源(与运行时取值同源):
|
|
87
|
+
* 1. dictionary.inputParams[].key 权威数据(data.json 是它的派生投影)
|
|
88
|
+
* 2. dictionary.mocks{} 演示数据
|
|
89
|
+
* 3. data.json 顶层键 渲染真正吃的值
|
|
90
|
+
* + 数组型 defaultValue 的**元素字段名**(`$txName` 这种扁平访问,见 makeSlotScope 的字段展开)
|
|
91
|
+
* + 事件代码 `this.setData({ k: ... })` 里写入的键(实测 monthlyBill 的 `$renderListData`
|
|
92
|
+
* 就是运行时由 customAction 造的,不在 dictionary 里 —— 不扫这段会把它误判成悬空引用)
|
|
93
|
+
* ──────────────────────────────────────────────────────────────────────── */
|
|
94
|
+
export function buildKnownRoots(schema, data) {
|
|
95
|
+
const known = new Set(BUILTIN_ROOTS)
|
|
96
|
+
const addArrayFields = (v) => {
|
|
97
|
+
if (Array.isArray(v) && v[0] && typeof v[0] === 'object') for (const k of Object.keys(v[0])) known.add(k)
|
|
98
|
+
}
|
|
99
|
+
for (const p of schema?.dictionary?.inputParams || []) {
|
|
100
|
+
if (p && typeof p.key === 'string' && p.key) {
|
|
101
|
+
known.add(p.key)
|
|
102
|
+
addArrayFields(p.defaultValue)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const [k, v] of Object.entries(schema?.dictionary?.mocks || {})) {
|
|
106
|
+
known.add(k)
|
|
107
|
+
addArrayFields(v)
|
|
108
|
+
}
|
|
109
|
+
for (const [k, v] of Object.entries(data || {})) {
|
|
110
|
+
known.add(k)
|
|
111
|
+
addArrayFields(v)
|
|
112
|
+
}
|
|
113
|
+
// 事件代码写入的变量。两种形态都扫:
|
|
114
|
+
// ① `this.setData({ renderListData: renderList })` —— monthlyBill 实测:`$renderListData` 由
|
|
115
|
+
// customAction 在运行时造出来,不在 dictionary 里,不扫这段会误判成悬空引用。
|
|
116
|
+
// ② `this.state.xxx = ...` / `this.data.xxx = ...` 直接赋值。
|
|
117
|
+
for (const code of collectActionCode(schema)) {
|
|
118
|
+
for (const m of code.matchAll(/setData\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
|
|
119
|
+
for (const line of m[1].split(',')) {
|
|
120
|
+
const k = line.split(':')[0].replace(/[\s{}[\]]/g, '')
|
|
121
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k)) known.add(k)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const m of code.matchAll(/this\.(?:state|data)\.([A-Za-z_$][A-Za-z0-9_$]*)\s*=/g)) known.add(m[1])
|
|
125
|
+
}
|
|
126
|
+
return known
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 收集 schema 里所有事件动作代码。
|
|
131
|
+
*
|
|
132
|
+
* ⚠️ 结构实测(2026-09-24,monthlyBill):代码**不在** `action.code`,而在
|
|
133
|
+
* `action.properties[]` 里 `itemKey === 'function'` 那一项的 `itemValue`(对象,含 `code`/`compiled`)。
|
|
134
|
+
* 按 `action.code` 取会永远拿到空数组 ⇒ `$renderListData` 这类运行时变量被当成悬空引用(实测误报)。
|
|
135
|
+
*/
|
|
136
|
+
function collectActionCode(schema) {
|
|
137
|
+
const out = []
|
|
138
|
+
const push = (v) => {
|
|
139
|
+
if (typeof v === 'string') {
|
|
140
|
+
if (v) out.push(v)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (v && typeof v === 'object') {
|
|
144
|
+
for (const k of ['code', 'compiled', 'expression']) if (typeof v[k] === 'string' && v[k]) out.push(v[k])
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const walk = (n) => {
|
|
148
|
+
if (!n || typeof n !== 'object') return
|
|
149
|
+
for (const ea of n.eventactions || []) {
|
|
150
|
+
const a = ea?.action
|
|
151
|
+
if (!a) continue
|
|
152
|
+
for (const k of ['code', 'compiled', 'expression', 'actionValue']) push(a[k])
|
|
153
|
+
for (const p of a.properties || []) push(p?.itemValue)
|
|
154
|
+
}
|
|
155
|
+
for (const c of n.components || []) walk(c)
|
|
156
|
+
for (const s of n.slots || []) for (const c of s.components || []) walk(c)
|
|
157
|
+
}
|
|
158
|
+
walk(schema?.view || schema)
|
|
159
|
+
return out
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ── 绑定引用收集:区分「复现器插槽外」(可硬判)与「复现器插槽内」(只能统计)──────
|
|
163
|
+
*
|
|
164
|
+
* ⚠️ 判据是**类**不是 slotKey(2026-09-24 实测踩坑):
|
|
165
|
+
* 本 DSL 里子节点一律存在 `slots[].components`,而 `slotKey` **恒为 `default`**
|
|
166
|
+
* (全仓 1010 个 slot:Row 597 / Column 328 / ListLayout 54 / AbsoluteLayout 20 / GridLayout 11)。
|
|
167
|
+
* 第一版按「有没有 slots 就算子节点在槽位里」判断 ⇒ gen-1 的 21 条引用**全部**被误标成
|
|
168
|
+
* `refsInsideSlots: 21 / refsOutsideSlots: 0` ⇒ 变量绑定门禁整条形同虚设(实测把
|
|
169
|
+
* `$notDeclaredVar` 这种悬空引用放行)。
|
|
170
|
+
* 正解:只有 `ListLayout` / `GridLayout` 这类**复现器**(元数据同时声明 `value` 属性与
|
|
171
|
+
* default 插槽)的插槽内才有 item 作用域,其字段无法静态穷举;其余容器的 default 插槽
|
|
172
|
+
* 就是普通嵌套,引用照样按根作用域解析。
|
|
173
|
+
*/
|
|
174
|
+
/**
|
|
175
|
+
* @param {object} schema
|
|
176
|
+
* @param {Set<string>} repeaterClasses 由 component-meta 推导(见 lib/component_contract.mjs)
|
|
177
|
+
* @returns {{ outside: Array<object>, slotRefs: Array<object>, slotCount: number,
|
|
178
|
+
* slotRoots: Set<string>, fromViolations: Array<object> }}
|
|
179
|
+
*/
|
|
180
|
+
export function collectBindingRefs(schema, repeaterClasses = new Set()) {
|
|
181
|
+
const outside = []
|
|
182
|
+
const slotRefs = []
|
|
183
|
+
const fromViolations = []
|
|
184
|
+
const slotRoots = new Set()
|
|
185
|
+
let slotCount = 0
|
|
186
|
+
|
|
187
|
+
/** @param {object} node @param {boolean} inSlot */
|
|
188
|
+
const visit = (node, inSlot) => {
|
|
189
|
+
if (!node || typeof node !== 'object') return
|
|
190
|
+
const props = []
|
|
191
|
+
for (const p of node.properties || []) props.push({ p, where: 'properties' })
|
|
192
|
+
for (const g of node.style?.properties || []) {
|
|
193
|
+
for (const c of g.itemChildren?.properties || []) props.push({ p: c, where: `style.${g.itemKey}` })
|
|
194
|
+
}
|
|
195
|
+
for (const { p, where } of props) {
|
|
196
|
+
if (!p || typeof p !== 'object') continue
|
|
197
|
+
const key = p.itemKey
|
|
198
|
+
if (typeof key !== 'string' || !key) continue
|
|
199
|
+
const from = String(p.itemValueFrom ?? '')
|
|
200
|
+
const val = typeof p.itemValue === 'string' ? p.itemValue : ''
|
|
201
|
+
// 三态取值口径(与 preflight Check 3 的 Image 版同源,这里推广到全部属性):
|
|
202
|
+
if (from === '1' && !val.includes('$')) {
|
|
203
|
+
fromViolations.push({ code: 'from1-not-expression', cid: node.componentId, cls: node.className, key, where, value: String(p.itemValue).slice(0, 60) })
|
|
204
|
+
} else if (from === '2' && val.trim().startsWith('$')) {
|
|
205
|
+
fromViolations.push({ code: 'from2-is-expression', cid: node.componentId, cls: node.className, key, where, value: val.slice(0, 60) })
|
|
206
|
+
} else if (from !== '' && from !== '0' && from !== '1' && from !== '2') {
|
|
207
|
+
fromViolations.push({ code: 'illegal-itemValueFrom', cid: node.componentId, cls: node.className, key, where, value: from })
|
|
208
|
+
}
|
|
209
|
+
if (!val.includes('$')) continue
|
|
210
|
+
const roots = new Set()
|
|
211
|
+
for (const m of val.matchAll(/\$([A-Za-z_][A-Za-z0-9_]*)/g)) roots.add(m[1])
|
|
212
|
+
if (!roots.size) continue
|
|
213
|
+
const rec = { cid: node.componentId, cls: node.className, key, where, value: val.slice(0, 60), roots: [...roots] }
|
|
214
|
+
if (inSlot) {
|
|
215
|
+
slotCount++
|
|
216
|
+
slotRefs.push(rec)
|
|
217
|
+
for (const r of roots) slotRoots.add(r)
|
|
218
|
+
} else {
|
|
219
|
+
for (const r of roots) outside.push({ root: r, ...rec })
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// 只有**复现器**的插槽才开 item 作用域(判据见上方注释:类名,不是 slotKey)
|
|
223
|
+
const isRepeater = repeaterClasses.has(node.className)
|
|
224
|
+
for (const c of node.components || []) visit(c, inSlot)
|
|
225
|
+
for (const s of node.slots || []) for (const c of s.components || []) visit(c, inSlot || isRepeater)
|
|
226
|
+
}
|
|
227
|
+
visit(schema?.view || schema, false)
|
|
228
|
+
return { outside, slotRefs, slotCount, slotRoots, fromViolations }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── Image 图片地址配置 ──────────────────────────────────────────────── */
|
|
232
|
+
export function collectImageConfig(schema) {
|
|
233
|
+
const problems = []
|
|
234
|
+
let nodes = 0
|
|
235
|
+
const walk = (n) => {
|
|
236
|
+
if (!n || typeof n !== 'object') return
|
|
237
|
+
if (n.className === 'Image') {
|
|
238
|
+
nodes++
|
|
239
|
+
const vp = (n.properties || []).find((p) => p.itemKey === 'value')
|
|
240
|
+
if (!vp) {
|
|
241
|
+
problems.push({ cid: n.componentId, code: 'image-no-value-key', detail: '没有 itemKey="value" 的属性(该组件图片地址的键固定为 value)' })
|
|
242
|
+
} else if (!String(vp.itemValue ?? '').trim()) {
|
|
243
|
+
problems.push({ cid: n.componentId, code: 'image-empty-value', detail: 'value 为空 ⇒ 渲染器回落到默认占位图 ⇒ 破图' })
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const c of n.components || []) walk(c)
|
|
247
|
+
for (const s of n.slots || []) for (const c of s.components || []) walk(c)
|
|
248
|
+
}
|
|
249
|
+
walk(schema?.view || schema)
|
|
250
|
+
return { nodes, problems }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ── 切图路径:引用落盘 + 设计稿声明覆盖 ─────────────────────────────── */
|
|
254
|
+
function checkSlices({ schema, specPath, cardIndex, designPath, slicesDir, auditPath }) {
|
|
255
|
+
const index = buildSliceFileIndex(slicesDir)
|
|
256
|
+
const refs = collectSliceRefStrings(schema)
|
|
257
|
+
const missingOnDisk = []
|
|
258
|
+
for (const ref of refs) {
|
|
259
|
+
if (isRemoteSliceRef(ref)) continue
|
|
260
|
+
const { resolved, name } = resolveSliceRefOnDisk(index, ref)
|
|
261
|
+
if (!resolved) missingOnDisk.push({ ref, expect: name })
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const out = { passed: missingOnDisk.length === 0, refs, missingOnDisk, declared: [], unreferenced: [], auditFresh: false, auditNote: '', slicesDir }
|
|
265
|
+
|
|
266
|
+
// 设计稿声明面(需要 S2 审计 + 与当前设计稿同一份)
|
|
267
|
+
let audit = null
|
|
268
|
+
const candidate = auditPath || ''
|
|
269
|
+
if (candidate && fs.existsSync(candidate)) {
|
|
270
|
+
try { audit = JSON.parse(fs.readFileSync(candidate, 'utf8')) } catch { audit = null }
|
|
271
|
+
}
|
|
272
|
+
if (!audit) {
|
|
273
|
+
out.auditNote = '没有 S2 审计(.cache/ai-scratch/<id>.audit.json)⇒ 无法核对「设计稿声明的切图是否都被引用」,本项从略'
|
|
274
|
+
return out
|
|
275
|
+
}
|
|
276
|
+
const auditCard = Number(audit?.card?.index)
|
|
277
|
+
const auditDesign = audit?.design?.path ? path.resolve(audit.design.path) : ''
|
|
278
|
+
const curDesign = designPath ? path.resolve(designPath) : ''
|
|
279
|
+
const cardOk = !Number.isFinite(auditCard) || !cardIndex || auditCard === cardIndex
|
|
280
|
+
const designOk = !auditDesign || !curDesign || auditDesign === curDesign
|
|
281
|
+
out.auditFresh = cardOk && designOk
|
|
282
|
+
if (!out.auditFresh) {
|
|
283
|
+
out.auditNote =
|
|
284
|
+
`审计与当前设计稿对不上(审计 card=${audit?.card?.index ?? '?'} design=${audit?.design?.path || '?'};` +
|
|
285
|
+
`当前 card=${cardIndex} design=${designPath || '?'})⇒ 覆盖核对从略,仅保留引用落盘检查`
|
|
286
|
+
return out
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const declared = (audit.slices || []).filter((s) => s && s.exists !== false)
|
|
290
|
+
const names = []
|
|
291
|
+
for (const s of declared) {
|
|
292
|
+
const cand = [s.name, path.basename(s.file || ''), ...(s.crops || []).map((c) => c && c.name)].filter(Boolean)
|
|
293
|
+
names.push({ name: s.name || cand[0] || '', cand, rects: s.rects || [], verdict: s.verdict || '' })
|
|
294
|
+
}
|
|
295
|
+
const matched = (cand) => refs.some((ref) => cand.some((c) => c && sliceNameMatches(path.basename(ref), path.basename(c))))
|
|
296
|
+
out.declared = names.map((n) => n.name)
|
|
297
|
+
out.unreferenced = names
|
|
298
|
+
.filter((n) => !matched(n.cand))
|
|
299
|
+
.map((n) => ({ name: n.name, verdict: n.verdict, at: (n.rects[0] ? `${n.rects[0].x},${n.rects[0].y} ${n.rects[0].w}x${n.rects[0].h}` : '') }))
|
|
300
|
+
return out
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ── 主入口 ─────────────────────────────────────────────────────────── */
|
|
304
|
+
export function buildReadinessReport(input = {}) {
|
|
305
|
+
const args = {
|
|
306
|
+
cardId: '',
|
|
307
|
+
schemaPath: '',
|
|
308
|
+
dataPath: '',
|
|
309
|
+
specPath: '',
|
|
310
|
+
designPath: '',
|
|
311
|
+
auditPath: '',
|
|
312
|
+
cardIndex: 0,
|
|
313
|
+
slicesDir: DEFAULT_SLICES_DIR,
|
|
314
|
+
metaDir: DEFAULT_META_DIR,
|
|
315
|
+
/** 上游已算好的 fidelity(iterate 手里就有)——给了就不重复读盘算一遍 */
|
|
316
|
+
fidelity: null,
|
|
317
|
+
...input,
|
|
318
|
+
}
|
|
319
|
+
const cardId = args.cardId || (args.schemaPath ? path.basename(path.dirname(args.schemaPath)) : '')
|
|
320
|
+
const schemaPath = args.schemaPath || (cardId ? cardSchemaPath(cardId) : '')
|
|
321
|
+
if (!schemaPath || !fs.existsSync(schemaPath)) throw new Error(`schema.json 不存在: ${schemaPath || '<未指定>'}`)
|
|
322
|
+
const dataPath = args.dataPath || (cardId ? cardDataPath(cardId) : '')
|
|
323
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
|
|
324
|
+
let data = null
|
|
325
|
+
try { data = JSON.parse(fs.readFileSync(dataPath, 'utf8')) } catch { data = null }
|
|
326
|
+
|
|
327
|
+
// 卡号:优先显式 --card,其次从 card-N.png 推断,最后退出码里都按 1
|
|
328
|
+
let cardIndex = Number(args.cardIndex) || 0
|
|
329
|
+
if (!cardIndex) {
|
|
330
|
+
const m = /card-(\d+)\.png$/i.exec(String(args.designPath || ''))
|
|
331
|
+
cardIndex = m ? Number(m[1]) : 1
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const checks = {}
|
|
335
|
+
const blockers = []
|
|
336
|
+
|
|
337
|
+
/* F spec 契约(输入侧;只在你传了 --spec 时才检查)
|
|
338
|
+
契约:skills/shared/spec-contract.md —— `cards[<i>].mask` / `cards[<i>].rect` 是必需字段,
|
|
339
|
+
上游 parse_meaxure.py 无条件产出。缺失 = 该 spec 是旧版产物,下游会静默取默认圆角/默认底色,
|
|
340
|
+
表现为「还原度怎么调都对不上且查不出根因」。这里显式拦住,让人回去重跑上游。
|
|
341
|
+
|
|
342
|
+
⚠ `cards=[] **不是** 错误`:上游有三类合法空数组,都只记 note、不阻断 ——
|
|
343
|
+
① text_only / overlay_dialog:按 artboardType 收口,本就不单独产卡(由父卡承载);
|
|
344
|
+
② artboardType=card 却 0 卡:上游已知态,`board_tag` 明写「0 卡 · 结构丰富,按整图参考建卡」
|
|
345
|
+
(parse_meaxure.py:1768);test_text_only_artboard.py 用例 2/3/6 正是 kind=card、cards=0
|
|
346
|
+
且断言通过 —— 属于被测试固化的设计,不是「上游自相矛盾」。
|
|
347
|
+
这类画板无 cards 数据可组装(卡片被整体导成切片时检测器看不到结构),建卡改走整图 / AI 读图。
|
|
348
|
+
`subcard` **不在**这三类里:自 2026-09-24 起上游为其产 1 张卡(面板几何直取,
|
|
349
|
+
`detect_artboard_cards` sheet 分支)—— 走到下面的 subcard 分支即说明是旧版产物。 */
|
|
350
|
+
const specIssues = []
|
|
351
|
+
const specNotes = []
|
|
352
|
+
if (args.specPath && fs.existsSync(args.specPath)) {
|
|
353
|
+
try {
|
|
354
|
+
const spec = JSON.parse(fs.readFileSync(args.specPath, 'utf8'))
|
|
355
|
+
const specCards = Array.isArray(spec.cards) ? spec.cards : []
|
|
356
|
+
const target = specCards[cardIndex - 1]
|
|
357
|
+
if (!specCards.length) {
|
|
358
|
+
const at = spec.artboardType || '未知'
|
|
359
|
+
if (at === 'text_only') {
|
|
360
|
+
specNotes.push('cards=[] 属正常:artboardType=text_only(纯文本画板,上游按类型收口,下游不为其建卡)')
|
|
361
|
+
} else if (at === 'overlay_dialog') {
|
|
362
|
+
specNotes.push('cards=[] 属正常:artboardType=overlay_dialog(系统弹框,A.5 规定不产卡 —— 由父卡 alertDialog 调用承载)')
|
|
363
|
+
} else if (at === 'subcard') {
|
|
364
|
+
specNotes.push(
|
|
365
|
+
'cards=[] 但 artboardType=subcard:该 spec 应属**旧版产物**(2026-09-24 前)——' +
|
|
366
|
+
'上游现已为底部抽屉产 1 张卡(面板 rect + 顶圆角遮罩,含 card-1.png,由 detect_artboard_cards 的 sheet 分支几何直取)。' +
|
|
367
|
+
'请回上游重跑该画板,不要在下游读图自定卡片边界;临时兜底可读 artboard.png + elements[],但还原度无保证',
|
|
368
|
+
)
|
|
369
|
+
} else {
|
|
370
|
+
specNotes.push(
|
|
371
|
+
`cards=[] · artboardType=${at}:本画板无 cards 可组装(卡片被整体导成切片时检测器看不到结构)。` +
|
|
372
|
+
'上游会兜底裁一张 card-1.png(去系统 chrome,parse_meaxure.py:645)可作卡片视觉参考,' +
|
|
373
|
+
'但**没有卡片坐标与切图归属**,建卡须以该图 + elements[] 为准(或 AI 读图);' +
|
|
374
|
+
'若确认板上确有卡片却未检出,回上游复核该画板(别在下游补)',
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
} else if (!target) {
|
|
378
|
+
specIssues.push(`cards[${cardIndex - 1}] 不存在(该 spec 只有 ${specCards.length} 张卡,--card 序号越界)`)
|
|
379
|
+
} else {
|
|
380
|
+
if (!target.rect) specIssues.push(`cards[${cardIndex - 1}].rect 缺失(卡片几何是必需字段)`)
|
|
381
|
+
if (!target.mask || typeof target.mask !== 'object') {
|
|
382
|
+
specIssues.push(`cards[${cardIndex - 1}].mask 缺失(旧版 spec 产物:mask.radius 是下游圆角的唯一来源,缺了只能取默认值)`)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} catch (e) {
|
|
386
|
+
specIssues.push(`spec.json 解析失败:${e?.message || e}`)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
checks.spec = args.specPath ? { passed: specIssues.length === 0, issues: specIssues, notes: specNotes } : null
|
|
390
|
+
if (specIssues.length) {
|
|
391
|
+
blockers.push(
|
|
392
|
+
`spec|上游 spec.json 契约不完整(${specIssues.length} 项):${specIssues.slice(0, 5).join(';')}` +
|
|
393
|
+
'(重跑上游:python skills/sketch-spec-to-card/scripts/parse_meaxure.py --src <导出目录> --draft <稿名> --build;' +
|
|
394
|
+
'不要手工补字段、不要在下游加兼容分支)',
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* A 组件契约 */
|
|
399
|
+
const contract = loadContract({ metaDir: args.metaDir })
|
|
400
|
+
const violations = collectContractViolations(schema, contract)
|
|
401
|
+
const compErrors = violations.filter((v) => v.severity === 'error')
|
|
402
|
+
checks.components = {
|
|
403
|
+
passed: compErrors.length === 0,
|
|
404
|
+
errors: compErrors.map(describeViolation),
|
|
405
|
+
warnings: violations.filter((v) => v.severity !== 'error').map(describeViolation),
|
|
406
|
+
}
|
|
407
|
+
if (compErrors.length) blockers.push(`components|${compErrors.length} 处组件契约违规(属性键/样式组不在元数据白名单内,运行时整条静默丢弃)`)
|
|
408
|
+
|
|
409
|
+
/* B 要素齐全 */
|
|
410
|
+
let fid = args.fidelity
|
|
411
|
+
if (!fid && args.specPath && fs.existsSync(args.specPath)) {
|
|
412
|
+
try {
|
|
413
|
+
const r = buildFidelityReport({ spec: args.specPath, schema: schemaPath, card: cardIndex, audit: args.auditPath || '' })
|
|
414
|
+
fid = { total: r.elements.texts.total, matched: r.elements.texts.matched, missing: r.elements.texts.missing, invented: r.elements.texts.invented }
|
|
415
|
+
} catch (e) {
|
|
416
|
+
fid = { error: String(e?.message || e) }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (fid && !fid.error) {
|
|
420
|
+
checks.elements = { passed: fid.missing.length === 0, total: fid.total, matched: fid.matched, missing: fid.missing, invented: fid.invented }
|
|
421
|
+
if (fid.missing.length) {
|
|
422
|
+
blockers.push(
|
|
423
|
+
`elements|设计稿有 ${fid.missing.length} 条文案在 schema 里找不到:${fid.missing.slice(0, 5).map((t) => `「${t}」`).join('、')}` +
|
|
424
|
+
'(补回对应节点,或把绑定变量的 defaultValue 改成设计稿真值;别用占位文案顶替)',
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
checks.elements = fid && fid.error ? { passed: null, error: fid.error } : null
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* C 变量绑定完备 */
|
|
432
|
+
const known = buildKnownRoots(schema, data)
|
|
433
|
+
const b = collectBindingRefs(schema, contract.repeaterClasses)
|
|
434
|
+
const unresolved = []
|
|
435
|
+
const seenRoot = new Set()
|
|
436
|
+
for (const r of b.outside) {
|
|
437
|
+
if (known.has(r.root)) continue
|
|
438
|
+
if (seenRoot.has(r.root)) continue
|
|
439
|
+
seenRoot.add(r.root)
|
|
440
|
+
unresolved.push(r)
|
|
441
|
+
}
|
|
442
|
+
const fromProblems = b.fromViolations
|
|
443
|
+
// 元数据没读到(contract 为空)⇒ 复现器集合无从得知 ⇒ 槽位内引用会被当成槽位外硬判,
|
|
444
|
+
// 必然大面积误报。这种环境下**整项退化为「无从判定」**,不放行也不拦。
|
|
445
|
+
const bindingsCheckable = contract.loaded.components
|
|
446
|
+
checks.bindings = {
|
|
447
|
+
passed: bindingsCheckable ? unresolved.length === 0 && fromProblems.length === 0 : null,
|
|
448
|
+
checkable: bindingsCheckable,
|
|
449
|
+
rootsKnown: known.size,
|
|
450
|
+
repeaters: [...contract.repeaterClasses].sort(),
|
|
451
|
+
refsOutsideSlots: b.outside.length,
|
|
452
|
+
refsInsideSlots: b.slotCount,
|
|
453
|
+
unresolvedRoots: bindingsCheckable ? unresolved : [],
|
|
454
|
+
fromProblems,
|
|
455
|
+
slotRootsUnresolved: [...b.slotRoots].filter((r) => !known.has(r)).length,
|
|
456
|
+
}
|
|
457
|
+
if (bindingsCheckable && unresolved.length) {
|
|
458
|
+
blockers.push(
|
|
459
|
+
`bindings|${unresolved.length} 个根变量在 dictionary / mocks / data.json / 事件 setData 里都找不到出处:` +
|
|
460
|
+
unresolved.slice(0, 5).map((r) => `$${r.root}(${r.cls} ${r.cid} ${r.where}.${r.key})`).join('、'),
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
if (bindingsCheckable && fromProblems.length) {
|
|
464
|
+
blockers.push(`bindings|${fromProblems.length} 处 itemValueFrom 与值不匹配(from=1 必须是 $表达式 / from=2 不能是 $表达式)`)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* C2 数组消费对账(列表数组化闭环,2026-09-25)——「数据绑定齐全」的机器化。
|
|
468
|
+
* 证据链(全仓 75 卡实测):transferPlan.payeeList / dreamPlan.planTimeList /
|
|
469
|
+
* creditCardPayment.myCardList / branchdata.businessScopeList /
|
|
470
|
+
* financialProductDetail.tradeRules 都是「列表数据建了、视图零消费」——
|
|
471
|
+
* 列表被静态展开或根本没建,此前无任何门禁能发现。
|
|
472
|
+
* 判据:名字带列表语义(list|items|rows|records|details|rules|entries)且未被
|
|
473
|
+
* 复现器 value / slotScope 模板 / 事件代码 / 任意表达式引用 ⇒ 硬伤拦像素循环;
|
|
474
|
+
* 其余未消费数组(radarData/donutData 类图表数据)只进 checks 不拦。 */
|
|
475
|
+
const arrAudit = collectArrayConsumption(schema, data || {})
|
|
476
|
+
const hardUnconsumed = arrAudit.unconsumed.filter((u) => u.semantic)
|
|
477
|
+
const softUnconsumed = arrAudit.unconsumed.filter((u) => !u.semantic)
|
|
478
|
+
checks.bindings.arraysUnconsumed = {
|
|
479
|
+
hard: hardUnconsumed,
|
|
480
|
+
soft: softUnconsumed,
|
|
481
|
+
arrays: [...arrAudit.arrays.keys()],
|
|
482
|
+
scopedLists: arrAudit.scopedListCount,
|
|
483
|
+
repeaters: arrAudit.repeaterCount,
|
|
484
|
+
}
|
|
485
|
+
if (hardUnconsumed.length) {
|
|
486
|
+
blockers.push(
|
|
487
|
+
`bindings|${hardUnconsumed.length} 个列表语义数组没有任何列表消费:` +
|
|
488
|
+
hardUnconsumed.slice(0, 5).map((u) => `$${u.name}(${u.len} 项)`).join('、') +
|
|
489
|
+
'(设计稿若有重复数据行,必须 ListLayout/GridLayout + value 绑数组 + slotScope 模板,禁止静态展开;范式 ir-format.md §2f)',
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* G 列表提名闭环(--audit 时):S2 审计的 listCandidates 是否被数组化实现。
|
|
494
|
+
* 提名 0 数组化 = blocker(视觉分析已明确是列表,静态展开就是疏漏);
|
|
495
|
+
* 提名 > 数组化 = 只记 note(AI 合并同构列表 / 提名假阳性都合法,不冤枉)。 */
|
|
496
|
+
const auditResolved = args.auditPath || (cardId ? cardAuditPath(cardId) : '')
|
|
497
|
+
if (auditResolved && fs.existsSync(auditResolved)) {
|
|
498
|
+
let candidates = []
|
|
499
|
+
try {
|
|
500
|
+
const audit = JSON.parse(fs.readFileSync(auditResolved, 'utf8'))
|
|
501
|
+
candidates = Array.isArray(audit.listCandidates) ? audit.listCandidates : []
|
|
502
|
+
} catch { /* 审计坏了不阻断本门 */ }
|
|
503
|
+
if (candidates.length) {
|
|
504
|
+
checks.lists = {
|
|
505
|
+
passed: arrAudit.scopedListCount > 0,
|
|
506
|
+
nominated: candidates.length,
|
|
507
|
+
scopedLists: arrAudit.scopedListCount,
|
|
508
|
+
hardUnconsumed: hardUnconsumed.map((u) => u.name),
|
|
509
|
+
}
|
|
510
|
+
if (arrAudit.scopedListCount === 0) {
|
|
511
|
+
blockers.push(
|
|
512
|
+
`lists|S2 审计提名了 ${candidates.length} 个列表候选(同构重复行 / tile,值逐行不同),` +
|
|
513
|
+
'schema 里却没有任何数组化列表(ListLayout/GridLayout 的 value 绑定数组):' +
|
|
514
|
+
'列表数据必须在视觉分析阶段就明确用数组绑定,禁止逐行静态展开(ir-format.md §2f;audit.listCandidates 的 fields/values 就是现成的字段与值矩阵)',
|
|
515
|
+
)
|
|
516
|
+
} else if (arrAudit.scopedListCount < candidates.length) {
|
|
517
|
+
checks.lists.note = `数组化列表 ${arrAudit.scopedListCount} < 提名 ${candidates.length}:若为同构列表合并或提名假阳性可忽略,否则逐条核对 listCandidates`
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* D 图片地址已配置 */
|
|
523
|
+
checks.images = collectImageConfig(schema)
|
|
524
|
+
checks.images.passed = checks.images.problems.length === 0
|
|
525
|
+
if (checks.images.problems.length) {
|
|
526
|
+
blockers.push(
|
|
527
|
+
`images|${checks.images.problems.length} 个 Image 节点没有配置图片地址:` +
|
|
528
|
+
checks.images.problems.slice(0, 5).map((p) => `${p.cid}(${p.detail})`).join('、'),
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* E 切图路径已配置 */
|
|
533
|
+
const slices = checkSlices({
|
|
534
|
+
schema,
|
|
535
|
+
specPath: args.specPath,
|
|
536
|
+
cardIndex,
|
|
537
|
+
designPath: args.designPath,
|
|
538
|
+
slicesDir: args.slicesDir,
|
|
539
|
+
auditPath: args.auditPath || (cardId ? cardAuditPath(cardId) : ''),
|
|
540
|
+
})
|
|
541
|
+
checks.slices = slices
|
|
542
|
+
if (slices.missingOnDisk.length) {
|
|
543
|
+
blockers.push(
|
|
544
|
+
`slices|${slices.missingOnDisk.length} 个切图引用在 src/slices 里没有对应文件(渲染 404、图空白、零报错):` +
|
|
545
|
+
slices.missingOnDisk.slice(0, 5).map((m) => `${m.ref}`).join('、') +
|
|
546
|
+
'(按引用名把切图落盘,或把引用改成磁盘上真实存在的名字 —— 两者必须同源)',
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const ORDER = ['spec', 'components', 'elements', 'bindings', 'lists', 'images', 'slices']
|
|
551
|
+
const firstFail = ORDER.find((k) => checks[k] && checks[k].passed === false)
|
|
552
|
+
const ready = blockers.length === 0
|
|
553
|
+
const HINT = {
|
|
554
|
+
spec: '重跑上游 parse_meaxure.py --src <导出目录> --draft <稿名> --build 补全 spec.json 字段(旧版产物缺 mask/rect)——不要手工补,也不要让下游兼容缺失字段。',
|
|
555
|
+
components: '按 blockers 里的节点路径去 IR / schema 改属性键(Image 的图片地址键是 value,不是 image)。改完重跑 readiness。',
|
|
556
|
+
elements: '先补要素再谈像素:把设计稿的文案补回对应节点,或改绑定变量的 defaultValue。',
|
|
557
|
+
bindings: '把缺失的根变量加进 dictionary.inputParams(带 defaultValue),或改写成已存在的变量名。列表语义数组未消费时:按 audit.listCandidates 的 fields/values 建数组变量 + ListLayout/GridLayout value 绑数组 + slotScope 模板(ir-format.md §2f、recipe_snippets/list-expand-collapse.json)。',
|
|
558
|
+
lists: '按 audit.listCandidates 逐条数组化:数组变量进 dictionary.inputParams(字段名照 values 列语义化命名,禁 text1/val2),ListLayout/GridLayout 的 value 用 itemValueFrom:"1" 绑 $数组,一行的完整结构写进 slotScope 插槽($<字段名> / $$ / $_index)。范式 ir-format.md §2f + recipe_snippets/list-expand-collapse.json。',
|
|
559
|
+
images: '给每个 Image 节点补 itemKey="value" 且值非空;静态路径用 itemValueFrom:"2",变量绑定用 "1" + $表达式。',
|
|
560
|
+
slices: '跑 crop_slices.mjs --from-audit .cache/ai-scratch/<id>.audit.json 把缺失切图落进 src/slices,或修正引用名。',
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
cardId,
|
|
564
|
+
ready,
|
|
565
|
+
stage: ready ? 'ready' : firstFail,
|
|
566
|
+
blockers,
|
|
567
|
+
hint: ready ? null : HINT[firstFail] || null,
|
|
568
|
+
checks,
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* ── CLI ───────────────────────────────────────────────────────────── */
|
|
573
|
+
function parseArgs(argv) {
|
|
574
|
+
const out = { id: '', schema: '', spec: '', design: '', audit: '', card: 0, slicesDir: '', json: false }
|
|
575
|
+
for (let i = 0; i < argv.length; i++) {
|
|
576
|
+
const a = argv[i]
|
|
577
|
+
if (a === '--id') out.id = argv[++i] || ''
|
|
578
|
+
else if (a === '--schema') out.schema = argv[++i] || ''
|
|
579
|
+
else if (a === '--spec') out.spec = argv[++i] || ''
|
|
580
|
+
else if (a === '--design') out.design = argv[++i] || ''
|
|
581
|
+
else if (a === '--audit') out.audit = argv[++i] || ''
|
|
582
|
+
else if (a === '--card') out.card = Number(argv[++i]) || 0
|
|
583
|
+
else if (a === '--slices-dir') out.slicesDir = argv[++i] || ''
|
|
584
|
+
else if (a === '--json') out.json = true
|
|
585
|
+
else if (a === '-h' || a === '--help') {
|
|
586
|
+
console.log('Usage: node readiness.mjs --id <cardId> [--spec <spec.json>] [--design <card-N.png>] [--audit <audit.json>] [--card N] [--json]')
|
|
587
|
+
console.log('退出码:0 = 就绪;3 = 未就绪(内容没准备好,不是脚本报错);1 = 用法/IO 错误')
|
|
588
|
+
process.exit(0)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (!out.id && !out.schema) {
|
|
592
|
+
console.error('缺少 --id <cardId> 或 --schema <schema.json>')
|
|
593
|
+
process.exit(1)
|
|
594
|
+
}
|
|
595
|
+
return out
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function main() {
|
|
599
|
+
const a = parseArgs(process.argv.slice(2))
|
|
600
|
+
let report
|
|
601
|
+
try {
|
|
602
|
+
report = buildReadinessReport({
|
|
603
|
+
cardId: a.id,
|
|
604
|
+
schemaPath: a.schema,
|
|
605
|
+
specPath: a.spec,
|
|
606
|
+
designPath: a.design,
|
|
607
|
+
auditPath: a.audit,
|
|
608
|
+
cardIndex: a.card,
|
|
609
|
+
slicesDir: a.slicesDir || DEFAULT_SLICES_DIR,
|
|
610
|
+
})
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.error(String(e?.message || e))
|
|
613
|
+
process.exit(1)
|
|
614
|
+
}
|
|
615
|
+
if (a.json) {
|
|
616
|
+
console.log(JSON.stringify(report, null, 2))
|
|
617
|
+
} else {
|
|
618
|
+
if (report.ready) {
|
|
619
|
+
console.log(`[readiness] ✅ ${report.cardId} 内容就绪:要素齐全 / 变量绑定完备 / 图片与切图路径已配置 —— 可以进像素循环。`)
|
|
620
|
+
} else {
|
|
621
|
+
console.log(`[readiness] ❌ ${report.cardId} 未就绪(stage=${report.stage}):${report.blockers.length} 条阻塞项`)
|
|
622
|
+
for (const x of report.blockers) console.log(` ❌ ${x}`)
|
|
623
|
+
if (report.hint) console.log(` → ${report.hint}`)
|
|
624
|
+
}
|
|
625
|
+
for (const x of report.checks?.spec?.notes || []) console.log(` ℹ ${x}`)
|
|
626
|
+
}
|
|
627
|
+
// 3 = 内容未就绪(调用方据此跳过 render + pixelDiff 并拒绝消耗迭代轮次);与 1(脚本报错)/2(gate-fail)区分
|
|
628
|
+
process.exit(report.ready ? 0 : 3)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const _thisScript = process.argv[1] ? path.resolve(process.argv[1]) : ''
|
|
632
|
+
if (_thisScript && import.meta.url === pathToFileURL(_thisScript).href) {
|
|
633
|
+
main()
|
|
634
|
+
}
|
|
@@ -26,6 +26,7 @@ import path from 'node:path'
|
|
|
26
26
|
import { execFileSync } from 'node:child_process'
|
|
27
27
|
import { fileURLToPath } from 'node:url'
|
|
28
28
|
import { cardsDir, skillRoot } from './lib/paths.mjs'
|
|
29
|
+
import { probePython, resolvePython as pickPython } from './lib/python.mjs'
|
|
29
30
|
import { extractCardFeatures, loadRegistry } from './build_card_templates.mjs'
|
|
30
31
|
|
|
31
32
|
const OUT = path.join(skillRoot, 'references', 'card_templates.json')
|
|
@@ -46,15 +47,11 @@ function parseArgs(argv) {
|
|
|
46
47
|
return out
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** Python 解释器;解析集中在 lib/python.mjs。**保留 null 语义**:healthGate 靠它区分
|
|
51
|
+
* 「没装 Python」(给 `python-not-found` 原因),而不是带着假解释器去试跑。 */
|
|
49
52
|
function resolvePython() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
execFileSync(cmd, ['--version'], { stdio: 'ignore', timeout: 5000 })
|
|
54
|
-
return cmd
|
|
55
|
-
} catch { /* try next */ }
|
|
56
|
-
}
|
|
57
|
-
return null
|
|
53
|
+
const bin = pickPython()
|
|
54
|
+
return probePython(bin) ? bin : null
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
/** 健康门禁:validate 0 error + lint 0 error。返回 { ok, reasons[] } */
|