deveco_hmigbot 0.1.0 → 0.1.2
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/README.md +2 -0
- package/dist/index.js +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/migrate-core/SKILL.md +43 -38
- package/skills/migrate-core/references/flow/build-error-patterns.md +1 -1
- package/skills/migrate-core/references/flow/conventions-template.md +14 -9
- package/skills/migrate-core/references/host-capabilities.md +3 -3
- package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +2 -2
- package/skills/migrate-core/scripts/closure/check-api-guards.mjs +44 -2
- package/skills/migrate-core/scripts/closure/check-safearea.mjs +20 -2
- package/skills/migrate-core/scripts/ledger/ledger.mjs +26 -12
- package/skills/migrate-core/scripts/smoke/walk.mjs +24 -3
- package/skills/migrate-core/templates/acceptance.js +15 -7
- package/skills/migrate-core/templates/mig_slices.js +32 -10
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -46,7 +46,7 @@ description: |
|
|
|
46
46
|
feasibility.json;模型结合调用深度改判(改判连理由写回同文件)。之后各阶段**只读 `shape.*` 决定形态,不再现场判断
|
|
47
47
|
"要不要跑"**——小应用不跑空探索/空地基波/对抗,大应用不因此少任何门。
|
|
48
48
|
|
|
49
|
-
**路径(一律工作区相对,禁绝对路径)**:`$SRC`=安卓源码相对目录(如 `MyApp`);`$DST`=鸿蒙工程相对目录(如 `MyAppHM`);`$HMIG`=本技能目录(skill 工具加载时返回的 Base directory 是 file URL:去掉 `file://` 前缀,Windows 上再去掉盘符前多出的那个 `/`(`file:///C:/x` → `C:/x`),`%20`
|
|
49
|
+
**路径(一律工作区相对,禁绝对路径)**:`$SRC`=安卓源码相对目录(如 `MyApp`);`$DST`=鸿蒙工程相对目录(如 `MyAppHM`);`$HMIG`=本技能目录(skill 工具加载时返回的 Base directory 是 file URL:去掉 `file://` 前缀,Windows 上再去掉盘符前多出的那个 `/`(`file:///C:/x` → `C:/x`),`%20` 还原为空格,含空格时整段加双引号;随 `deveco_hmigbot` 包封装:DevEco Code 自带的副本在二进制旁 `vendor/hmigbot/skills/migrate-core`,`deveco plug` 装的副本在宿主插件缓存里(绝对路径,仅用于 `node $HMIG/scripts/…` 调用,不进任何产物)——以返回值为准,scripts/ 与 references/ 随技能封装);`$SPEC`=`$DST/spec`(**迁移产物全在鸿蒙工程内**——多项目共用工作区互不干扰,工程交付自带全程审计);`$LEDGER`=`$SPEC/ledger`。命令与所有落盘产物(args/台账/报告/manifest)中的路径**必须是工作区相对形式**;selfcheck 检测工作区搬运。
|
|
50
50
|
只读写 `$SRC`、`$DST`(含 `$SPEC`)与 `.deveco/`,写进每次 workflow 的 goal。
|
|
51
51
|
|
|
52
52
|
**纪律**:源码绝对真源;不降级不占位(做不到的进台账 degradations,且先记待裁决);args>2KB 走 argsFile;
|
|
@@ -144,7 +144,7 @@ args 里的 `$ cd $DST && …` 由引擎执行,两端都行。台账入账一
|
|
|
144
144
|
- 页面卡=逐页功能断言;**域卡=无页面核心功能**(下载队列/播放会话/同步引擎/解析器等)
|
|
145
145
|
同规格断言——服务重于页面的应用,域卡应多于页面卡;断言标 `kind: ui|backend|on_device`:
|
|
146
146
|
**`on_device` 只给设备上可观察且静态证不了的契约**(真震/真弹通知/运行时授权/精确定时/冷重启后仍在/系统能力真调起);
|
|
147
|
-
数据层方法契约("提供 updateCart(cart)")、纯逻辑、可读码判定的 UI 行为一律 `backend`/`ui`——4b
|
|
147
|
+
数据层方法契约("提供 updateCart(cart)")、纯逻辑、可读码判定的 UI 行为一律 `backend`/`ui`——4b 的必验清单就是 on_device 全集,标错即撑大清单;
|
|
148
148
|
- **断言=每卡短验收单,宁短勿全**:只录"不达成即算失败"的行为(条数由模型按页面复杂度判断);
|
|
149
149
|
视觉参数/尺寸/间距/动画细节**一律不入账**(保真由转译骨架+safearea 门+smoke 截图承载,REPORT 附录提示人工复核);
|
|
150
150
|
- 断言纪律:原子可断言、带源锚点 `文件:行`、写**结果契约**(用户可见行为)不写实现步骤;
|
|
@@ -216,7 +216,7 @@ args 里的 `$ cd $DST && …` 由引擎执行,两端都行。台账入账一
|
|
|
216
216
|
设备级套件(装 HAP 上机跑 selfTest)另列、归阶段四·运行时验证(4b);
|
|
217
217
|
实现提示写 instructions.md 的 SECTION(格式照 `$HMIG/references/flow/unit-breakdown.md`)。
|
|
218
218
|
4. conventions.md:§A 以 `$HMIG/references/flow/conventions-template.md` §A 种子为基准;
|
|
219
|
-
`
|
|
219
|
+
`hmos-arkui-develop-skill` 在场则一次性并入,缺失/失败即用种子、不重试。§A/§A2 按本工程 SDK 查证后保留;
|
|
220
220
|
`$HMIG/references/flow/ui-layout-semantics.md` 拷到 $SPEC/,UI 切片 SECTION 首行写"必读 + 过其 §5 自查门"。
|
|
221
221
|
|
|
222
222
|
## 阶段三 · 开发(地基波 + 切片流水)
|
|
@@ -288,7 +288,7 @@ source_stub 须指出源码桩位置),成立的 `--to open`,不成立的 `
|
|
|
288
288
|
按 `## SECTION: <id>` 切分后把「头部公共说明+规范全文+该片 SECTION」注入该片 prompt,agent 免读 $SPEC/ 文件;
|
|
289
289
|
`card` 字段可显式覆盖;无对应 SECTION 的片回退文件指路。单片 SECTION 控制 ≤6K。
|
|
290
290
|
**波序纪律(首屏先行)**:wave-0 = 首页垂直片(首页真实数据流+其直接依赖的核心片),
|
|
291
|
-
|
|
291
|
+
构建+装机首屏可见真实数据后,其余切片再开闸;首页不通不铺面。**wave-0 之后其余切片一次提交**(片间 deps 照写,模板按依赖分层调度),不按依赖层再拆波;只有 resume/`only` 续跑才缩小执行集。
|
|
292
292
|
|
|
293
293
|
**跨波依赖闭包与路由归属**:`slices[]` 是**整个计划的切片全集**(所有页面片含后续波的、已完成的地基片一并列进来,未执行的 `expectation` 标注"本轮不执行"),只用 `only:[<本波片>]` 限定执行集——模板视不在执行集的依赖为已满足外部依赖(extSat,只记日志不重跑)。两道校验都按 `slices[]` 全集:依赖闭包(否则报 `切片 X 依赖不存在的 <core-id>`)与路由归属(route_map 每个注册页都要落在某片 `targetPath` 内,否则报 `路由 X 的注册页 … 不在任何切片 targetPath 内`;只有确不迁移的路由才写 `unownedRoutes`)。每波只改 `only`,不重写 `slices[]`。
|
|
294
294
|
|
|
@@ -353,22 +353,26 @@ acceptance 一遍:构建绿 → 逐单元 verify(判断性)→ 失败定
|
|
|
353
353
|
- 退出码 2(立案未裁决完)不许进下一阶段。
|
|
354
354
|
- **记门(门历史必须反映实况)**:`$ node $HMIG/scripts/ledger/ledger.mjs gate --dir $LEDGER --name closure-final --stage final --ok <true|false> --evidence "硬违规X·软立案Y已裁决"`。
|
|
355
355
|
|
|
356
|
-
### 4b
|
|
356
|
+
### 4b 运行时验证(核心路径确认,不是猎 bug、不是全量走查)
|
|
357
357
|
|
|
358
358
|
**定位(决定时长的关键)**:会抛 API/权限声明/**转译桩**的死按钮已由**静态门**(api-guards/permissions/stubs)在集成闭合
|
|
359
359
|
确定性拥有——**本步不再穷举重猎这些**;模型自写组件的点击死、缓存回调崩溃这类静态门看不见的只能靠本步走查
|
|
360
|
-
(`tapsNoChange`、崩溃日志)暴露,出现即按第 5 条归因;布局避让门只出**软立案**(非确定性),A
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
360
|
+
(`tapsNoChange`、崩溃日志)暴露,出现即按第 5 条归因;布局避让门只出**软立案**(非确定性),A 级走查逐页截图时**顺检顶部重合兜底**。
|
|
361
|
+
真机只答两个问题:①**核心用户路径真能跑通**(基本功能可用、迁移已完成)②**全部 on_device 断言**真的成立
|
|
362
|
+
(振动真震、通知真弹、权限运行时真授予、精确定时、冷重启后仍在)——on_device 是"静态证不了、只能上机"的契约,一条不漏地验到
|
|
363
|
+
passed/failed(--layer device)或带理由 waived(模拟器无该硬件→留真机),不许以"非核心路径"为由留 untested。
|
|
364
|
+
**不在核心路径上的 PRD 场景本步不走**,如实记"未走"进报告——不算失败、也不许记通过。随静态覆盖变强,真机应越来越短。
|
|
365
|
+
|
|
366
|
+
0. **清单 = 机械逐页计划 + 1–3 条核心路径流;不现场编、不按场景全集展开、不为"走全"加流。**
|
|
367
|
+
核心路径 = 应用主价值链,按源码路由图与首屏链路定:冷启(`"fresh": true`)→ 首页/列表 → 详情 → 核心操作 → 一次编辑回写
|
|
368
|
+
(改名/改描述/删条目后列表与详情同步)→ 一次冷重启核持久化(先改成非默认值)→ 设置页可达。每条流 ≤ 20 步、走到**操作之后的可观察结果**
|
|
369
|
+
(状态变化、文案变化、列表增减、重启后仍在)为止;超过就按状态断点拆成两条流,重跑一次不超过一分钟。
|
|
370
|
+
**on_device 断言全集是必验清单**:`$ node $HMIG/scripts/ledger/ledger.mjs list --dir $LEDGER --kind on_device --status untested` 输出的每一条
|
|
371
|
+
都要落进某条流——核心路径顺路覆盖的照验;覆盖不到的,在对应页面的流里追加最短的触发→观察步,或单开一条"on_device 补验"流(同样 ≤ 20 步),
|
|
372
|
+
不为它们展开整页走查。**有 PRD 时**:核心路径流照常写 `"scenarios": [...]` 标注它顺路覆盖的编号场景;其余场景不另写流,报告 PRD 栏由 render 按
|
|
373
|
+
`scenarios.txt` 全集记"未走"——分母不缩、不补写"全过"。PRD 步骤照抄时插入的导航步仍写 `"extra": true`(该场景只能记"走通但契约未满足")。
|
|
374
|
+
**有登录/账号的应用,登录是核心路径第一段**(有凭据或验证码通道就真登录,没有则依赖登录的段记 blocked,不绕过)。
|
|
375
|
+
|
|
372
376
|
1. **走查驱动只用 `$HMIG/scripts/smoke/walk.mjs`(确定性·单飞·可证),禁止现场自写 python/node uitest 脚本**
|
|
373
377
|
`walk.mjs help` 看用法:逐步交互用
|
|
374
378
|
`start/dump/tap/type/expect/wait/back/swipe/scroll/shot/alive`;核心路径写成步骤数据 `$SPEC/4b/<路径名>.json` 后
|
|
@@ -383,8 +387,10 @@ acceptance 一遍:构建绿 → 逐单元 verify(判断性)→ 失败定
|
|
|
383
387
|
probe 只记见到/未见不判失败(断言含空态/校验提示/保存后提示/用户键入等状态相关文案,落地页不可能一次全显),
|
|
384
388
|
"未见"不是缺陷,是提示核心路径计划该在哪个状态下 expect 它。每份报告的 probe 步带**页面字典**(该页全部 text/id/可点击项),
|
|
385
389
|
写核心路径计划时照字典取 text/id,一次写准,不要再手工 dump 猜文案。
|
|
386
|
-
|
|
387
|
-
|
|
390
|
+
`run` 不给 `--out` 时报告落在计划旁 `<计划名>.report.json`,**不要改名、不要删报告**(阶段五 render 按报告内 plan 字段配对,改名/删报告都会让场景表失真);
|
|
391
|
+
同目录已有走查报告时 run 前会列出计划里没在任何页面字典出现过的 match 目标——先照字典改计划再上设备,状态相关文案除外。
|
|
392
|
+
你只写第 0 条定的核心路径流(一流一份),不重复逐页枚举;**有 PRD 时每份流计划顶层写
|
|
393
|
+
`"scenarios": ["S12","S13"]`**(该流顺路覆盖的 PRD 编号场景)——阶段五 `render --walks $SPEC/4b` 按它机械汇总"场景 N/M",不手写。
|
|
388
394
|
**直达边界**:`start --route` 不经过首页,首页承载的状态恢复(读偏好/数据库)不会执行,看到的是种子/默认态。
|
|
389
395
|
**持久化类断言**("重启后保留")必须 `start` 正常冷启 + 应用内导航核;且先把状态改成**非默认值**再重启——用默认值验重启证明不了持久化
|
|
390
396
|
**首启/fixture/持久化类断言在干净态验**:`start` 步写 `"fresh": true`(清应用数据后冷启),评测用例每条都清数据。
|
|
@@ -395,14 +401,11 @@ acceptance 一遍:构建绿 → 逐单元 verify(判断性)→ 失败定
|
|
|
395
401
|
**滚动只用驱动的 `swipe`/`scrollUntil`**(步骤 `{"op":"scrollUntil","match":"<目标>","dir":"up","within":"type:^List$"}`;
|
|
396
402
|
`within` 缺省整屏、横滑区给容器匹配加 `--nth`),在命中容器的边界内 70%→30% 滑,折叠线下的项、触底加载、横滑条都能走到;
|
|
397
403
|
禁止自算坐标注入 swipe/fling。折叠线外内容不再靠"直达路由或 Back+重进"绕过——那验不到列表操作。
|
|
398
|
-
2. **A
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
装 HAP → 按清单走(无 PRD 时是应用主价值链:列表→详情→核心操作→设置),
|
|
404
|
-
逐页截图存 `$SPEC/screenshots/`。只补测静态门覆盖不到的**功能交互**(长按改数据/编辑保存/退后台回前台的状态对齐);
|
|
405
|
-
**别再逐个枚举猎崩溃/层序**(静态门已拦)。**超时间盒的边角一律转 untested,不深挖、不 fix-loop**。
|
|
404
|
+
2. **A 级 = 核心路径单遍确认(硬时间盒,自装 HAP 起计,只计走查本身)**:small 档 15 分钟、standard 档 30 分钟,**有无 PRD 一样**;
|
|
405
|
+
走查中发现的缺陷按第 5 条定向修+只复验涉事流,**修复与复验计入时间盒**;到点没走完的段转 untested **逐条写进报告**,不是丢弃、更不许记"全过"。
|
|
406
|
+
顺序:装 HAP → 先跑机械逐页计划(逐页可达、崩溃、页面字典,一两分钟)→ 再跑核心路径流,逐页截图存 `$SPEC/screenshots/`。
|
|
407
|
+
只补测静态门覆盖不到的**功能交互**(长按改数据/编辑保存/退后台回前台的状态对齐);**别再逐个枚举猎崩溃/层序**(静态门已拦)。
|
|
408
|
+
**超时间盒的边角一律转 untested,不深挖、不 fix-loop**。
|
|
406
409
|
写"无法到达"前必须差分:同一路径源端走一遍——源能到目标不能到=页面缺失(缺陷),两端都到不了才写不可达。
|
|
407
410
|
3. B 级:体检标记的 crypto/扫码/网络/媒体/振动/通知授权断言必须真机;模拟器不算验证。
|
|
408
411
|
**权限弹窗照常弹**:未签名调试包上系统授权弹窗一样弹出,
|
|
@@ -413,12 +416,14 @@ acceptance 一遍:构建绿 → 逐单元 verify(判断性)→ 失败定
|
|
|
413
416
|
走查证实坏了的断言先 `--to failed --layer device`(不是只落债),修好复验后再 `--to passed --layer device`;验不到留 untested 或 waived(带理由)。
|
|
414
417
|
真机结论(device)不会被静态判断改写;报告按"真机/静态"分列 passed 与 failed。
|
|
415
418
|
5. **发现问题 → 先读报告的失败类型再动手**:每个失败步带 `kind`——`target_missing`(附页面字典,照它改计划)、`target_ambiguous`
|
|
416
|
-
(用 `--nth`/`within`)、`expect_missing
|
|
419
|
+
(用 `--nth`/`within`)、`expect_missing`(附页面字典)**——先看期望文案的来源:来自 PRD/源码断言的字面量不许改计划,按应用缺陷处理;
|
|
420
|
+
只有模型自拟的文案才照字典改。改代码前先做一次"离开页面再进"或冷重启对照:重建后值对了 = 显示未刷新(@Builder 值参、非响应式绑定),
|
|
421
|
+
重建后仍错 = 状态未变(接线/逻辑)**、`process_died`(附 `crashDetail`:Reason/Error message/首个应用源码帧,
|
|
417
422
|
直接定位到 `entry/….ets:行`,不用再去设备上读 faultlog 二分);`tap` 步带 `occludedBy` 时点在弹层/对话框遮罩上,
|
|
418
423
|
目标在弹层内用 `"within": "type:^SheetPage$"` 之类限定容器。**只有 kind 指向应用缺陷时才改代码**。
|
|
419
424
|
→ **目标化修**(限涉事片)→ **只复验涉事片一次**(`only:[涉事片]` 或重跑该份走查计划)→ 过账。
|
|
420
|
-
|
|
421
|
-
|
|
425
|
+
**每条流累计最多 2 次复验(不论失败在哪一步,计划文案写错也算一次)**:第 3 次仍不过即 `--to failed --layer device` 记账 + 立案交阶段五,
|
|
426
|
+
**不再改代码、不再重跑**;报告 `failStreak ≥ 3` 就是停手信号,`≥ 2` 是最后一次机会。**铁律:禁重跑整轮 acceptance/对抗;修一片验一片,不牵动全局。**
|
|
422
427
|
**把崩溃/冻结/点击丢失/渲染异常归因为"平台级缺陷"或某个 idiom 之前必须走归因三步,缺一步不得写成约定、不得据此降级契约、
|
|
423
428
|
改写全局组件或"如实立案不深挖"**:
|
|
424
429
|
① 复现:同一构建 ≥2 次同结果(崩溃看 faultlog 首帧+寄存器特征一致;冻结/点击死看同步骤同现象);② 反转:对每个嫌疑条件单独撤掉→绿、装回→再现,各 ≥2 次,
|
|
@@ -428,8 +433,8 @@ acceptance 一遍:构建绿 → 逐单元 verify(判断性)→ 失败定
|
|
|
428
433
|
**归因有时间盒:单个崩溃/冻结/点击死 30 分钟**,到点未坐实就按
|
|
429
434
|
"实测缓解 + debts(附已排除的条件清单)"收口,别继续猎;已坐实的规则连同反转数据写进 §J,下一轮免重猎。
|
|
430
435
|
6. **收口记门**:`$ node $HMIG/scripts/ledger/ledger.mjs gate --dir $LEDGER --name runtime-4b --stage integration --ok <true|false> --evidence "<证据>"`;
|
|
431
|
-
evidence
|
|
432
|
-
`--ok true`
|
|
436
|
+
evidence 写"核心路径 N 条走通/M 条·on_device 断言 passed K/failed F/waived W(共 T,untested 必须为 0)",**有 PRD 时再加"顺路场景 N/M 已走·未走:<场景号>"(N/M 抄 render 的场景表,不手算)**;
|
|
437
|
+
`--ok true` 只在核心路径流报告全部 `ok:true`、`tapsNoChange` 为 0 且 on_device 无 untested 时给,否则 `--ok false`——门红不阻断收尾(阶段五照走、报告如实),
|
|
433
438
|
但不许把"部分完成"记成 true。
|
|
434
439
|
|
|
435
440
|
## 阶段五 · 发布回流(收尾)
|
|
@@ -453,14 +458,14 @@ acceptance 一遍:构建绿 → 逐单元 verify(判断性)→ 失败定
|
|
|
453
458
|
|
|
454
459
|
| 用在哪 | 自有知识(为准,恒可用,自足) | 可选增强(在场才用·一次不重试·缺失跳过) |
|
|
455
460
|
|---|---|---|
|
|
456
|
-
| conventions §A | `$HMIG/references/flow/conventions-template.md` §A 种子 |
|
|
457
|
-
| 构建/运行期修错 | `references/flow/build-error-patterns.md` + `platform-api-guards.md` + 构建输出错误清单 |
|
|
458
|
-
| 工程创建 | `$ devecocli create --project-path $DST --app-name … --bundle-name …`(宿主自带,无头可跑) | deveco-
|
|
461
|
+
| conventions §A | `$HMIG/references/flow/conventions-template.md` §A 种子 | hmos-arkui-develop-skill(§A 种子已吸收常踩项) |
|
|
462
|
+
| 构建/运行期修错 | `references/flow/build-error-patterns.md` + `platform-api-guards.md` + 构建输出错误清单 | hmos-runtime-fix-skill(运行期崩溃) |
|
|
463
|
+
| 工程创建 | `$ devecocli create --project-path $DST --app-name … --bundle-name …`(宿主自带,无头可跑) | deveco-cli 技能(同一脚本的会话式封装) |
|
|
459
464
|
| UI 切片 | `$HMIG/references/topics/ui-alignment/README.md` + `flow/ui-layout-semantics.md` | hmos-arkui-develop-skill |
|
|
460
|
-
| 状态管理切片 | conventions §A2 状态条目 |
|
|
461
|
-
| 沉浸式/安全区 | `scripts/closure/check-safearea.mjs` 门 + conventions §E |
|
|
465
|
+
| 状态管理切片 | conventions §A2 状态条目 | —(§A2 已吸收官方状态管理迁移语料) |
|
|
466
|
+
| 沉浸式/安全区 | `scripts/closure/check-safearea.mjs` 门 + conventions §E | —(§E 已吸收官方避让区语料) |
|
|
462
467
|
| 平台行为查证 | grep 本机 SDK `.d.ts` 声明为最准证据,结论进 conventions 标出处 | — |
|
|
463
|
-
| 崩溃分析 | hdc 拉 faultlogger + 按栈回源码 + `platform-api-guards.md` | hmos-
|
|
468
|
+
| 崩溃分析 | hdc 拉 faultlogger + 按栈回源码 + `platform-api-guards.md` | hmos-runtime-fix-skill |
|
|
464
469
|
| 多语言(源含多 locale values-*) | `$HMIG/references/topics/i18n/README.md`(references/ 按需) | — |
|
|
465
470
|
| 应用名称/图标规格 | `$HMIG/references/topics/app-identity.md`、`$HMIG/references/topics/icon-sizing.md` | — |
|
|
466
471
|
| 域知识(媒体/登录/支付等) | `$HMIG/references/topics/system-capabilities/*` + `topics/library-migration/*`(按加载索引取) | — |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> ⚠ 本文样例中 `getContext(this)` 为已废弃写法:组件内一律改 `this.getUIContext().getHostContext()`;Ability 内用 `this.context`。
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> 官方语法检查与废弃接口检查语料的蒸馏落点。
|
|
7
7
|
> 修错纪律:只修编译器报的错,不顺手重构;一个根因(如缺 interface)常消一片报错——修根因后先重建再继续;上限 20 轮,超限带残余错误清单上报。
|
|
8
8
|
|
|
9
9
|
## 1. 错误码 → 确定性修法
|
|
@@ -14,7 +14,7 @@ conventions.md 是**每个实现单元的第一必读文件**,决定并行子
|
|
|
14
14
|
|
|
15
15
|
## A. ArkTS 硬性规则(违反=编译失败;写每个文件前过一遍)
|
|
16
16
|
|
|
17
|
-
<用 skill 工具加载
|
|
17
|
+
<用 skill 工具加载 hmos-arkui-develop-skill(缺失即只用本种子),只提炼最常踩的那批(以常踩为准,不是配额),逐条写成"禁…→改用…"。
|
|
18
18
|
必含:禁 any/unknown;禁解构;对象字面量处处要类型(嵌套不继承外层);**禁计算属性名**
|
|
19
19
|
(`{[key]: v}` 一律改显式 key 或 Map——ValuesBucket 组装是最高频踩点);类字面量列全字段或 static of();
|
|
20
20
|
禁 index signature(Record 用引号 key);async 显式 Promise<T>;禁函数表达式/嵌套函数声明;
|
|
@@ -26,7 +26,7 @@ null 安全窄化;禁 delete/in/for-in/对象 spread/正则字面量/@ts-ignor
|
|
|
26
26
|
运行时 close() 必崩——编译与静态检查均不报)。>
|
|
27
27
|
|
|
28
28
|
<再补两张表:本项目会用到的 Kit 导入表(模块名→@kit.*);常用组件真实签名与常用枚举成员
|
|
29
|
-
(从 explore 结果和
|
|
29
|
+
(从 explore 结果和 hmos-arkui-develop-skill 的 API 速查里抄实际用得到的,不要全抄)。>
|
|
30
30
|
|
|
31
31
|
**§A 增补块**(逐条照录进生成的 §A):
|
|
32
32
|
· **import 纪律**:ArkUI 组件/容器/枚举/装饰器/组件控制器(Navigation、NavPathStack、LaunchMode、
|
|
@@ -96,7 +96,9 @@ null 安全窄化;禁 delete/in/for-in/对象 spread/正则字面量/@ts-ignor
|
|
|
96
96
|
`.nestedScroll(SELF_FIRST)` 并有界外尺寸;**不同向**嵌套不用配;下拉刷新头这类外层先滚
|
|
97
97
|
的用 `PARENT_FIRST`。点击被抢是另一回事:子元素 `parallelGesture`/`priorityGesture` 处理。
|
|
98
98
|
12. `@Builder` 值参按调用时刻快照传递:**动态**值(会随 `@Local`/`@Trace` 变且要刷 UI 的)
|
|
99
|
-
禁走值参——builder 内直读 `this.`
|
|
99
|
+
禁走值参——builder 内直读 `this.` 状态或按引用传对象 `{ v: this.x }`;静态标签/常量走值参完全正常,不要为此拆组件。
|
|
100
|
+
页面文本/标签 helper(`pageText(value)` 这类全局 `@Builder`)尤其如此:`pageText(\`总重量:${this.total} g\`)` 只渲染一次、之后永远显示旧值;
|
|
101
|
+
闭合门 `builder_state_by_value` 会把值参里引用状态字段的调用立案。
|
|
100
102
|
13. `@ObservedV2`/`@Trace`/`@Observed` 实例禁直接 `JSON.stringify`(访问器字段枚举不到,
|
|
101
103
|
产出空 JSON 或 key 全变 `__ob_` 前缀、后端不识别):模型写配对的 `toJson()/fromJson()`,
|
|
102
104
|
跨页传参/持久化/跨线程一律走它们。
|
|
@@ -110,10 +112,10 @@ null 安全窄化;禁 delete/in/for-in/对象 spread/正则字面量/@ts-ignor
|
|
|
110
112
|
16. V1/V2 状态体系禁混用:全工程统一 V2(`@ComponentV2` + `@Local/@Param/@Event/
|
|
111
113
|
@Provider/@Consumer/@ObservedV2/@Trace`),V1 装饰器(`@State/@Link/@Prop/@Provide/
|
|
112
114
|
@Observed`)一律不出现;混用是崩溃与不刷新的高发源。
|
|
113
|
-
|
|
115
|
+
〔来源:官方状态管理迁移语料蒸馏;按本工程 SDK 首用查证〕
|
|
114
116
|
17. `@Local` 禁止外部初始化:父传子初值用 `@Param`;"父给初值、子可自改"用
|
|
115
117
|
`@Param @Once`,子改需回传父用 `@Event` 回调,不许直接改 `@Param`。
|
|
116
|
-
|
|
118
|
+
〔来源:官方状态管理装饰器对照表;首用查证〕
|
|
117
119
|
18. `@ObservedV2`/`@Trace` 实例**禁存 V1 存储**(AppStorage/LocalStorage/PersistentStorage,V1/V2 混用即崩或不刷);
|
|
118
120
|
V2 存储 `AppStorageV2`/`PersistenceV2` **支持** @ObservedV2 对象——官方 PersistenceV2 指南明写"关联对象的 @Trace 属性变化
|
|
119
121
|
触发整个对象自动持久化",限制是:持久化的类属性必须有初值、不存 PixelMap/Native 类型、API 23 前不存容器/@Sendable
|
|
@@ -191,14 +193,17 @@ null 安全窄化;禁 delete/in/for-in/对象 spread/正则字面量/@ts-ignor
|
|
|
191
193
|
Image 默认裁剪,源没写 gravity/scaleType 目标也必须显式写),写完过其 §5 自查门;非 UI 单元忽略本条。
|
|
192
194
|
- 图标 $r('app.media.ic_*');缺图标记录到 $SPEC/front/missing-icons.txt 并用相近替代,不许编造资源名。
|
|
193
195
|
- 颜色走主题 token(随深浅色);固定色用 $r('app.color.*')。
|
|
194
|
-
-
|
|
196
|
+
- 全屏沉浸式按**分层契约**做,层与层不越界;
|
|
195
197
|
弹窗/半模态不做:
|
|
196
198
|
① 窗口层(EntryAbility.onWindowStageCreate):`setWindowLayoutFullScreen(true)` +
|
|
197
199
|
`getWindowAvoidArea`(SYSTEM 与 NAVIGATION_INDICATOR)存入全局状态,并订阅
|
|
198
200
|
`on('avoidAreaChange')` 刷新(折叠屏/转屏后避让值会变;`windowSizeChange` 只报窗口尺寸不报避让区);
|
|
199
201
|
② 容器层:`expandSafeArea` 只加在**根 Navigation/根容器一处**;
|
|
200
|
-
③
|
|
201
|
-
|
|
202
|
+
③ 页面层:各页以**响应式绑定**消费全局避让值后手动 padding——AppStorage 键用
|
|
203
|
+
`@StorageProp('键')`/`@StorageLink('键')`,自建全局态用 @ObservedV2/@Trace 或 @Consumer;
|
|
204
|
+
**禁止在 build 里 `AppStorage.get()` 一次性读取**(避让值经 `avoidAreaChange` 异步到达,非响应式读到的是
|
|
205
|
+
首帧的 0 且页面永不重排,顶部必压状态栏);**禁止再自行 expandSafeArea**——
|
|
206
|
+
非响应式读取、重复声明、只 expand 不避让是三类最常见的重叠/留黑根因;
|
|
202
207
|
子组件/浮层一律不碰安全区(宿主负责)。集成闭合的 safearea 静态门逐页核查(closure-suite)。
|
|
203
208
|
- 系统符号 `$r('sys.symbol.*')` 名称必须经 SDK 符号表/官方文档核实后用(高频幻觉名:
|
|
204
209
|
music_note/copy/doc_on_doc/square_and_arrow_up 均不存在);无对应符号改用工程 media 资源,
|
|
@@ -238,7 +243,7 @@ null 安全窄化;禁 delete/in/for-in/对象 spread/正则字面量/@ts-ignor
|
|
|
238
243
|
```
|
|
239
244
|
|
|
240
245
|
要点提醒:
|
|
241
|
-
- §A 不要整段复制
|
|
246
|
+
- §A 不要整段复制 hmos-arkui-develop-skill——提炼常踩项即可,全文让需要深挖的单元自己加载技能。
|
|
242
247
|
- §I 的独占清单必须与 implement 调用的 `sharedFiles` 一致。
|
|
243
248
|
- §J 是并行波次之间传递经验的唯一通道,波结束不写 = 下一波重复踩坑。
|
|
244
249
|
- 反向依赖用注入回调解耦(例:底层 Writer 需要触发上层逻辑时,提供 `setXxxHook(cb)` 由集成单元注册),把这类钩子约定写进 §C/§G。
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
| 能力 | 期望的宿主件 | 实测(名称/形态/限制) | 兜底 |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| 工程创建 | `devecocli create --project-path <dir> --app-name <X> --bundle-name <Y>` | **实测无头可跑**(自动探测本机 SDK API 级别,产出含 IDE
|
|
13
|
+
| 工程创建 | `devecocli create --project-path <dir> --app-name <X> --bundle-name <Y>` | **实测无头可跑**(自动探测本机 SDK API 级别,产出含 IDE 默认图标的标准工程);宿主 deveco-cli 技能是它的会话式封装,加载器不继承 bash 环境时直接跑命令即可 | 手工 scaffold(不再随包带模板工程) |
|
|
14
14
|
| 构建 | build_project(结构化错误,IDE 可跳转) | **实测可用**;依赖 ohpm registry,网络断时 install 步失败→回退 hvigorw(两单实测) | `$ hvigorw` 退出码 |
|
|
15
15
|
| 静态检查 | arkts_check / codelinter | 待实测(历史证据:存在;@kit 声明有已知误报,以 hvigor 为准) | codelinter CLI |
|
|
16
16
|
| **界面核验** | verify_ui | **不在默认阶梯**:VL 模型驱动,单次 12 分钟量级;报"应用未安装"是其内部 startApp 解析 `bm dump` 失败即判未装(无重试,本机 64 次启动 11 次中招,与签名无关,`bm dump` 手工 16 次零失败);判定文本亦有"点击未生效"类误判。只作补充证据,报未安装先 `walk.mjs alive` 核实再重试一次 | `$HMIG/scripts/smoke/walk.mjs`(确定性控件树断言+截图+崩溃,单飞) |
|
|
17
17
|
| 设备控制 | start_app + devecocli device/run | **实测可用**(装启/设备列表/跳过构建重装);uitest 长按时长不足是已知限制 | hdc + uitest uiInput |
|
|
18
18
|
| 模拟器 | 模拟器 | **实测在位**(Pura 90 2,全链路走查可用;无触觉/部分 picker 形态不可达) | 手动起模拟器 |
|
|
19
|
-
| 崩溃日志 | faultlogger 读取 | 待实测 | hdc shell + hmos-
|
|
20
|
-
| 官方文档检索 |
|
|
19
|
+
| 崩溃日志 | faultlogger 读取 | 待实测 | hdc shell + hmos-runtime-fix-skill 技能 |
|
|
20
|
+
| 官方文档检索 | 无(本机 SDK d.ts 为准) | 宿主未带检索技能;hmos-arkui-develop-skill 只有 API 速查 | references/ 语料 |
|
|
21
21
|
| 权限预放行 | headless 运行的权限策略 | 待实测(引擎有自动应答,策略随宿主) | 安装期预授权清单 |
|
|
22
22
|
|
|
23
23
|
填表完成后:把"实测"列同步进 migrate-core 阶段一的盘点说明,退役重复自建件,
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
## 查询机制(deveco 原生、离线、随 SDK/工具链——不依赖外部 skill)
|
|
13
13
|
1. **grep/ls 本机 SDK 声明(最准,SDK 自带的权威索引)**:
|
|
14
14
|
- `ls <SDK>/openharmony/ets/arkts/` **一次列出整个 ArkTS 基础库面**(`@arkts.math.Decimal.d.ets`、`@arkts.collections` 等)——模型直接看见平台提供了什么,杜绝瞎判"缺失"。
|
|
15
|
-
- `<SDK>/openharmony/ets/api/`(`@ohos.*`/`@kit.*` 的 `.d.ts`)+ `ets/arkts/`(`@arkts.*` 的 `.d.ets`)。grep 类/方法名即得权威签名。SDK
|
|
15
|
+
- `<SDK>/openharmony/ets/api/`(`@ohos.*`/`@kit.*` 的 `.d.ts`)+ `ets/arkts/`(`@arkts.*` 的 `.d.ets`)。grep 类/方法名即得权威签名。SDK 路径按 `references/topics/env-doctor.md` 解析。
|
|
16
16
|
- 实证:`<SDK>/openharmony/ets/arkts/@arkts.math.Decimal.d.ets` 就在——BigDecimal 有原生等价,无需手造。
|
|
17
17
|
2. **`devecocli docs search <关键词>` / `docs read <id>`**:官方文档全量检索(API 参考+指南+FAQ),离线。实证:"BigDecimal 高精度"秒中 `@arkts.math.Decimal`。用法/语义看这里。
|
|
18
18
|
3. **锚文档一次读全平台面**:`从TypeScript到ArkTS的适配规则`、`ArkTS基础类库概述`。
|
|
19
19
|
|
|
20
|
-
>
|
|
20
|
+
> **不依赖外部检索技能**:宿主未内置文档检索件;方法已吸收进上面的原生机制(本机 SDK 声明 + 语料)。
|
|
21
21
|
|
|
22
22
|
## 确认种子(高置信,直接用)
|
|
23
23
|
| JDK/Kotlin 原语 | ArkTS 等价 | Level | 用法 | 备注 |
|
|
@@ -163,11 +163,53 @@ for (const ROOT of ROOTS) for (const f of walkFiles(ROOT, { match: (_p, e) => e.
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
// ---- 规则③:@Builder 值参传状态(官方 arkts-builder:按值传递的参数是调用时刻快照,状态变化不刷新 builder 内 UI;语料 conventions §12)----
|
|
167
|
+
// 判据(结构):带参 @Builder(全局 function 或组件方法)的调用点,非函数实参里直接引用本组件状态装饰字段 this.<@Local/@State/@Param/@Trace…>,
|
|
168
|
+
// 且实参不是对象字面量(按引用形态 { v: this.x } 合法)。回调 lambda 按值传本就正常,排除;常量/未装饰字段不算。
|
|
169
|
+
// 语料量化(2026-09-22,49 工程):gold 20 个 0/117 调用命中;本套件试点 116 处/7 工程、他工具 322 处/15 工程——bikepacking 真机复现"总重量"永远显示上一次值。
|
|
170
|
+
// 分级:case(页面重建时碰巧能对的良性用法存在,如 NavDestination body 按次传 hostArgs),裁决=builder 内直读 this. 或按引用传对象。
|
|
171
|
+
const STATE_DECOR = /@(?:State|Prop|Link|Provide|Consume|ObjectLink|StorageProp|StorageLink|LocalStorageProp|LocalStorageLink|Local|Param|Trace|Computed|Provider|Consumer)\b[^;\n]*?\b([\w$]+)\s*[:=]/g;
|
|
172
|
+
const BUILDER_FN_G = /@Builder\s*(?:export\s+)?function\s+([\w$]+)\s*\(([^)]*)\)/g;
|
|
173
|
+
const BUILDER_FN_M = /@Builder\s+(?:(?:private|public|protected|static)\s+)*([\w$]+)\s*\(([^)]*)\)/g;
|
|
174
|
+
function callArgs(code, i) { let d = 0; const out = []; let cur = ''; for (let j = i; j < code.length; j++) { const c = code[j]; if (c === '(' || c === '{' || c === '[') d++; if (c === ')' || c === '}' || c === ']') { d--; if (d === 0) { out.push(cur); return out; } } if (c === ',' && d === 1) { out.push(cur); cur = ''; continue; } if (d >= 1 && !(d === 1 && c === '(' && j === i)) cur += c; } return null; }
|
|
175
|
+
let builderStateByValue = 0;
|
|
176
|
+
{
|
|
177
|
+
const files = []; for (const ROOT of ROOTS) for (const f of walkFiles(ROOT, { match: (_p, e) => e.endsWith('.ets') })) files.push(f);
|
|
178
|
+
const codes = files.map(f => [f, stripComments(readFileSync(f, 'utf8'))]);
|
|
179
|
+
const globalBuilders = new Set();
|
|
180
|
+
for (const [, c] of codes) for (const m of c.matchAll(BUILDER_FN_G)) if (m[2].trim()) globalBuilders.add(m[1]);
|
|
181
|
+
for (const [f, c] of codes) {
|
|
182
|
+
const state = new Set([...c.matchAll(STATE_DECOR)].map(m => m[1]));
|
|
183
|
+
if (!state.size) continue;
|
|
184
|
+
// 同名撞车防护:裸调用 name( 只认本文件声明或 import 进来的全局 builder;this.name( 只认本文件的 @Builder 方法
|
|
185
|
+
const localGlobals = new Set([...c.matchAll(BUILDER_FN_G)].filter(m => m[2].trim()).map(m => m[1]));
|
|
186
|
+
const imported = new Set([...c.matchAll(/import\s*\{([^}]*)\}\s*from/g)].flatMap(m => m[1].split(',').map(x => x.trim().split(/\s+as\s+/).pop()).filter(Boolean)));
|
|
187
|
+
const methods = new Set([...c.matchAll(BUILDER_FN_M)].filter(m => m[2].trim() && m[1] !== 'function').map(m => m[1]));
|
|
188
|
+
const names = new Set([...globalBuilders].filter(n => localGlobals.has(n) || imported.has(n)).concat([...methods]));
|
|
189
|
+
for (const name of names) {
|
|
190
|
+
for (const m of c.matchAll(new RegExp(`(?<![\\w$.])(?:this\\.)?${name}\\s*\\(`, 'g'))) {
|
|
191
|
+
if (/@Builder\s*(?:export\s+)?(?:function\s+)?(?:(?:private|public|protected|static)\s+)*$/.test(c.slice(Math.max(0, m.index - 60), m.index))) continue;
|
|
192
|
+
const viaThis = m[0].startsWith('this.');
|
|
193
|
+
if (viaThis ? !methods.has(name) : !(localGlobals.has(name) || imported.has(name))) continue;
|
|
194
|
+
const a = callArgs(c, m.index + m[0].length - 1); if (!a) continue;
|
|
195
|
+
const bad = a.map(x => x.trim()).filter(t => t && !t.startsWith('{') && !/=>/.test(t) && !/^function\b/.test(t) && [...t.matchAll(/\bthis\.([\w$]+)/g)].some(mm => state.has(mm[1])));
|
|
196
|
+
if (!bad.length) continue;
|
|
197
|
+
builderStateByValue++;
|
|
198
|
+
const line = c.slice(0, m.index).split('\n').length;
|
|
199
|
+
const fields = [...new Set(bad.flatMap(t => [...t.matchAll(/\bthis\.([\w$]+)/g)].map(mm => mm[1]).filter(n => state.has(n))))];
|
|
200
|
+
findings.push({ severity: 'case', type: 'builder_state_by_value', name, file: rel(f), line,
|
|
201
|
+
basis: '判据:带参 @Builder 的调用点,非函数实参直接引用本组件状态装饰字段(this.' + fields.slice(0, 3).join('/this.') + '),且非对象字面量按引用形态',
|
|
202
|
+
evidence: `${rel(f)}:${line} ${name}(${bad[0].slice(0, 60)}) 按值传状态——@Builder 值参是调用时刻快照,this.${fields[0]} 之后变化不会刷新该处 UI(官方 arkts-builder;bikepacking 真机复现总重量永远显示上一次值)。改法:builder 内直读 this.${fields[0]},或按引用传对象 ${name}({ v: this.${fields[0]} });页面重建时才变的值(如导航 hostArgs)可带理由豁免` });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const report = { project: relative(process.cwd(), resolve(PROJECT)) || '.', stats: { scannedFiles: scanned, nakedCalls: naked, nakedDeferred, discardedAccess, builderLambda, builderStateByValue }, findings };
|
|
167
209
|
const out = argOf('--json', null);
|
|
168
210
|
if (out) { mkdirSync(dirname(resolve(out)), { recursive: true }); writeFileSync(out, JSON.stringify(report, null, 2)); }
|
|
169
211
|
const vio = findings.filter(f => f.severity === 'violation');
|
|
170
|
-
console.log(`[check-api-guards] 涉险文件 ${scanned} · 裸调(未防护) ${naked}(其中 async/Promise 回调内静默失败档 ${nakedDeferred})· accessSync 弃置 ${discardedAccess}`);
|
|
212
|
+
console.log(`[check-api-guards] 涉险文件 ${scanned} · 裸调(未防护) ${naked}(其中 async/Promise 回调内静默失败档 ${nakedDeferred})· accessSync 弃置 ${discardedAccess} · @Builder 值参传状态 ${builderStateByValue}`);
|
|
171
213
|
for (const v of vio.slice(0, 12)) console.log(` ✗ ${v.evidence}`);
|
|
172
214
|
const cas = findings.filter(f => f.severity === 'case');
|
|
173
215
|
for (const c of cas.slice(0, 8)) console.log(` ▲ CASE ${c.type} — ${c.evidence}`);
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* ② 取一次不订阅:getWindowAvoidArea 只算一次、全工程无 avoidAreaChange 重挂——初值/旋转/多窗后 inset 卡 0;
|
|
9
9
|
* ③ 页面根本不消费:壳层建好了避让基建(全局 insets 服务),但某些导航页既不引用它也不自取避让——
|
|
10
10
|
* "每页手动消费"模式必然漏页(两轮真机重合实证,且漏的页每轮不同——系统病)。
|
|
11
|
+
* ④ 非响应式消费:页面只用 AppStorage.get 一次性读提供者键、无 @StorageProp/@StorageLink 绑定——避让值经
|
|
12
|
+
* avoidAreaChange 异步到达时读到首帧的 0 且页面不重排(真机实证:6 页全部顶部重合,布局树却已"padding")。
|
|
11
13
|
*
|
|
12
14
|
* 判定全为**软立案**(case)——竞态/消费与否静态无法定罪(全屏页、对话框、时序确稳皆属合法),
|
|
13
15
|
* 绝不设硬违规、恒退 0:由 closure-suite final 阶段汇总,模型逐条回源裁决(真重合→修;合法→带理由豁免)。
|
|
@@ -40,10 +42,16 @@ const projHasAvoidChange = [...texts.values()].some(c => /avoidAreaChange/.test(
|
|
|
40
42
|
// import+赋值双条件是显式跨文件链,杜绝把内置全局名误当提供者)。
|
|
41
43
|
// 页面凡引用其一(或自取避让/expandSafeArea)即视为"消费了避让"。
|
|
42
44
|
const providerNames = new Set();
|
|
45
|
+
// 避让键子集:setOrCreate 的值表达式取自 AvoidArea 结构(SDK 声明 AvoidArea{topRect,bottomRect,…} / AvoidAreaOptions.area)——
|
|
46
|
+
// 同一提供者文件里写的其他键(路由、深链)不是避让值,④ 只看这个子集
|
|
47
|
+
const insetKeys = new Set();
|
|
43
48
|
for (const code of texts.values()) {
|
|
44
49
|
if (!/getWindowAvoidArea/.test(code)) continue;
|
|
45
50
|
for (const m of code.matchAll(/export\s+(?:default\s+)?(?:@\w+\s+)*(?:class|struct|const|let|function|enum)\s+([A-Za-z_$][\w$]*)/g)) providerNames.add(m[1]);
|
|
46
|
-
for (const m of code.matchAll(/AppStorage\.setOrCreate\(\s*['"]([^'"]+)['"]/g))
|
|
51
|
+
for (const m of code.matchAll(/AppStorage\.setOrCreate\(\s*['"]([^'"]+)['"]\s*,([^;\n]*)/g)) {
|
|
52
|
+
providerNames.add(m[1]);
|
|
53
|
+
if (/\b(?:top|bottom|left|right)Rect\b|\.area\b|AvoidArea\b/.test(m[2])) insetKeys.add(m[1]);
|
|
54
|
+
}
|
|
47
55
|
const imported = [];
|
|
48
56
|
// 四形态:命名 { a, b as c } / 默认 x / 混合 x, { y } / 命名空间 * as ns
|
|
49
57
|
for (const m of code.matchAll(/import\s+(?:([A-Za-z_$][\w$]*)\s*,\s*)?(?:\{([^}]*)\}|\*\s+as\s+([A-Za-z_$][\w$]*)|([A-Za-z_$][\w$]*))\s*from/g)) {
|
|
@@ -100,6 +108,16 @@ if (edgeToEdge) {
|
|
|
100
108
|
pagesChecked++;
|
|
101
109
|
const selfAvoids = /expandSafeArea|getWindowAvoidArea|avoidAreaChange/.test(code);
|
|
102
110
|
const consumesProvider = [...providerNames].some(n => n && new RegExp(`\\b${escRe(n)}\\b`).test(code));
|
|
111
|
+
// ④ 消费了提供者键却只 AppStorage.get 一次性读取(无 @StorageProp/@StorageLink/AppStorage.prop|link 绑定):结构判据只看绑定形态
|
|
112
|
+
if (consumesProvider && insetKeys.size) {
|
|
113
|
+
const keys = [...insetKeys].filter(n => new RegExp(`\\b${escRe(n)}\\b`).test(code));
|
|
114
|
+
const reactive = keys.some(n => new RegExp(`@Storage(?:Prop|Link)\\(\\s*['"]${escRe(n)}['"]|AppStorage\\.(?:prop|link|setAndProp|setAndLink)\\(\\s*['"]${escRe(n)}['"]`).test(code));
|
|
115
|
+
const oneShot = keys.some(n => new RegExp(`AppStorage\\.get\\s*(?:<[^>]*>)?\\(\\s*['"]${escRe(n)}['"]`).test(code));
|
|
116
|
+
if (oneShot && !reactive) {
|
|
117
|
+
findings.push({ severity: 'case', type: 'nonreactive_inset_read', name: rel(f), file: rel(f), line: line(/AppStorage\.get/),
|
|
118
|
+
evidence: `${rel(f)}:${line(/AppStorage\.get/)} 用 AppStorage.get 一次性读取避让键 [${keys.slice(0, 2).join('/')}]、无 @StorageProp/@StorageLink 绑定——avoidAreaChange 异步到达时读到首帧 0 且不重排,顶部内容压状态栏(裁决:改响应式绑定,或证明读取必晚于避让值到达则豁免)` });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
103
121
|
// 页面把避让委托给它 import 的组件(AppScaffold/TopBar 之类)也算消费:沿相对 import 走 ≤3 跳,被引文件自取避让或引用提供者即可
|
|
104
122
|
if (!selfAvoids && !consumesProvider && avoidsViaImport(f)) { continue; }
|
|
105
123
|
if (!selfAvoids && !consumesProvider) {
|
|
@@ -115,7 +133,7 @@ const byType = (t) => cases.filter(c => c.type === t).length;
|
|
|
115
133
|
const report = { project: relative(process.cwd(), resolve(PROJECT)) || '.', edgeToEdge, stats: { insetFiles, pagesChecked, providers: [...providerNames] }, findings };
|
|
116
134
|
const out = argOf('--json', null);
|
|
117
135
|
if (out) { mkdirSync(dirname(resolve(out)), { recursive: true }); writeFileSync(out, JSON.stringify(report, null, 2)); }
|
|
118
|
-
console.log(`[check-safearea] edge-to-edge:${edgeToEdge ? '是' : '否'} · 取顶部avoid文件 ${insetFiles} · 页面消费检查 ${pagesChecked} 页 · 软立案 ${cases.length}(竞态${byType('racy_safearea_gate')}/失订${byType('stale_inset_no_subscription')}/页未避让${byType('page_top_unavoided')})`);
|
|
136
|
+
console.log(`[check-safearea] edge-to-edge:${edgeToEdge ? '是' : '否'} · 取顶部avoid文件 ${insetFiles} · 页面消费检查 ${pagesChecked} 页 · 软立案 ${cases.length}(竞态${byType('racy_safearea_gate')}/失订${byType('stale_inset_no_subscription')}/页未避让${byType('page_top_unavoided')}/非响应式${byType('nonreactive_inset_read')})`);
|
|
119
137
|
for (const c of cases.slice(0, 12)) console.log(` ▲ ${c.evidence}`);
|
|
120
138
|
if (!cases.length) console.log(edgeToEdge ? ` ✓ 未发现脆弱写法(软证据:本门非确定性,真机走查仍须逐页顺检顶部重合)` : ' ✓ 非 edge-to-edge,系统自动避让');
|
|
121
139
|
// 软立案不让 checker 失败(恒退 0)——closure-suite final 阶段汇总裁决,绝不逼修
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
* add --dir D --type pages|features|slices|degradations|debts --file <JSON 文件>(对象或数组;--json '<…>' 仅 POSIX shell 可用,Windows 一律 --file)
|
|
16
16
|
* status --dir D --type T --id X --to 状态 [--evidence "..."] [--reason "..."] [--layer static|device]
|
|
17
17
|
* --layer 记证据来源:static=读码判断(验收代理)、device=真机走查(4b);device 覆盖 static,static 不许改写 device 的结论
|
|
18
|
-
* gate --dir D --name 名称 --ok true|false [--evidence "..."] [--stage slice|integration|final]
|
|
18
|
+
* gate --dir D --name 名称 --ok true|false [--evidence "..."] [--stage slice|integration|final] # runtime-4b 且 on_device 仍有 untested → 改判 FAIL 并写明
|
|
19
19
|
* list --dir D --type T [--kind K] [--status S] # 列条目(如 4b 待验:--type features --kind on_device --status untested)
|
|
20
20
|
* render --dir D [--out report.md] [--anchors spec/closure/anchors.json] [--walks spec/4b] [--scenarios spec/4b/scenarios.txt]
|
|
21
21
|
* # 投影:进度+四态(按证据层分列)+门历史+降级(含待裁决)+债务;--anchors 追加 PRD 逐行覆盖对照表;
|
|
22
|
-
* # --walks 读 4b 计划/报告目录,按计划里的 scenarios 字段汇总"场景走查"表(PRD
|
|
22
|
+
* # --walks 读 4b 计划/报告目录,按计划里的 scenarios 字段汇总"场景走查"表(PRD 栏由此出,不手写);报告按其 plan 字段配对计划(同一计划多份取最新 at),文件名只是后备;
|
|
23
23
|
* # 分母取 --scenarios(缺省 <walks>/scenarios.txt:4b 第 0 条枚举的场景全集,一行一个),没有该文件时分母退化为计划提到的并明示
|
|
24
24
|
* verify --dir D [--sdk auto|<SDK 根>] [--require-gates a,b] # 自检:evidence/reason 纪律、状态合法性、
|
|
25
25
|
* # 降级无待裁决(pending)、阻塞债(blk-*)已清、--require-gates 指定的门有记录;
|
|
@@ -196,10 +196,23 @@ if (cmd === 'init') {
|
|
|
196
196
|
const l = load();
|
|
197
197
|
const okRaw = argOf('--ok', null);
|
|
198
198
|
if (okRaw !== 'true' && okRaw !== 'false') { console.error(`--ok 只接受 true|false(收到 ${okRaw})——拼错不许静默记 FAIL 污染门历史`); process.exit(2); }
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
let ok = okRaw === 'true';
|
|
200
|
+
const name = argOf('--name', '?'); let evidence = argOf('--evidence', '');
|
|
201
|
+
// runtime-4b 的结构判据:on_device 断言(静态证不了、只能上机)只要还有 untested,就不能记 ok——真机门存在的意义就是这批契约;
|
|
202
|
+
// 逐条 status --layer device 过账或带理由 waived 后再记门。cofi 2026-09-22:门记"34/34 全过"而 on_device 13 条 0 passed、11 untested
|
|
203
|
+
if (name === 'runtime-4b') {
|
|
204
|
+
const od = (l.features || []).filter(f => f.kind === 'on_device');
|
|
205
|
+
const left = od.filter(f => f.status === 'untested'), waived = od.filter(f => f.status === 'waived');
|
|
206
|
+
if (ok && left.length) {
|
|
207
|
+
ok = false; evidence = `${evidence}${evidence ? ';' : ''}[台账改判 FAIL] on_device 仍 untested ${left.length} 条:${left.map(f => f.id).join(' ')}`;
|
|
208
|
+
console.error(`[ledger] runtime-4b 不能记 ok:on_device 断言仍 untested ${left.length} 条(${left.slice(0, 8).map(f => f.id).join(' ')}${left.length > 8 ? ' …' : ''})——逐条 status --to passed|failed --layer device 或带理由 waived 后再记门;本次按 FAIL 记录`);
|
|
209
|
+
}
|
|
210
|
+
// waived 不算 untested,但要写在门上:绿门若靠豁免换来,报告读者一眼能看见是哪几条
|
|
211
|
+
if (waived.length) evidence = `${evidence}${evidence ? ';' : ''}[台账附注] on_device waived ${waived.length} 条:${waived.map(f => f.id).join(' ')}`;
|
|
212
|
+
}
|
|
213
|
+
l.gates.push({ at: now(), name, stage: argOf('--stage', 'integration'), ok, evidence });
|
|
201
214
|
save(l);
|
|
202
|
-
console.log(`[ledger] gate ${
|
|
215
|
+
console.log(`[ledger] gate ${name} ${ok ? 'PASS' : 'FAIL'}`);
|
|
203
216
|
} else if (cmd === 'list') {
|
|
204
217
|
// 机读清单查询(4b 走查清单派生等):按 type/kind/status 过滤,行式输出 id\tkind\tstatus\ttitle
|
|
205
218
|
const l = load();
|
|
@@ -250,11 +263,14 @@ if (cmd === 'init') {
|
|
|
250
263
|
} else lines.push('', `⚠ --anchors 报告无 prdCoverage(跑 check-anchors 时未定位到 PRD)`);
|
|
251
264
|
} catch (e) { lines.push('', `⚠ --anchors 报告不可读:${e.message}`); }
|
|
252
265
|
}
|
|
253
|
-
// --walks <4b 目录>:读 *.json 计划的 scenarios 字段 +
|
|
266
|
+
// --walks <4b 目录>:读 *.json 计划的 scenarios 字段 + 报告(按报告内 plan 字段配对,同一计划多份取最新 at;无 plan 字段才按同名)的结果,汇总场景走查表——"场景 N/M"由此出,不由模型手写
|
|
254
267
|
const walksDir = argOf('--walks', null);
|
|
255
268
|
if (walksDir) {
|
|
256
269
|
try {
|
|
257
270
|
const plans = readdirSync(walksDir).filter(f => f.endsWith('.json') && !f.endsWith('.report.json'));
|
|
271
|
+
// 报告按 plan 字段归到计划名下(模型常把 --out 改名;同一计划多份取最新 at),没有 plan 字段的按文件名同名
|
|
272
|
+
const byPlan = new Map();
|
|
273
|
+
for (const rf of readdirSync(walksDir).filter(f => f.endsWith('.report.json'))) { let rep; try { rep = JSON.parse(readFileSync(join(walksDir, rf), 'utf8')); } catch { continue; } const key = String(rep.plan || rf.replace(/\.report\.json$/, '.json')).replace(/^.*[\\/]/, ''); const prev = byPlan.get(key); if (!prev || String(rep.at || '') > String(prev.at || '')) byPlan.set(key, rep); }
|
|
258
274
|
const rows = new Map(); // scenario → { plans: [], ok: bool|null }
|
|
259
275
|
// 分母:--scenarios <文件>(4b 第 0 条从 PRD 枚举的全集,一行一个 id 或 JSON 数组);不给则退回"计划里提到的"并明示
|
|
260
276
|
const scenFile = argOf('--scenarios', walksDir ? join(walksDir, 'scenarios.txt') : null);
|
|
@@ -267,16 +283,14 @@ if (cmd === 'init') {
|
|
|
267
283
|
for (const pf of plans) {
|
|
268
284
|
let plan; try { plan = JSON.parse(readFileSync(join(walksDir, pf), 'utf8')); } catch { continue; }
|
|
269
285
|
const isFlow = plan.kind !== 'skeleton' && (plan.steps || []).some(st => INTERACT.has(st.op));
|
|
270
|
-
const
|
|
271
|
-
let rep0 = null; try { rep0 = existsSync(rf0) ? JSON.parse(readFileSync(rf0, 'utf8')) : null; } catch { rep0 = null; }
|
|
286
|
+
const rep0 = byPlan.get(pf) || null;
|
|
272
287
|
if (rep0 && rep0.seen) { for (const t of rep0.seen.texts || []) seenTexts.add(t); for (const t of rep0.seen.ids || []) seenIds.add(t); }
|
|
273
|
-
if (rep0 && rep0.failStreak >=
|
|
288
|
+
if (rep0 && rep0.failStreak >= 3) streakWarn.push(`${pf.replace(/\.json$/, '')}×${rep0.failStreak}`);
|
|
274
289
|
if (isFlow) { flows++; if (rep0) { flowsRan++; if (rep0.ok) flowsOk++; } } else skeletons++;
|
|
275
290
|
const scen = Array.isArray(plan.scenarios) ? plan.scenarios.map(String) : [];
|
|
276
291
|
if (!scen.length) continue;
|
|
277
292
|
planned++;
|
|
278
|
-
const
|
|
279
|
-
let rep = null; try { rep = existsSync(rf) ? JSON.parse(readFileSync(rf, 'utf8')) : null; } catch { rep = null; }
|
|
293
|
+
const rep = rep0;
|
|
280
294
|
if (rep) { ran++; if (rep.ok) okPlans++; }
|
|
281
295
|
const noChange = rep && rep.tapsNoChange ? rep.tapsNoChange : 0;
|
|
282
296
|
for (const sid of scen) { const r = rows.get(sid) || { plans: [], ok: null }; r.plans.push(pf.replace(/\.json$/, '')); if (rep) r.ok = r.ok === false ? false : (rep.ok && !noChange); rows.set(sid, r); }
|
|
@@ -288,7 +302,7 @@ if (cmd === 'init') {
|
|
|
288
302
|
for (const i of ids) { const r = rows.get(i); lines.push(`| ${i} | ${!r.plans.length ? '未列入计划' : r.ok === null ? '未走' : r.ok ? '通过' : '未过(步失败或未预期的无变化点击)'} | ${r.plans.join(' ')} |`); }
|
|
289
303
|
if (!ids.length) lines.push('', `⚠ --walks 目录里没有带 scenarios 字段的计划——有 PRD 时每份流计划须写 "scenarios":["S1","S2"]`);
|
|
290
304
|
// 计划形态账:骨架计划只证明"可达+出现率",不算走查;流计划才算
|
|
291
|
-
lines.push('', `走查形态:流计划 ${flows} 份(已跑 ${flowsRan}、全过 ${flowsOk})· 骨架计划 ${skeletons} 份(直达/存活/出现率,不计"已走")${flows === 0 ? ' ⚠ 没有任何流计划——4b 没有验证过任何操作后的可观察结果' : ''}${streakWarn.length ? ` ⚠ 连续失败≥2 的计划:${streakWarn.join(' ')}
|
|
305
|
+
lines.push('', `走查形态:流计划 ${flows} 份(已跑 ${flowsRan}、全过 ${flowsOk})· 骨架计划 ${skeletons} 份(直达/存活/出现率,不计"已走")${flows === 0 ? ' ⚠ 没有任何流计划——4b 没有验证过任何操作后的可观察结果' : ''}${streakWarn.length ? ` ⚠ 连续失败≥2 的计划:${streakWarn.join(' ')}(每条流最多 2 次复验,超过即记 failed 交阶段五)` : ''}`);
|
|
292
306
|
// 真机可见性:静态 passed 的 ui 断言,其字面量是否在任一走查 dump 里出现过(seen = walk 报告全程见过的 text/id 并集)
|
|
293
307
|
if (seenTexts.size || seenIds.size) {
|
|
294
308
|
const nz = (x) => String(x).normalize('NFKC').toLowerCase();
|