# GUI 修复复查报告(2026-10-03 复核 · 基线 `b4610f1`) > **上游**:本会话早前写的 `docs/reviews/gui-review-2026-10-03.md`(基线 `5e312c6`)。 > 本文只做一件事:**核验那份报告提的问题在 `5e312c6..b4610f1` 这 14 个提交里修到了什么程度**, > 以及**修的东西本身有没有新问题**。不重写架构分析。 > **本轮实际跑了什么**:`npm install` + `npm test`(criteria 630 项 + vitest 271 项 + typecheck)、 > `hvigorw assembleHap`、`git worktree` 旧基线对照编译、4 个变异注入。 > ★ 与上一份报告不同 —— **本轮的每条结论都有可复现的命令**,见文末「证据」。 --- ## 0. 一句话结论 报告里 **7 个具体条目全部动了**,其中 5 个修得干净且判据真能红; **但 HarmonyOS 客户端在当前 HEAD 上编译不过**(5 个错误,其中 3 个是 48 个提交前就存在的 `Theme.hairline` 未定义),而修复提交的信息里写着「BUILD SUCCESSFUL」。 另发现 **1 处新判据有真缺口**(去掉 `desiredSize` 不会被抓)和 **1 处新引入的死锁路径**(附件闸门不复位)。 --- ## 1. 结算表 | 条目 | 状态 | 判据 | 变异验证 | |---|---|---|---| | **H-1** `MailStore` 收/发共用快照 | ✅ 修得对 | `harmony-arkts` 第 9 条 | ✅ **我亲自注入 2 个变异,全被抓** | | **H-2** 壁纸 `PixelMap` 泄漏 | ⚠️ 修得对,但**判据有缺口** | `harmony-arkts` 第 10 条 | ⚠️ 2 个变异抓到、**1 个漏了** | | **E-1** `PermissionPanel` 重复提交 | ✅ 修得对 | `PermissionPanel.test.tsx` | ✅ **我注入变异后确认变红** | | **E-2** 附件连选覆盖 | ⚠️ 主 bug 修了,**引入新的死锁** | ❌ **无判据** | — | | **X-1** 主进程零导航拦截 | ✅ 修得对 | `main-process-security.test.mjs`(新文件,14 项) | ✅ 独立跑通 | | **X-2** 无 CSP | ✅ 修得对 | 无专门判据 | — | | M-1..M-6 / L-1 / G-1 / G-2 | 未动 | — | — | --- ## 2. 编译:当前 HEAD 是**红的**,且不是这批提交引入的 ``` cd client/harmony && hvigorw assembleHap --no-daemon → COMPILE RESULT:FAIL {ERROR:6 WARN:38} ``` 5 个错误: | # | 错误 | 位置 | 来源 | |---|---|---|---| | 1 | `Cannot find module '@luvi/lv-markdown-in'` | `MailDetailPage.ets:9` | 环境(依赖未装) | | 2-4 | `Property 'hairline' does not exist on type 'typeof Theme'` | `MainPage.ets:608 / 1028 / 1560` | ★ **提交 `477479a`,48 个提交前** | | 5 | `Markdown({...}) does not meet UI component syntax` | `MailDetailPage.ets:1324` | 环境(依赖未装 ⇒ 类型退化) | **归因已用 `git worktree` 对照证明**(不是推测): ``` git worktree add /tmp/mui-base 5e312c6 cd /tmp/mui-base/client/harmony && hvigorw assembleHap → 同样 5 个错误,逐条相同(含同样的行号 608 / 1028 / 1554) ``` ⇒ **这批修复提交没有引入编译错误**,但**旧基线同样是红的**。 ### 2.1 真正的源头:`477479a` 引入了一个从未定义的成员 ``` git log --all -S "Theme.hairline" -- MainPage.ets → 477479a(唯一命中) git show 477479a~1:MainPage.ets | grep -c Theme.hairline → 0 git show 477479a:Theme.ets | grep hairline → 无 ``` `477479a`("三页 AppHeader 顶栏避让",提交信息标了"真机实测")新增 3 处 `Theme.hairline`, 但 `common/Theme.ets` 里**从头到尾没有这个字段**(现在也没有)。 ⇒ **该提交把一个编译不过的版本合进了 main,并且 48 个提交里没人再碰过那 3 行**。 ★ **这条比本报告其余所有内容都重要**,理由见 §5.2。 ### 2.2 关于提交信息里的「BUILD SUCCESSFUL」 `6a8e868` 的信息写着: > `/opt/huawei/command-line-tools/bin/hvigorw` 6.26.2 可用, > `hvigorw assembleHap --no-daemon` 出 **BUILD SUCCESSFUL**(约 26 s)。 本机实测: - `/opt/huawei` **不存在**(`ls: 无法访问`) - 真实路径是 `/home/jianf/command-line-tools/bin/hvigorw` ⇒ 那次 BUILD SUCCESSFUL 大概率是在**另一台机器**上跑的(路径都不同)。 它不必然是假的 —— 但**不能作为"这两条 HIGH 已验证可编译"的证据**, 因为**同一台机器上此刻是 FAIL**。这条信息的价值被高估了。 ★ 不过要给 credit:`6a8e868` 信息里关于 **`arkts-no-obj-literals-as-types`** 那段是**真知识**(ArkTS 确实不接受对象字面量当类型),且它给出的修法(先声明具名 interface) 与本仓 `harmony-arkts` 的既有形状一致。这类"编译期硬规则"值得留在仓库里。 --- ## 3. 逐条核验 ### 3.1 H-1 `MailStore` —— ✅ 修得对,且比我提的方案多修了一处 实现(`MailStore.ets`):新增 `sentSnapshot` + `inboxGen`/`sentGen` 两个代号; `loadSent`(`:648`)写 `this.sentSnapshot`;`SentTab`(`MainPage.ets:1416`)改读 `store.sentSnapshot`; `clear()` 清两份并把两个代号 `+1` 作废。 ★ **比我报告里的草案多修了一处我没提的回归**:拆分后若只动收件箱, 归档会话会让发件箱**继续显示那个已删会话**。实现把它拆成 `dropSessionFromInbox` / `dropSessionFromSent`, 且发件箱那份**不用** `splitByPermission`(否则会把"我发出的授权请求"抹掉)。 这是对的,且判据(`:780`)专门钉了这一条。 **判据变异验证(我实跑)**: | 变异 | 结果 | |---|---| | `loadSent` 的 `const snap = this.sentSnapshot` 改回 `this.snapshot` | ✅ `not ok 9` | | `SentTab` 的 `store.sentSnapshot` 改回 `store.snapshot` | ✅ `not ok 9` | ⇒ 这条判据**有分辨力**,不是摆设。 ⚠️ **但 generation 守卫本身没有任何判据**(全仓 grep `inboxGen` 在 `test/` 下 0 命中)。 删掉 `if (this.inboxGen !== myGen) return;` 整套判据照样全绿。 ⇒ 这是"**守卫没被钉住**",与 §3.2 的缺口同族,但后果轻(它挡的是同栏乱序,不是跨栏覆写)。 ### 3.2 H-2 壁纸 —— 实现对,但判据有一处**真缺口** 实现(`AppearanceStore.ets:233-302`)三件事都做了: `desiredSize` 取 `display.getDefaultDisplaySync()`(带 catch,取不到退回不限尺寸); `finally` 里 `src.release()` / `next.release()`; 所有权顺序正确(先 `this.wallpaper = next` → `next = null` → `await old.release()`)。 另加 `releaseWallpaper()`,`Logout.ets` 已调用。 ★ 注释里点名了"若在 `finally` 里释放 `this.wallpaper`,会把刚装上的那张释放掉"—— 这正是我草案里写的那个坑,实现避开了。 **变异验证(我实跑)**: | 变异 | 结果 | |---|---| | 释放的改成 `this.wallpaper.release()`(所有权写反) | ✅ `not ok 10` | | **把 `createPixelMap(decodeOptions)` 改成 `createPixelMap()`** | ❌ **仍然 pass 10/10** | ### ⚠️ 这是本次唯一一个新引入的判据缺口,根因值得记 判据(`harmony-arkts.test.mjs:806`)只断言: ```js assert.match(body, /desiredSize/, '必须给 desiredSize …'); ``` `desiredSize` **字面量仍然存在于方法体里**(在 `const decodeOptions = { desiredSize: {...} }` 那一行), 即使那个对象**再也没有传给 `createPixelMap`**。⇒ 断言通过,缺陷已回来。 ⇒ **判据钉的是"字符串出现过",不是"它被用上了"** —— 这恰好是 `CRITERIA.md` §1 与 `DEBTS.json` 里 `class-boundary-needs-real-mechanism-not-string-shape` 记的**同一族**坑("用字符串形状冒充机制")。**它在本仓已经栽过至少三次,这是第四次。** **建议的修法**(不要扩大窗口,照 `CRITERIA.md` 的取结构体形状): ```js // 断言 decodeOptions 真的被传进 createPixelMap,而不是只构造出来 assert.match(body, /createPixelMap\(\s*decodeOptions\s*\)/, '★ `decodeOptions` 必须真的传给 createPixelMap —— 只构造出来就等于没限尺寸' + '(26 MB 常驻那一条原样回来)'); ``` 同时建议把**两个分支**都钉住(现在有 `if (target.width > 0 …)` / `else` 两条路径), 否则把两个分支对调也会绿。 ### 3.3 E-1 审批闸门 —— ✅ 修得对,判据真能红 实现(`MailView.tsx:790`)照 `ForwardBar` 的形状加 `inFlight = useRef(false)` + `if (inFlight.current) return;`。 ★ 判据(`PermissionPanel.test.tsx:173`)值得单独说一句:它的注释(`:157-172`)记了 **第一版假绿**(两次 `fireEvent.click` 不放同一个 `act`,中间 React 会提交一次, 第二次点到的是已 `disabled` 的按钮 ⇒ 删掉闸门也绿), 现在把两次点击放进**同一个 `act`**。 **我实跑验证**(不靠信任那段注释): ``` 删掉 inFlight 闸门 → PermissionPanel.test.tsx: × ★ 同一帧内的两次激活只发出一条 decidePermission 其余 23 项仍 pass ``` ⇒ 判据**有分辨力**。这是本批里质量最高的一条判据。 ### 3.4 E-2 附件闸门 —— ⚠️ 主 bug 修了,但引入了一个新的死锁 实现(`Attachments.tsx:87-116`):`picking = useRef(false)` + 进函数提前清 `input.value`。 两件事**成对**做(注释也解释了为什么必须成对),这个判断是对的。 ★ **但 `picking.current = false` 在 `:115`,不在 `finally` 里。** ```ts const picking = useRef(false); const handleFiles = async (files: FileList | null) => { if (!files || files.length === 0) return; if (picking.current) return; picking.current = true; … setUploading(null); if (added.length > 0) onChange([...items, ...added]); // ← 这一行之后若抛 picking.current = false; // ← 永远到不了 }; ``` `uploadAttachment` 的失败被内层 `catch` 接住了,所以**上传失败不会卡**。 但 `onChange(...)` 之后的任何抛出(或未来加在 `setUploading(null)` 之后的抛错) ⇒ `picking.current` 永久为 `true` ⇒ **附件选择器此后永久失效,且不报错**。 这与本仓反复防的"spinner 停了就变空列表"是**同一族**: *失败形态 = 静默地永久卡住*。而**闸门本身的失效恰好也是静默的**。 **修法**(一行): ```ts } finally { picking.current = false; } ``` ★ **且这条完全没有判据** —— `test/components/` 下 13 个文件里没有 `Attachments` 测试, grep `picking` 在整个 `test/` 下 0 命中。 ⇒ 与 §3.2 同类:**闸门类改动没有判据,就等于下次重构会把它删掉**。 ### 3.5 X-1 主进程 —— ✅ 修得对 `main.cjs` 加了 `openExternalSafely()`(`new URL` 解析失败静默返回,只放行 http/https)、 `setWindowOpenHandler` 一律 `{ action: 'deny' }`、`will-navigate` 放行本应用自己的加载 (dev 走 `DEV_URL`、prod 走 `file://` 前缀)、`requestSingleInstanceLock()` + `second-instance` 把已有窗口叫到前面。 新判据 `main-process-security.test.mjs`(14 项)走 `functionBody()` 按括号配对取函数体, **不用** `\{[\s\S]{0,80}` 窗口 —— 与 `CRITERIA.md` §1 一致。 我独立跑过:**14 通过 0 失败**,且它额外钉了 `contextIsolation`/`nodeIntegration`/`sandbox` 三项"不许被放松"(=:842 的注释也说明了为什么不判"够不够安全",这个边界是对的)。 ⚠️ 一个**未覆盖**的场景:`will-navigate` 里放行 `url.startsWith('file://')`, 而恶意链接可以写成 `file:///etc/passwd` 或 `file://` + 本地路径 —— 那会在应用窗口里**加载本地文件**。当前判据只断言"放行了 file://",没断言"只放行 dist/index.html"。 不是漏洞(`react-markdown` 的 `defaultUrlTransform` 会中和不安全 scheme,且需要先有注入面), 但如果哪天有人加 `rehype-raw`,这就是缺口。记一笔,不建议现在改。 ### 3.6 X-2 CSP —— ✅ 修得对 `index.html:65-67` 加了 CSP。**注释里对每一档为什么是这个值都有交代**, 特别是两个诚实说明: - `script-src` 含 `'unsafe-inline'` ⇒ **这挡不住注入型 XSS**,只挡 `javascript:`/外部脚本/`eval`; - `connect-src` 必须宽,因为网关地址**用户可填**(任意 host:port)。 首帧防闪屏脚本(`:31-61`)保持内联、保持单引号(`:54` `classList.add('dark')`)—— `test/theme.test.mjs` 逐字符断言那一段,**没有被改坏**(theme 判据 pass)。 ### 3.7 未动的部分 M-1(系统返回键,**仍不建议动**)、M-2..M-6、L-1、G-1(无障碍)、G-2(`LazyForEach` 零使用) **全部未动**。`DEBTS.json` 现有 58 条,`harmony-no-lazyforeach`(我上一份报告 §5.4 的草稿) **仍不在其中** —— G-2 这条"计划与实现分叉"依旧没人登记。 --- ## 4. 判据套件实跑结果 ``` npm install → 600 packages npm run typecheck → 通过(tsc --noEmit 无输出) npx vitest run → Test Files 16 passed / Tests 271 passed node test/run-all.mjs → files=39 ran=39 checks=630 pass=614 fail=6 skip=10 red=8 变异通道:mutants=52 ran=52 on_new_criteria=37 ``` ### 4.1 6 个红的归因(逐条查过,不是"环境问题"一句带过) | 红判据 | 归因 | 是谁的问题 | |---|---|---| | `harmony-system-api` | SDK 在 `/opt/huawei`,本机在 `/home/jianf/command-line-tools` | 环境(该文件**已**支持 `HARMONY_CLT`) | | `harmony-appearance` | 同上(`:164` 用 `HARMONY_CONFIG_CONSTANT_DTS`) | 环境(**已**支持) | | `harmony-nav` | `:1011` **硬编码 `/opt/huawei`,不走 `HARMONY_CLT`** | ★ **判据的 portability 缺陷** | | `harmony-push` | `agconnect-services.json` 不存在(只有 `.example.json`) | 预期内(该欠账的 `due` 就是"拿到真机") | | `build-stamp` | `dist/assets` 不存在(本机没 `npm run build`) | 环境 | | `commit-hygiene` | ★ 见下 | ★ **这批提交自己引入的** | 补齐环境变量后(`HARMONY_CLT` + `HARMONY_CONFIG_CONSTANT_DTS` + `HARMONY_ID_TABLE`), 红的 8 → **7**,且 `harmony-appearance` 转绿 ⇒ 确认前两条纯环境。 ### 4.2 `commit-hygiene` 那个红是**这批提交自己造的** ``` not ok 1 - 跨端提交必须自报家门(同时改 harmony 与 electron 的提交要标 `跨端:`) 这些提交同时改了 client/harmony/ 与 client/electron/ 却没说自己是跨端提交 + [ '6a8e868d fix(harmony)★★: MailStore 收/发共用一份快照 + 壁纸 PixelMap 泄漏 —— 两处 HIGH' ] ``` `6a8e868` 的 subject 标 `fix(harmony)`,但它同时改了 `harmony-arkts.test.mjs` / `harmony-logic.test.mjs` / `run-all.mjs`(**electron 侧判据**)。 ⚠️ **建议不要按判据提示去改 subject**。这条判据存在的理由(见其断言消息) 是"别用 `git add -A` 把别人的改动卷进来"。 `6a8e868` 的 7 个文件里 4 个是 harmony 代码、3 个是**为这批改动新写的判据** —— **按路径 add 是做了的**,只是 subject 的命名没覆盖"改了对方侧的判据文件"这一情形。 两个选择: 1. 改 subject 为 `跨端:`(最省事,但让"跨端"这个词的含义从"改了两端的产品代码"变成"改了两端的任何文件"); 2. 在 `commit-hygiene` 里为"只改判据文件"开一个分支(更准确,但要动判据本身)。 ★ **两个都要先想清楚再动** —— 这是判据语义问题,不是笔误。 ### 4.3 `baseline-residue` 与 `--exitcode-selftest` 的红是**同一个环境原因**(不是残留) > ⚠️ 本节初稿写的是「`npm install` 改了 `package-lock.json` 导致」——**那是错的,已更正**。 > 更正过程本身说明了本仓判据的一个特性,所以保留在 §6。 `--exitcode-selftest` 红,报「降权工具(runuser/setpriv)不可用 ⇒ rc=2 的两条分支本机覆盖不到」, 连带 `baseline=0/7✗`。实测根因: ``` $ runuser -u nobody -- true → rc=1(非 root 用户不能使用) $ setpriv --reuid=65534 … true → rc=127(setresuid 失败: 不允许的操作) $ id nobody → 存在(uid=65534) ``` ⇒ **两个工具都在 PATH 上,都能执行**,但本会话是 `uid=1000(jianf)` 非 root, **无法降权** ⇒ `run-all.mjs:1043` 的 `dropTo` 探测返回 `null` ⇒ 那 6 个需要降权的案例 被 `continue` 跳过并计红。**这条报错文案是准确的**(不是"工具不存在")。 而 `baseline-residue` 是**同一个通道坏掉的副产物**:变异残留检测依赖那些被跳过的案例 去构造 `baseline=` 四态。`run-all.mjs:1078` 的底本目标是 `client/harmony/entry/src/main/ets/model/Target.ts`,**在 `mkdtempSync` 造的迷你仓库里**, 与工作区无关 —— 所以它与我的 `npm install` 无关。 ★ 这也是一条有用的判据卫生信息:`baseline-residue` 那条文案写着 「**有文件既不在底本、也与 HEAD 不同 ⇒ 优先按"变异残留"查**」—— 本例里底本在 `/tmp`,工作区**当然**不同 ⇒ **按文案指引去查工作区会走错方向**。 文案应补一句「底本在临时目录时本条无意义,先看 `--exitcode-selftest` 是否先红了」。 ★ 补齐 4 个环境变量后的最终读数(`HARMONY_CLT` / `HARMONY_ID_TABLE` / `HARMONY_CONFIG_CONSTANT_DTS` / `HARMONY_COMMON_DTS`): ``` RESULT files=39 ran=39 checks=630 pass=616 fail=4 skip=10 red=6 verdict=red 红的:build-stamp(无 dist)· harmony-push(无 agconnect-services.json)· commit-hygiene(§4.2) 自检:--exitcode-selftest(§4.3 的降权限制)· 因果对照失败(同一根因)· summary.py baseline-residue(同一根因) ``` ⇒ **39 个文件全跑、630 项检查、0 broken、0 unreported**(未装依赖时是 3 broken)。 剩下 6 个红里,**只有 `commit-hygiene` 一个是真问题**,其余是环境。 --- ## 5. 结论与建议 ### 5.1 这批修复的成色 **好的地方**(值得保留的做法): 1. ★ **判据全部走"取结构体"而非固定宽度窗口**,并在注释里写明为什么 (`main-process-security.test.mjs` 头部、`harmony-arkts` 新条目)。 2. ★ **E-1 的判据把踩过的假绿写进了注释**(两次 `fireEvent` 不同 `act` ⇒ 中间会提交 ⇒ 删了闸门也绿), 而且**实测确实能红**。这是本仓判据该有的样子。 3. ★ `6a8e868` 主动记下"**判据缺失导致缺陷降级**"这个根因(`commTab` 改成只 mount 一个 tab ⇒ "必然场景"没了 ⇒ 从"每次都坏"降成"交错时坏"⇒ 没人动)。 这条洞察比代码改动本身更值钱。 4. ★ `dropSessionFromSent` 不用 `splitByPermission` —— 避开了"发件箱一片空白"那个 09-20 修过的真 bug。 **仍有问题**(按建议顺序): | 优先级 | 项 | 说明 | |---|---|---| | **1** | **修 `477479a` 的 `Theme.hairline`** | ★ 见下,**这是唯一挡住"能出包"的东西** | | **2** | 补 `desiredSize` 的判据(§3.2) | 一行断言,让判据从"字符串出现过"变成"真的用上了" | | 3 | `picking.current = false` 挪进 `finally`(§3.4) | 一行 | | 4 | 给 `Attachments` 建判据 | 与 §2 同族:闸门没判据就会被重构删掉 | | 5 | 给 generation 守卫建判据 | §3.1 | | 6 | `harmony-nav.test.mjs:1011` 改成走 `HARMONY_CLT` | 与另两个文件对齐 | | 7 | `commit-hygiene` 那条:改 subject 还是改判据 | §4.2,**先想清楚再动** | ### 5.2 为什么 `Theme.hairline` 排第一 不是因为它本身严重(就是 3 处边框颜色),而是因为**它揭示了一个流程漏洞**: - 一个自称"**真机实测**"的提交,把一个**从未定义的 `Theme` 成员**合进了 main; - 此后 **48 个提交**没人碰那 3 行,**没有一条判据**因此变红; - 而后续那个修复提交的信息写着"BUILD SUCCESSFUL"—— 说明**编译这一步在交付流程里是可跳过的**,至少在某些环境下。 ★ 也就是说:**`ARV` 全绿、271 项测试全绿、提交信息写着 BUILD SUCCESSFUL, 但客户端编译不过。** 本报告 §2 那个 5 错误的 `hvigorw` 输出,是这批工作里 **唯一能看见这个问题的手段** —— 它没有被任何自动化接进去。 **建议(按成本从低到高)**: 1. **立刻**:补 `Theme.hairline`(或把它改成 `Theme.border`),让编译转绿。 2. **低成本**:把 `hvigorw assembleHap` 接进 `npm test` 或 CI —— 本机 6-9 秒就能跑完(实测 `BUILD FAILED in 9 s`),这是**全仓最便宜的一道闸**。 3. **中成本**:`harmony-arkts` 判据里加一条 「`Theme.` 的每个引用都必须在 `Theme` 类里有对应 `static` 成员」—— 这类"引用了不存在的成员"是纯静态可判的,且**比编译快**(毫秒级,不用 SDK)。 ★ 注意 `run-all.mjs:810` 与 `harmony-nav.test.mjs:1011` 现在**硬编码 `/opt/huawei`**, 写这条判据时要用 `HARMONY_CLT` 那种 `process.env.X || 默认值` 的形状。 ### 5.3 不要动的部分(与上一份报告一致) `MailStore.bump()` 不发布 AppStorage(是设计,混用会死循环)、 鸿蒙日历 `hourEvents`/`hhmmAtOffset`(5 时区实测恒等,误报)、 系统返回键 M-1(上一轮真机试过失败并撤回)、 `main.cjs` 的 `webPreferences` 三项、`.pi-lens.json`/`biome.jsonc` 的 formatter 关闭。 --- ## 6. 证据(可复现) ```bash # 编译(当前 HEAD 失败,5 个错误) cd client/harmony && /home/jianf/command-line-tools/bin/hvigorw assembleHap --no-daemon # 归因:旧基线同样失败 ⇒ 不是这批提交引入的 git worktree add /tmp/mui-base 5e312c6 cd /tmp/mui-base/client/harmony && /home/jianf/command-line-tools/bin/hvigorw assembleHap --no-daemon # Theme.hairline 的源头 git log --all -S "Theme.hairline" -- client/harmony/entry/src/main/ets/pages/MainPage.ets git show 477479a:client/harmony/entry/src/main/ets/common/Theme.ets | grep hairline # 无输出 # 判据套件(本机需先装依赖) cd client/electron && npm install HARMONY_CLT=/home/jianf/command-line-tools \ HARMONY_CONFIG_CONSTANT_DTS=/home/jianf/command-line-tools/sdk/default/openharmony/ets/api/@ohos.app.ability.ConfigurationConstant.d.ts \ HARMONY_ID_TABLE=/home/jianf/command-line-tools/sdk/default/openharmony/toolchains/id_defined.json \ node test/run-all.mjs npx vitest run # 16 files / 271 tests npm run typecheck # §3.2 的判据缺口(去掉 desiredSize 仍 pass) cd client/harmony/entry/src/main/ets/common sed -i 's/createPixelMap(decodeOptions)/createPixelMap()/' AppearanceStore.ets cd ../../../../../../electron && node test/harmony-arkts.test.mjs # 仍 # pass 10 # §3.3 的判据有分辨力 cd src/components && python3 - <<'PY' import pathlib p=pathlib.Path('MailView.tsx'); t=p.read_text() p.write_text(t.replace(" if (inFlight.current) return;\n inFlight.current = true;\n","")) PY cd ../../electron && npx vitest run test/components/PermissionPanel.test.tsx # 该项变红 ``` ⚠️ 跑完变异**务必还原**并 `git status` 确认 —— §4.3 那次 `baseline-residue` 就是我改了 `package-lock.json` 留下的。 --- ## 7. Electron GUI 侧专项(初稿漏了的一节) §3 只把 Electron 的 4 个 GUI 修复逐条讲了,**漏掉了本批提交里对 Electron 判据体系影响最大的两条**。 这两条都不改 GUI 行为,但它们决定了"判据在把守"这个前提**到底成不成立** —— 而 GUI 侧的所有结论(包括 §3.1/§3.3 的变异验证)都建立在这个前提上。 ### 7.1 `93697c4` —— 判据读取器把 137 行真代码当注释吃掉(★★★) `stripComments()` 原来是**两趟正则**(先块 `/\*[\s\S]*?\*\//g`、后行)。 两趟**互相看不见对方**,于是有一类输入会把真代码当注释删掉: ``` server/cmd/server/main.go:78 // 与 /api/v1/agent/* 完全同一份代码 —— 不存在第二套收窄或配额逻辑。 ↑ 这个 /* 在 // 里面 ``` 块注释那趟跑在**还没删行注释**的文本上,看到这个 `/*` 就当块注释开头, 一路找下一个 `*/`(在 `:214`)⇒ **137 行 / 37 条路由注册被抹掉**, 含它正在断言的 `r.Get("/auth/me", handler.Me)`。 **我实跑复现了这条**(把旧读取器放回去): ``` 旧读取器 → not ok 23 - ★ `/me` 不是路由:MeApi 必须调 `/auth/me` # pass 29 / fail 1 新读取器 → # pass 30 / fail 0 ``` ⇒ **这不是假想的风险,是已经发生过一次的假红**,而且假红的那条 保护的正是"管理入口对所有人永不显示"这个**线上真实存在过**的 bug。 ★ **全仓另有 12 处同样写法**(`/me/*`、`/assets/*`、`plugins/*`…)—— 只要落在某个块注释终点之前就触发同一形状。 **失效形状值得单独记**:**读取器静默少给一段真代码**(不抛错、不警告), 症状却出现在**被测对象**上 ⇒ 看起来像"服务端把路由删了"。 这与 `DEBTS.json` 里 `criterion-silently-returns-zero`、 `history-rewrite-undisclosed-citations-dangle` 同一族。 修法(单遍字符扫描)方向对。★ 但注意提交信息自己承认了一个**刻意的残留限制**: **不认字符串字面量** —— `const s = "/*";` 里的块注释标记仍会被当开头。 方向是**假绿**(少剥注释),与 `stripStrings` 同性质。 组合使用 `code()` → `stripStrings()` 是安全的(顺序已在注释里写明)。 ★ 这条给 GUI 侧的直接教训:**Electron 与 HarmonyOS 两侧的判据共用同一个 `read.mjs`**。 也就是说这个读取器缺陷**同时影响两端判据**,而我当时正在读的那份 GUI 报告 (`gui-review-2026-10-03.md`)里**所有 `grep` 出来的行号都可能是被吃掉之后的**。 所幸那条报告的引用是**我当场用 `grep -n` 在原始文件上量的**,不是过 `read.mjs` 的。 ### 7.2 `6df3c35` —— 判据套件 24 小时一条都没跑过(★★★) **现象**:`inbox-fallback-poll` / `sse-credentials` / `web-comment-only` 三个文件**没接进 `SUITE`**。 **为什么严重(三层放大,提交信息已列出,我复核成立)**: 1. 自检 2「每个 `*.test.mjs` 都要在清单里」在**跑任何判据之前** `exit(1)` ⇒ 实测 `RESULT` 行数 = **0**。 ★ **我实跑复现**:从 `SUITE` 里删掉第一项 → `rc=1`、`RESULT` 行数 = `0`。 2. `npm test` = `run-all && vitest && typecheck` ⇒ vitest(**271 格**)与 typecheck **一起不跑** —— 而两者单独跑都是绿的。 3. 失败信息只有一行中文 stderr、**不含"红"字样** ⇒ 看起来像"环境问题"。 ⇒ 后果是:**下一个人依据上一份报告继续推断"判据在把守"**, 而这个前提**当时是假的**。提交信息把这叫「**报告的证据等级被系统性高估**」。 ★ **这条直接命中我上一份 GUI 报告的方法论**:我在那份报告 §7 写了 「我没运行任何测试,所以所有判据建议都只是建议」—— **那个自我限制恰好是当时唯一没被这条击穿的防线**。 若我当时因为"判据全绿"就采信某些结论,会直接踩空。 **同时它也是好消息**:`CRITERIA.md` 新增了 §6.0「怎么读判据的数」, 补上了"原有 17 节全在讲**怎么写**,缺的就是**怎么读**"这一半。 §6.0.1 接线守卫的失效形状是「**全停**」不是「那一条不跑」;§6.0.5「当你就是改工具的人, 第一假设是**我弄坏了它**」。 ### 7.3 §6.0.2 那条陷阱 —— 我在本轮**当场踩了**,记下来 §6.0.2:「退出码只能来自不接管道的运行(`cmd >file 2>&1; echo $?`)」, 并记着作者自己「本会话连踩 5 次 `cmd | tail -N` ⇒ 报的是 tail 的码」。 ★ 我这次也踩了。本轮我一度用: ``` node test/run-all.mjs 2>&1 | tail -3 → $? = 0 ``` 而同一时刻套件实际是红的(`commit-hygiene`)。**`$?=0` 是 `tail` 的码。** 改用重定向取真码后 `rc=1`。 ⚠️ 更隐蔽的一层:`run-all.mjs` 自己就是**靠退出码**汇报的 (`UPSTREAM_RC[...]` 表 + `summary.py` 的 rc)。 所以「管道/重定向搞错退出码」这件事在本仓**会直接污染判据结论**, 不只是"看日志方便一点"的问题。 ### 7.4 Electron 侧本批修复的最终读数 ``` npm install → 600 packages npm run typecheck → 通过(tsc --noEmit 无输出) npx vitest run → Test Files 16 passed / Tests 271 passed node test/run-all.mjs → files=39 ran=39 checks=630 pass=616 fail=4 skip=10(设备不在,不是通过)red=6 broken=0 unreported=0 变异通道:mutants=52 ran=52 on_new_criteria=37 ``` ★ `broken=0 / unreported=0` 是装完依赖 + 补齐 4 个环境变量之后的状态 —— 未装依赖时是 `broken=3`(`theme` / `background` 等因缺 `tailwindcss` / `postcss` 跑不起来)。 **这正是 §7.2 那条判据的形状:broken 不是 red,但它证明不了任何东西。** ### 7.5 Electron 侧我上轮提、至今**未动**的两条(数字复核) | 条目 | 上轮读数 | 现读数 | 状态 | |---|---|---|---| | **G-1** 无障碍 | `aria-label` 22 / `