cardbuddy 0.25.0 → 0.26.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.
@@ -8,8 +8,8 @@
8
8
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16-Cg6Nrosk.png" />
9
9
  <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32-B_1744sp.png" />
10
10
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-BQcES9QK.png" />
11
- <script type="module" crossorigin src="/assets/index-DL4ZCJCA.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-MWKPP9nd.css">
11
+ <script type="module" crossorigin src="/assets/index-Bn5gP68-.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-CeoWrec1.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="app"></div>
@@ -238,17 +238,19 @@ function convertDirectives(component, template, warnings) {
238
238
  const extend = bucket.extend && typeof bucket.extend === 'object' ? bucket.extend : {}
239
239
  bucket.extend = extend
240
240
 
241
+ // 可见时写空串而非 'block':display 由样式类决定(.ce_row/.ce_column 为 flex),
242
+ // 内联 block 会盖掉 flex,导致 justify/align 失效(同 h5.js applyDirectives 注释)。
241
243
  const expression = conditionMap.expression
242
244
  if (hasText(expression)) {
243
- if (showType === '01') extend.display = `(${expression}) ? 'block' : 'none'`
244
- else if (showType === '02') extend.display = `(${expression}) ? 'none' : 'block'`
245
+ if (showType === '01') extend.display = `(${expression}) ? '' : 'none'`
246
+ else if (showType === '02') extend.display = `(${expression}) ? 'none' : ''`
245
247
  return
246
248
  }
247
249
 
248
250
  if (Array.isArray(conditionMap.group) && conditionMap.group.length > 0) {
249
251
  const expr = convertCondition(conditionMap.group)
250
- if (showType === '01') extend.display = `${expr} ? 'block' : 'none'`
251
- else if (showType === '02') extend.display = `${expr} ? 'none' : 'block'`
252
+ if (showType === '01') extend.display = `${expr} ? '' : 'none'`
253
+ else if (showType === '02') extend.display = `${expr} ? 'none' : ''`
252
254
  return
253
255
  }
254
256
 
@@ -116,6 +116,51 @@ export function collectProps(properties = []) {
116
116
 
117
117
  const IDENT_RE = /^[A-Za-z0-9_$]+$/
118
118
 
119
+ /* ------------------------ fastjson / HashMap 键序 ------------------------ */
120
+
121
+ /** Java String.hashCode */
122
+ function javaStringHash(text) {
123
+ let h = 0
124
+ for (let i = 0; i < text.length; i++) h = (Math.imul(31, h) + text.charCodeAt(i)) | 0
125
+ return h
126
+ }
127
+
128
+ /** HashMap 的扰动函数:h ^ (h >>> 16) */
129
+ function hashSpread(h) {
130
+ return (h ^ (h >>> 16)) >>> 0
131
+ }
132
+
133
+ /** 按元素个数推算 HashMap 表容量(默认 16,负载因子 0.75,翻倍扩容) */
134
+ function hashCapacity(size) {
135
+ let cap = 16
136
+ while (size > cap * 0.75) cap *= 2
137
+ return cap
138
+ }
139
+
140
+ /**
141
+ * 复现 Java HashMap 的迭代顺序
142
+ *
143
+ * 平台产物里的 JSON 由 fastjson 的 `JSONObject`(底层 HashMap)序列化,键序是**哈希桶顺序**
144
+ * 而不是插入顺序。产物只用于「替换线上文件」时,键序语义上无所谓;但只要希望与平台产物
145
+ * 逐字节一致(便于 diff 对拍),就必须复现这个顺序。
146
+ *
147
+ * 规则:cap = 16 起按 0.75 负载因子扩容;桶下标 = (h ^ h>>>16) & (cap-1);
148
+ * 按下标升序输出,同桶内保持插入顺序。
149
+ */
150
+ export function javaHashMapOrder(value) {
151
+ if (Array.isArray(value)) return value.map(javaHashMapOrder)
152
+ if (!value || typeof value !== 'object') return value
153
+ const entries = Object.entries(value)
154
+ const cap = hashCapacity(entries.length)
155
+ return entries
156
+ .map(([k, v], i) => ({ k, v: javaHashMapOrder(v), i, bucket: hashSpread(javaStringHash(k)) & (cap - 1) }))
157
+ .sort((a, b) => a.bucket - b.bucket || a.i - b.i)
158
+ .reduce((acc, x) => {
159
+ acc[x.k] = x.v
160
+ return acc
161
+ }, {})
162
+ }
163
+
119
164
  /**
120
165
  * JS 对象字面量的键名写法(平台口径)
121
166
  *
@@ -143,7 +188,7 @@ export function convertPropsH5(properties = []) {
143
188
  parts.push(`${key}:${convertPropsH5(prop.children)}`)
144
189
  } else if (prop.list && prop.list.length > 0) {
145
190
  const groups = prop.list.map((g) => convertPropsH5(g))
146
- parts.push(`${key}:[${groups.join(',')}]`)
191
+ parts.push(`${key}: [${groups.join(',')}]`)
147
192
  } else {
148
193
  parts.push(`${key}:${JSON.stringify(prop.itemValue)}`)
149
194
  }
@@ -161,7 +206,7 @@ export function convertPropsH5(properties = []) {
161
206
  /**
162
207
  * `$foo` → `s.$foo`
163
208
  *
164
- * 平台卡片状态挂在上下文实例的 `state` 上(生成代码里 `var s = c.state`),
209
+ * 平台卡片状态挂在上下文实例的 `s` 上(生成代码里 `var s = c.s`,ExpressionState),
165
210
  * 所以表达式里的 `$x` 必须改写为 `s.$x`。
166
211
  * 字符串字面量('…' / "…" / `…`)内部的 `$` 不得改写。
167
212
  *
@@ -14,7 +14,8 @@
14
14
  * 元素组件通过 `this.getElement('Text')` 取宿主 elements 注册表。
15
15
  *
16
16
  * 关键契约(照抄平台侧,改动会直接导致真机渲染空白):
17
- * - 数据挂在 `s = c.state`,表达式里的 `$foo` 已改写为 `s.$foo`;
17
+ * - 数据挂在 `s = c.s`(ExpressionState,提供 $key / $$),表达式里的 `$foo` 已改写为 `s.$foo`;
18
+ * 不能用 `c.state` —— 那是 MethodState,只有平铺键名,`s.$foo` 会一律求值为 undefined;
18
19
  * - `data` 里必须同时给 defaultState / state / n(n 是刷新因子,模板里靠它触发重算);
19
20
  * - watch.data 重新合并 state,宿主下发新数据时才能刷新;
20
21
  * - 每个事件的实现体被搬到 methods 里,on 只留转发(避免闭包捕获旧 state)。
@@ -27,6 +28,7 @@ import {
27
28
  convertPropsClient,
28
29
  convertPropsH5,
29
30
  hasText,
31
+ javaHashMapOrder,
30
32
  jsKey,
31
33
  pickVarProps,
32
34
  } from './common.js'
@@ -89,8 +91,12 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
89
91
  for (const child of card.components) slot.components.push(convertComponent(child))
90
92
 
91
93
  function convertComponent(component) {
94
+ const desc = elementOf(component.className)
92
95
  const layer = {
93
- id: component.componentId,
96
+ // 列表 / 网格在平台侧会先被客户端编译改写成 `<cardCode>_sub_<componentId>`(共享同一份
97
+ // CardModel,H5 编译在其后,于是把这个改写后的 id 带进了 ref)。此处等价复现:
98
+ // 只影响容器自身的 ref,循环项内部的组件 id 保持原样。
99
+ id: desc.isNewTemplate ? `${card.code}_sub_${component.componentId}` : component.componentId,
94
100
  name: component.className,
95
101
  style: {},
96
102
  props: convertPropsH5(component.properties),
@@ -148,8 +154,12 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
148
154
  return
149
155
  }
150
156
 
151
- if (showType === '01') layer.style.display = `(${expression}) ? 'block' : 'none'`
152
- else if (showType === '02') layer.style.display = `(${expression}) ? 'none' : 'block'`
157
+ // 可见时不能写死 `block`:布局元素的 display 由样式类决定(`.ce_row`/`.ce_column`
158
+ // 是 flex),内联 `display:block` 会盖掉 flex → justifyContent/alignItems 全失效。
159
+ // accountCreditedlist 的「更多转账伙伴」行就是被它压成 block,文字靠左 + 图标换行被裁掉。
160
+ // 空串等价于 v-show 的「显隐只切 none」——清掉内联值,恢复原生 display。
161
+ if (showType === '01') layer.style.display = `(${expression}) ? '' : 'none'`
162
+ else if (showType === '02') layer.style.display = `(${expression}) ? 'none' : ''`
153
163
  }
154
164
 
155
165
  /**
@@ -190,9 +200,16 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
190
200
  const fn = constProps.function
191
201
  const code = fn && typeof fn === 'object' ? fn.code : null
192
202
  const compiled = fn && typeof fn === 'object' ? fn.compiled : null
193
- // 平台侧优先用 compiled(ES5 转译版),缺失才回退手写 code
194
- if (hasText(compiled)) return compiled
203
+ // 取 code 优先,compiled 只作兜底 —— 与编辑器预览(RenderTree.actionCode)和
204
+ // 运行时(preview 的 actionCode,`code || compiled`)保持同一优先级。
205
+ //
206
+ // 反过来(compiled 优先)会让「编辑器里改好、导出产物还是旧的」:两者是双份数据,
207
+ // compiled 是平台侧某次转译留下的快照,编辑器改 code 时它不会跟着变(SetCode 只覆写 code;
208
+ // ai-chat 的自动同步也只在「原本就同步」时生效)。实例:account 的 onMeasured 标题脚本,
209
+ // code 已是左对齐 `600 17px` / 浅绿 #D8F5E8 / x=0,compiled 仍停在居中的 `bold 16px` /
210
+ // #B9D0FA / x=canvas.width/2,产物跑旧版,标题位置与配色全错。
195
211
  if (hasText(code)) return code
212
+ if (hasText(compiled)) return compiled
196
213
  warnings.push(`事件 ${eventAction.eventKey} 的 customAction 缺少 code,已跳过`)
197
214
  return ''
198
215
  }
@@ -241,9 +258,9 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
241
258
  const code = scopeSlots
242
259
  .map(
243
260
  (s) =>
244
- `${jsKey(s.slotKey)}:function(_sd_){return (function(c){var s = c.state; return [${s.components
261
+ `${jsKey(s.slotKey)}:function(_sd_){return (function(c){var s = c.s; return [${s.components
245
262
  .map(serializeComponent)
246
- .join(',')}]})(new CC(_vm,_sd_,c))}`
263
+ .join(',')}]})(new CC(_vm,_sd_,c)) }`
247
264
  )
248
265
  .join(',')
249
266
  options.push(`scopedSlots:{${code}}`)
@@ -271,7 +288,9 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
271
288
  "var CC = this.getObject('context');" +
272
289
  'var e = this.getElement;' +
273
290
  'var c = new CC(this,this);' +
274
- 'var s = c.state;' +
291
+ // 必须是 c.s(ExpressionState,提供 $key / $$ 读取),不能用 c.state(MethodState,只有平铺键名)。
292
+ // 平台 Context 里两者并存:s 走表达式读、state 走方法写。写成 c.state 会让所有 s.$xxx 求值为 undefined。
293
+ 'var s = c.s;' +
275
294
  'var n = this.n;' +
276
295
  `return ${components.map(serializeComponent).join('')}` +
277
296
  '}'
@@ -286,7 +305,8 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
286
305
  parts.push('props:{data:{type:Object,default:function(){ return {} }}}')
287
306
 
288
307
  const dataCode = [
289
- `defaultState:${JSON.stringify(defaultState)}`,
308
+ // 键序按 Java HashMap 桶序输出:与平台产物逐字节一致(语义等价,仅便于 diff 对拍)
309
+ `defaultState:${JSON.stringify(javaHashMapOrder(defaultState))}`,
290
310
  'state:{}',
291
311
  'n:0',
292
312
  ].join(',')
@@ -25,6 +25,7 @@ dsl-card 项目的 AI 技能层。它是**编排层**:把卡片编辑任务拆
25
25
  6. **改坏可回滚**:`card_write` 每次写卡前自动把当前版本存为历史快照(与编辑器改卡同语义);编辑失误时用 `card_rollback`(默认回滚到最新快照)撤销,用 `card_history` 挑选更早的回滚目标。
26
26
  7. **dev server 前置**:`card_render_status` / `card_node_models` / `card_import_meta` 必须 dev server 已启动,未起时返回 `dev-server-required`;`card_sync` / `card_save` / `card_slice_upload` 除 dev server 外还需已登录远端。(本仓库 `npm run dev` 默认 5180,由 Vite 把 `/__*` 代理到 8787;`cardbuddy init` 出来的工程 `npm run dev` 直接是 8787。)`card_list` / `card_read` / `card_write` / `card_validate` / `card_history` / `card_rollback` 会自动降级为直接读盘/写盘/跑 Python(render_status 标 skipped);`card_screenshot` 降级到包内独立离线渲染器(`source:'standalone'`)。
27
27
  8. **建卡先判输入形态**(Workflow D):收到设计稿输入先调 `card_route_input({path})`,按返回的 `kind` 分流,**不要凭感觉决定要不要跑上游**。判为 `meaxure-export`(Sketch MeaXure 标注导出)时必须先经 `sketch-spec-to-card` 前处理产出 `UI/<稿名>/` 要素包,**禁止直接读 `preview/` 整图建卡**;`preprocess.needed=false` 表示已前处理过,直接复用存量产物。`card_route_input` 纯离线,不需要 dev server。
28
+ 9. **编辑前先适配**(Workflow A/B 第 0 步):`node skills/dsl-card-generator/scripts/adapt_schema.mjs --id <id>`(可加 `--write`)。存量卡、刚同步进来的卡、复制过子树的卡必须先把旧形态清掉再改——未声明变量、越界 `$_index`、style 里写死的 display、空条件 v-show 会让 `bindingErrors` 混进与本次任务无关的历史噪音。规则与事故来源见 `dsl-card-generator/references/schema_adaptation.md`。
28
29
 
29
30
  ## 前置:`card_*` 工具从哪来(agent 里看不到工具先看这里)
30
31
 
@@ -141,6 +142,10 @@ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
141
142
 
142
143
  目标:把一张卡的 `bindingErrors` 清零。
143
144
 
145
+ 0. **适配(先做)**:`node skills/dsl-card-generator/scripts/adapt_schema.mjs --id <id> [--write]`。
146
+ 确定性可修项(style 里写死的 display、空条件 v-show)直接 `--write` 落盘;未声明变量(AD-01)、
147
+ 越界 `$_index`(AD-02)按报告人工判定后再进下一步。不做这步会把历史遗留错误当成本次要修的
148
+ 绑定问题,白跑几轮。
144
149
  1. `card_render_status(id)` → 读 `bindingErrors`(每项有 `path` / `expr` / `message`)。
145
150
  2. **区分「真坏绑定」与「级联误报」**(实测关键,见下):
146
151
  - **真坏绑定** = 表达式**自身语法错误**,message 形如 `Unexpected token '}'` / `Unexpected end of input`。这类是编辑手滑(括号/引号没闭合),必须修。
@@ -159,10 +164,11 @@ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
159
164
 
160
165
  1. `card_list` → 选目标卡集合(id 白名单 / `className` 特征 / 全量)。
161
166
  2. 对每张卡:
162
- a. `card_node_models(id)` 定位要改的字段,确认跨卡字段形状一致(同一 `className` 的字段通常同构)。
163
- b. `card_read(id)` 拿完整 schema + data。
164
- c. 改 schema(**整体替换**,保留未涉及节点/字段与孤儿字段)。
165
- d. `card_write(id, { schema })` 收口校验。
167
+ a. **适配**(同 Workflow A 第 0 步):`adapt_schema.mjs --id <id> [--write]`,先把历史遗留形态清掉再进编辑。
168
+ b. `card_node_models(id)` 定位要改的字段,确认跨卡字段形状一致(同一 `className` 的字段通常同构)。
169
+ c. `card_read(id)` 拿完整 schema + data。
170
+ d. 改 schema(**整体替换**,保留未涉及节点/字段与孤儿字段)。
171
+ e. `card_write(id, { schema })` 收口校验。
166
172
  3. 汇总:哪些卡改了、哪些卡校验失败(失败的不写或回滚,报告用户)。
167
173
  4. **纪律**:批量改是整卡为单元,不要逐字段 patch 漏掉兄弟字段;跨卡编辑前先 `card_node_models` 验证形状一致,避免把 A 卡的字段名错用到 B 卡。
168
174
 
@@ -29,6 +29,7 @@ description: 生成、编辑远端/YNet 低代码卡片 schema.json + data.json
29
29
  14. **数字语义变量提取** — 含运行期数字(金额/笔数/利率/期限/日期)抽 inputParams + 表达式拼接;禁整串写死/绑 string/正则见数字就拆(R61,rules.md §十四)
30
30
  15. **图表绘制层序(R62)** — 底→顶:参考线/网格/轨道最底 → 数据标记 → 文字标签最顶;禁先填充后画网格(rules.md §十五)。机械兜底在 v3 生成器 `chartPaintLayers`(`lib/v3_shared.mjs`)+ 回归测试 `tools/tests/chart-layer-order.test.mjs`;**lint 不拦这一条**(无法可靠解析 Canvas 表达式的调用顺序),AI 手写时靠渲染后目视。
31
31
  16. **宽度自适应(占满禁写死 px,R66)** — 顶层 `view.width` = 卡面宽(非画板宽);纵向流父级里占满父内容盒的容器**禁写 px 宽**,用 `100%` 或不写(meta 默认即 100%),px 只给真固定元素(ir-format.md §2f,lint `r66-fullwidth-px`)。写死 px 的卡在非设计宽度的宿主容器里锁死不自适应。
32
+ 17. **编辑前先适配(AD-*)** — 存量卡 / 刚同步进来的卡 / 从别卡复制过子树的卡,动手前先跑 `adapt_schema.mjs` 归一到当前契约:未声明变量、越界 `$_index`、style 里写死的 display、空条件 v-show 一次清掉;布局容器条件显隐(发布产物 `display:'block'` 风险)按报告确认宿主已装兜底补丁(`references/schema_adaptation.md`)。
32
33
 
33
34
  ## 0.5 速查优先
34
35
 
@@ -222,6 +223,11 @@ node scripts/diff_bands.mjs --id <卡id> --design <card-1.png> --tol 2 # 量
222
223
 
223
224
  ### Workflow B — 编辑已有卡片
224
225
 
226
+ 0. **适配(必跑)**:`node scripts/adapt_schema.mjs --id <id>`。有 `auto` 可修项就加 `--write` 落盘
227
+ (只改「改法唯一」的:style 里写死的 display、空条件 v-show 降级);AD-01/AD-02 按报告人工判定
228
+ (未声明变量是否复制残留、越界 `$_index` 节点该删还是该显)。存量卡、刚同步进来的卡、复制过子树
229
+ 的卡**必须**先做这步——否则在错基线上改,改完仍报同一个错。规则与事故来源见
230
+ `references/schema_adaptation.md`。
225
231
  1. 读卡:`card_read(id)` 或直接读 `src/cards/<id>/schema.json`(及 data.json)。
226
232
  2. 改结构:`add_component.mjs --id <id> --parent <cid> --className <X> [--text "…"] [--click]` 或 MCP `card_add_component`(自带离线硬校验);`card_node_models(id)` 定位字段。保留未涉及节点与孤儿字段。
227
233
  3. 同步数据:增删改 inputParams[] 时 data.json 同步(autofix 或 derive)。
@@ -248,6 +254,7 @@ node scripts/expand_ir.mjs --ir <IR.json> --out src/cards/<卡id>/ # IR→s
248
254
  node scripts/iterate.mjs --ir <IR> --out <dir> --design <png> [--set 'p=v'] # 一条龙视觉闭环
249
255
  node scripts/spec_tree.mjs --spec <spec.json> [--card 1] [--compact] # 层级树
250
256
  node scripts/lint_card.mjs --id <卡id> | --all [--json] # 行为规则体检(条数以实跑为准)
257
+ node scripts/adapt_schema.mjs --id <卡id> | --all [--write] [--json] # 适配环节:旧形态/错误属性归一(编辑前必跑)
251
258
  node scripts/diff_card.mjs --id <卡id> [--baseline <path>] # 变更对比(整体替换后必跑)
252
259
  node scripts/check_fidelity.mjs --spec <spec.json> --schema <schema.json> # 保真门禁(npm run check:fidelity)
253
260
  node scripts/render_card.mjs --id <卡id> [--keep-alive] # 离线截图
@@ -260,7 +267,7 @@ node scripts/v3_regression.mjs [--update-baseline] [--cases <path>] # v3 回
260
267
 
261
268
  **P0 生成必读**:`quickref.md`(一页纸速查)+ `ui_fidelity_rules.md`(还原度规则)+ `component_rules.md`(组件硬约束)。
262
269
 
263
- **P1 按场景**:`rules.md`(铁律全集)|`dsl_structure.md`(结构/itemId 全表)|`component_specs.md`(组件骨架)|`expression_engine.md`($ 引擎)|`amount_formatting.md`(金额)|`event_patterns.md` + `platform_jsapi.md`(事件/JSAPI)|`field_naming.md`(命名)|`recipes.md` + `recipe_snippets/`(范式)|`previewer_contract.md`(预览器契约)|`spec_to_schema_patterns.md`(v3 范式 + §20 决策层)|`iteration_protocol.md`(迭代协议)|`semantic_adjudication.md`(语义裁决细则)|`scripts_reference.md`(脚本全集)|`slices_convention.md`(切图)|`image_to_card.md`(图片管线)|`message-center-pattern.md`(消息中心)|`image_diff.md`(视觉清单)|`card_to_summary.md`(反向描述)|`card_index.json`/`card_templates.json`(路由索引)。
270
+ **P1 按场景**:`rules.md`(铁律全集)|`schema_adaptation.md`(适配规则库:旧形态/错误属性判定与修法)|`dsl_structure.md`(结构/itemId 全表)|`component_specs.md`(组件骨架)|`expression_engine.md`($ 引擎)|`amount_formatting.md`(金额)|`event_patterns.md` + `platform_jsapi.md`(事件/JSAPI)|`field_naming.md`(命名)|`recipes.md` + `recipe_snippets/`(范式)|`previewer_contract.md`(预览器契约)|`spec_to_schema_patterns.md`(v3 范式 + §20 决策层)|`iteration_protocol.md`(迭代协议)|`semantic_adjudication.md`(语义裁决细则)|`scripts_reference.md`(脚本全集)|`slices_convention.md`(切图)|`image_to_card.md`(图片管线)|`message-center-pattern.md`(消息中心)|`image_diff.md`(视觉清单)|`card_to_summary.md`(反向描述)|`card_index.json`/`card_templates.json`(路由索引)。
264
271
 
265
272
  **P2 改脚本/排查**:`generation_pitfalls.md`(132KB 事故档案,**训练侧编号体系**,与 lint 的 code 不是一套——查 lint 规则用 `scripts/lib/rules/registry.mjs`;已固化进 lint/expand_ir/autofix 的条目机械拦截,生成时无需预读)。
266
273
 
@@ -0,0 +1,116 @@
1
+ # 适配环节(Schema Adaptation)
2
+
3
+ > 存量卡 / 跨项目同步进来的卡 / 从别卡复制的片段,往往「结构合法、lint 也过」,但在
4
+ > 当前渲染契约或发布链路上会出错。适配环节就是**编辑与生成前的统一归一步骤**:
5
+ > 把这类历史形态一次性扫出来,能确定性修的自动修,需要语义判断的列成清单交人。
6
+
7
+ 执行器:`scripts/adapt_schema.mjs`。规则表在脚本顶部 `RULES`,本文是它的判定依据与事故来源。
8
+
9
+ ## 与相邻工具的边界(不要重复实现)
10
+
11
+ | 工具 | 回答的问题 | 改不改文件 |
12
+ |---|---|---|
13
+ | `validate_schema.py` | 结构合法吗、能渲染吗(契约硬卡点) | 否 |
14
+ | `autofix_schema.py` | 契约级机械错误(itemValueFrom / 重复 componentId / data.json 派生 / `$item.xxx` 别名) | 是(确定性) |
15
+ | `lint_card.mjs` / `lint_autofix.mjs` | 体验达标吗(漏 click、缺 color、切图命名…) | lint_autofix 带渲染守护才改 |
16
+ | **`adapt_schema.mjs`(本环节)** | **旧版本 / 旧编译器 / 跨宿主遗留的错误形态,在当前链路下会不会坏** | **`--write` 只改「改法唯一」的** |
17
+
18
+ 一条心法:**validate 管「能不能跑」,adapt 管「跑起来对不对」**。
19
+
20
+ ## 何时跑
21
+
22
+ 1. **Workflow B(编辑已有卡)第 0 步**——先适配再动手,避免在错基线上改。
23
+ 2. **Workflow A 收尾**(生成完成后)——新生成的卡不该有历史遗留,但复用模板/片段时可能带进来。
24
+ 3. **`platform-card-sync` 刚同步进来的卡**——远端存量卡是历史形态的最大来源。
25
+ 4. **从别卡复制子树后**——`$listData1/$listData2` 这类坏引用就是这么来的。
26
+
27
+ ## 用法
28
+
29
+ ```bash
30
+ node scripts/adapt_schema.mjs --id <卡id> # 只报告(默认)
31
+ node scripts/adapt_schema.mjs --id <卡id> --write # 落盘可确定性修复项
32
+ node scripts/adapt_schema.mjs --all # 全量库存巡检
33
+ node scripts/adapt_schema.mjs --all --json # 机器可读(接 CI / card-doctor)
34
+ node scripts/adapt_schema.mjs --list-rules # 打印规则表
35
+ ```
36
+
37
+ 退出码:`0` 无 error(或 `--write` 后清零)、`1` 存在未修复 error、`2` 用法/路径错误。
38
+ `--project-root <dir>` 可指定宿主工程根(默认 cwd)。
39
+
40
+ ## 规则表
41
+
42
+ 级别:`error` 会让卡出错;`warn` 有风险或需人工确认。
43
+ 修复:`auto`(`--write` 落盘)、`manual`(只报告)、`nolocal`(schema 无修点,属链路问题)。
44
+
45
+ ### AD-01 未声明变量引用(warn / manual)
46
+
47
+ - **判定**:表达式(properties / style / v-show 条件 / 事件代码)引用了既不在
48
+ `dictionary.inputParams` 声明、也不在列表项作用域(`slotScope`)、也不是内建的 `$var`。
49
+ - **分级**:
50
+ - 事件代码里有运行时写入(`this.setData({ x })` / `this.state.x =`)→ 动态 state 字段,
51
+ 只需确认注入时机早于首次渲染;
52
+ - 无写入痕迹 → 可能是复制残留,也可能是外部 `dataResponse` 直传字段,**需人工判定**。
53
+ - **为何不自动修**:外部数据字段是合法用法,机器无法与错误区分(实测全量 5 处,3 处是动态字段)。
54
+ - **实例**:`creditCardBill` 的 `column_cj` 引用了 `$listData1/$listData2`(从有 listData 结构的
55
+ 别卡复制残留),运行时求值抛错;修复是删掉该指令(父级已有等价守卫)。
56
+
57
+ ### AD-02 越界 `$_index`(error / manual)
58
+
59
+ - **判定**:节点不在 `slotScope: true` 的插槽内,却使用 `$_index`。
60
+ - **后果**:`$_index` 只由列表项作用域注入,域外求值为 `undefined`,条件恒 false → 节点永不渲染。
61
+ - **为何不自动修**:删节点还是去条件,取决于作者本意(要分隔线还是不要)。
62
+ - **实例**:`creditCardBill` 的 `divider_7l`/`divider_ep` 挂在「查看全部」行里(列表作用域外)
63
+ 却带 `$_index === 0`,从未渲染过;修复是删除(列表项模板内的 `divider_73/divider_d0` 合法保留)。
64
+
65
+ ### AD-03 布局容器上的条件显隐(warn / nolocal)
66
+
67
+ - **判定**:`className` ∈ {Root, Row, Column, SingleColumn, MultiColumn, GridLayout,
68
+ ListLayout, AbsoluteLayout} 且 v-show 的 `showType` 为 `01`/`02`。
69
+ - **后果**:v-show 在发布时由平台编译成内联 `display: (expr) ? 'block' : 'none'`。布局元素
70
+ 的 flex 来自样式类(`.ce_row{display:flex}`),内联 `display:block` 会把它压掉 →
71
+ justifyContent / alignItems / gap / grid 全失效。**schema 侧没有任何 display 可改**。
72
+ - **三层责任划分**(本次事故的完整结论):
73
+ 1. dsl-card 本地编译器(`server/core/flameCompile/{h5,client}.js`)已改为
74
+ `(expr) ? '' : 'none'`(提交 789573a,`tools/tests/flame-compile.test.mjs` 有断言);
75
+ 2. **线上产物由平台 Java 服务编译**(发布 → MinIO jsUrl),本地改不到,仍写 `'block'`;
76
+ 3. 兜底在宿主渲染层:`ai-assistant-h5/src/flame/cardStylePatch.js` 包装
77
+ `mCard.getCreateElement` 的 h,剔除 `style.display === 'block'`。
78
+ - **报告含义**:这条 warn 不是让你改 schema,而是提醒「这张卡在**未装 display 补丁的宿主**上
79
+ 会塌布局」。新宿主接入时必须装同类补丁。
80
+ - **实例**:`creditCardBill` 10 处(`row_f1`/`row_77`/`row_eg` 等)、全量库存 130 处。
81
+
82
+ ### AD-04 style 里写死 display(error / auto)
83
+
84
+ - **判定**:`style.properties[*].itemChildren.properties[*].itemKey === 'display'`。
85
+ - **后果**:`display` 不在 style-meta 白名单,validate 报 ERROR;旧渲染器里则会压掉样式类。
86
+ - **自动修**:删除该子项(display 只应由 v-show 产生,删掉等价于回到元素自身 display)。
87
+
88
+ ### AD-05 v-show 条件为空(error / auto)
89
+
90
+ - **判定**:`showType` 为 `01`/`02`,但 `condition.expression` 为空或缺失。
91
+ - **后果**:编译出的 display 表达式无意义,节点显隐不可控。
92
+ - **自动修**:`showType` 降级为 `'00'`(等价于「不做显隐控制」),语义安全。
93
+
94
+ ### AD-06 旧作用域别名 `$item`(warn / manual)
95
+
96
+ - **判定**:表达式出现 `$item.xxx` 或独立 `$item`,且卡片未声明同名根变量。
97
+ - **后果**:运行时插槽作用域是「外层 ∪ item 字段扁平展开 ∪ `{$$, _index}`」,**没有 `item` 键**,
98
+ `$item.xxx` 恒 undefined。
99
+ - **修法**:`python3 scripts/autofix_schema.py <schema>` 规则 5 归一为扁平字段名 / `$$` / `$_index`
100
+ (本脚本不重复实现,只负责发现)。
101
+
102
+ ## 扩展新规则(三步)
103
+
104
+ 1. `scripts/adapt_schema.mjs` 顶部 `RULES` 加一条:`{ title, level, fix, why }`;
105
+ 2. `analyzeCard` 里加检测(用现成的 `walkTree` / `expressionTexts` / `readVShow`);
106
+ 3. 若 `fix: 'auto'`,在 `applyFixes` 里补「改法唯一」的修复动作,并在本文补一节。
107
+
108
+ **准入标准**:每条规则必须有真实事故来源(哪张卡、什么现象、根因),否则不进规则表——
109
+ 没有证据的规则会变成噪音,最后没人看报告。
110
+
111
+ ## 已知的误报与边界
112
+
113
+ - 列表作用域内的自由变量一律放行(它们就是 item 字段);`slotScope: true` 的插槽子树整体豁免 AD-01。
114
+ - 事件代码里的 `$var` 命中按 warn 处理(代码里可能拼字符串,判定力弱于表达式)。
115
+ - AD-03 只看布局容器:`Text`(span)/`Divider`(div)被设 `block` 在 flex 容器里会被 blockify,
116
+ 视觉无差异,不单独报。
@@ -20,6 +20,7 @@ node scripts/ir_restore.mjs --list|--best|--round … # 迭代快
20
20
  node scripts/lint_card.mjs --id <卡id> | --all [--json] # 行为规则体检(条数以实跑输出为准,文档不复制数字)
21
21
  python3 scripts/validate_schema.py <schema.json> [--topology] # 结构合法性(CI 硬卡点)
22
22
  python3 scripts/autofix_schema.py <schema.json> [--check] # 机械修复 itemValueFrom/重复 id/data 漂移
23
+ node scripts/adapt_schema.mjs --id <卡id> | --all [--write] [--json] # 适配环节:旧形态/错误属性归一(编辑前必跑,规则见 schema_adaptation.md)
23
24
  node scripts/preflight.mjs --spec <spec.json> --id <卡id> # 14 项机械 Pre-flight
24
25
  node scripts/diff_card.mjs --id <卡id> [--baseline <path>] # 变更对比(铁律机械兜底,整体替换后必跑)
25
26
  node scripts/check_fidelity.mjs --spec <spec.json> --schema <schema.json> # 保真度门禁(元素级对齐断言,npm run check:fidelity)