c1465e09ab5b50efca9d8a155369deecd6cff4a1
229 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| c1465e09ab |
跨端: 平板三处真 bug(返回回登录页 / 避让重复叠加 / 联系人卡被裁 10vp)
用户报的「在主页返回为什么会直接回到登陆页」是**原语用错**: LoginPage 用 pushUrl 进 MainPage,路由栈成 [LoginPage, MainPage], 返回自然弹回登录页。而 Logout.ets 早就是 replaceUrl 并写了理由 (「退出后不该还能'返回到已登出的页'」)—— 同一个不变式、相反方向, 只改了一半。三处 pushUrl → replaceUrl,设备实测:返回直接退出 app (前台变 com.huawei.hmos.browser),不再回登录页。 另两处平板(HUAWEI MatePad Pro, 2800x1840, ratio 1.52 ⇒ 宽屏): · 内容列 `this.isWide ? Theme.surface : (bgActive ? 透明 : surface)` —— 宽屏分支把 bgActive 丢掉了 ⇒ 平板上壁纸永远被挡。 · `top: this.isWide ? paneGap : statusBar` —— 宽屏分支把状态栏避让丢掉 ⇒ 页签字压在系统时钟下。 · 侧栏自己也加了一次 topInset,而父 Row 的 padding 已经含状态栏 ⇒ 83+83=166px 空白(用户:「避让有点用力过猛」)。 · 联系人列表项硬写 .height(85)+clip,内容实际要 95vp ⇒ 写信/归档行被裁一半。 判据 harmony-nav 新增「登录/退出必须用同一个原语」并做变异验证 (改回 pushUrl 即红);注册数 18→19,全绿 19/19。 ★ 途中发现的记账缺口:设备判据连续失败时走 noteBusySkip 计数, .tmp/harmony-busy-skips.json 累到 10 后拒绝再当'礼貌跳过'。 清除账本 + 让 app 真在前台后立刻 19/19 —— 说明**不是代码问题**, 是账本把'设备忙'当成了证据。这一点记进 DEBTS。 |
|||
| 110de79401 |
跨端: 三级文字**浅色也一样不够**(12 处 2.85:1)+ 判据不再只验一半
## 一、这是"我自己推的理由"被实测推翻 上一条我把 `textSubtleFor()` 写成**只改深色**,理由是: > 「浅色下这些令牌本来就是为浅底配的,量它们没有信息量」 **那个理由是错的。** 切到浅色主题复扫,立刻抓到 12 处: ``` "用户名 / 显示名 / 角色 / 状态 / 创建时间 / 最后登录" 2.85:1 "已同步" / "37%" / "3px"(外观区) 2.85:1 "这一天没有日程" 2.85:1 ``` `ink rgb(153,153,153)` 压白底 —— 全是 `textSubtle`。 对照 WebUI 浅色(`index.css` `:root`): - `gray-400` = `107 114 128` = **4.83:1** - `gray-500` = `90 98 112` = **6.15:1** ⇒ 系统三级色**两个主题下都不够**。它的语义只是"比二级更淡", **不保证任何可读性下限**;WebUI 那边两套主题都做过适配,这边一档都没有。 修:`textSubtleFor()` 两个主题都返回 `textMuted` (系统二级色,语义就是"次要文字",且**跟随主题** —— 不必再维护两个手写常量、不必登记)。 ## 二、判据从"只验深色"改成"两个主题都验" 原来那条设备判据**在浅色下直接 skip**,skip 的理由正是上面那句错话。 现在两个主题都扫,断言消息里带上主题名(`深色主题下…` / `浅色主题下…`)。 ★ 纪律:**色令牌的可读性是两个主题各自的事,不能只验一半。** "另一半没问题"若没量过,就只是推测。 **变异验证**:把 `textSubtleFor()` 改回"两主题都用 textSubtle" (此时设备正在浅色主题)⇒ **判红**;还原 ⇒ 绿。 ## 三、设备复扫(浅色主题) ``` [通信] 扫 41 段,低对比 0 [日历] 扫 84 段,低对比 0 [联系人] 扫 50 段,低对比 0 [我的] 扫 40 段,低对比 0 ``` **215 段文字全部 ≥3:1。** 两个主题合起来:深色 219 段 + 浅色 215 段,全绿。 ## 四、顺手改掉的 测试名从「深色下短文本必须都读得动」改成「**当前主题**下…」 —— 名字得跟着它实际判的东西走(否则下一个人会以为它只管深色)。 `run-all.mjs` → `checks=515 pass=515 fail=0 skip=0 red=0 broken=0 unreported=0`。 **当前设备主题**:浅色(本轮为验另一半切过去的;服务端 `theme=light`)。 |
|||
| 1da4e15a7b |
跨端: 补齐「我的」页深色可读性(4 页 219 段全 ≥3:1)+ 判据自己漏报的那一类
上一条把通信/日历/联系扫干净了,但**「我的」页没扫到**
(扫描脚本按文案点不到它 —— 那是侧栏**底部的头像按钮**,不是导航项)。
补上后立刻又抓出问题,并把判据自身的**漏报形状**一并修了。
## 一、「我的」页两处真 bug
1. **主题分段按钮 `SettingsPage.ets:834`**:
`.fontColor(... ? Theme.surface : Theme.textPrimary)` —— `surface` 是
**会翻转的面色**,压在 `Theme.accent` 蓝底上,深色下变成近黑。
设备读数:`深色 2.93:1 ink rgb(32,34,36) bg rgb(34,96,228)`。
2. **权限档徽标 `MailDetailPage.ets:391`**:同一个错法(第 13 处)。
3. **账号名 `SettingsPage.ets:1179`**:三元里的裸 `Theme.accent`
压在 `accentSoftFor()` 底上 ⇒ `jianf 2.83:1`。
`AdminUsersPage.ets:386` 同形状(角色徽标)。
## 二、判据自己的漏报形状(比 bug 本身更值得记)
静态防线(`C|会翻转的 Resource 不得当前景色`)**在 bug 存在时是绿的**。
原因是它的提取正则:
/\.fontColor\(Theme\.([A-Za-z0-9_]+)\)/ ← 要求令牌是**唯一实参**
于是**三元里的令牌全被漏掉**:
.fontColor(this.appearanceTheme === t ? Theme.surface : Theme.textPrimary)
改成"在 `fontColor(` 之后的整段实参里找所有 `Theme.X`"之后,
它**立刻报出上面第 1、2 两处**(此前一直绿)。
★ **判据漏报的常见形状是它自己的正则太窄,而不是被测代码太隐蔽。**
这条写成注释留在判据里了。
## 三、我自己犯的批量替换错误(已加判据钉住)
把裸 `Theme.accent` 换成 `accentFor()` 时,**误把 6 处 `backgroundColor` 也换了**。
`accentFor()` 深色给浅蓝 `#80AFF9`,而搭档前景是白色 `accentFg`
⇒ 白字压浅蓝 ≈ **1.4:1**,主按钮文字会彻底看不见。
- 6 处已逐处还原(复核:`grep -c "backgroundColor(Theme.accentFor())"` = 0)。
- 新增判据 **`C2`**:`accentFor / dangerFor / approveFor / warnFgFor / textSubtleFor`
**只能用于前景**,`backgroundColor(Theme.XFor(...))` 直接判红。
(`accentSoftFor` 是例外 —— 它本来就是"面"。)
★ 修法不是"下次小心点":批量替换一定会再犯,**一行判据把它变成不可能**。
## 四、设备复扫(修后)
```
[通信] 扫 42 段,低对比 0
[日历] 扫 85 段,低对比 0
[联系人] 扫 51 段,低对比 0
[我的] 扫 41 段,低对比 0 ← 新增
```
**219 段文字全部 ≥3:1**(本轮全部针对深色)。
## 五、底本
`baseline.sha` 第 5 次重算。重算前**专门复核**了"那 6 处误改有没有残留"
(不只看 `git diff` 非空就放行):`backgroundColor(accentFor)` 计数为 0、
`git diff HEAD~1` 里 backgroundColor 只有 calendar 那一处(有意改动)。
`run-all.mjs` → `checks=515 pass=515 fail=0 skip=0 red=0 broken=0 unreported=0`。
**未验**:浅色主题下的对比度没扫(本轮全部针对深色)。
|
|||
| 4ecddfc2b6 |
跨端: 语义色/三级文字在深色下没提亮(设备扫出 12 处)+ 判据取样法修到第三版
## 一、判据基建先修对,否则是在听噪声
深色可读性扫描的**取样法迭代了三版**,每版都因为**假红**才改的
(记在这里,因为"判据自己错"比"界面错"更费时间):
1. **取中心一个像素** ⇒ 落在笔画之间、读到的是底色 ⇒ 一片 ratio=1.00 假红。
2. **取全局最暗/最亮** ⇒ 会采到**两个不同的东西**:细字的抗锯齿中间色。
实例:segmented「日」报 ink `rgb(32,34,35)` / bg `rgb(98,100,101)` 2.69:1 ——
而截图里它要么蓝底白字、要么深底浅字,两种都远高于 3:1。
**反复核对才确认是判据的错,不是界面的错。**
3. **步长采样 + 众数** ⇒ 仍然假红:周表头「一」报 2.03:1,
而逐像素的真实墨是 `rgb(166,167,167)`(**6.6:1**)——
稀疏网格**整个跳过了笔画**。这不是调参能修好的(字越小越糟)。
4. **逐像素 + 众数=底 + 离底最远者=墨**(最终版)。
理由:一个文字框里**面积最大的一定是底**,而"离底色最远"就是笔画最实那部分。
⚠️ 逐像素是**负担得起的**(整屏已解码在内存,一个框最多 9 万像素)——
**不要再为了省开销退回抽样**,前两版都因此假红。
## 二、真 bug 三批(都是"深色下没提亮")
### ① 语义色前景(12 处 → 修完设备复扫 0)
WebUI 的语义色也是**双通道**,`.dark` 段把 `red/green/amber-700` 换成浅色
(`248/118/249 → 164/219/195` 那组)。鸿蒙只有一个深色值:
- 「归档」按钮 `Theme.danger` #B91C1C 压深面 ⇒ **2.47:1**(联系人页 7 处)
- 「今天」/「日」等 ⇒ **2.38–2.77:1**
加 `dangerDark/approveDark/warnFgDark`(逐字对齐 WebUI `.dark`)
+ `dangerFor()/approveFor()/warnFgFor()` 入口,22 处接入。
### ② 日历工具条整块配色抄错(不是"深色档选错")
对照 WebUI `CalendarView.tsx:313-331`:
| 元素 | WebUI | 鸿蒙(改前) |
|---|---|---|
| 「今天」 | `border-gray-300` + `text-gray-700`(**中性**) | `accentSoft` 底 + `accentStrong` 字 |
| 月/周/日 未选中 | `bg-white` + `text-gray-700` | `accentSoft` 底 |
鸿蒙把**次要的中性按钮**画成了**品牌色按钮** —— 视觉重量跟主操作一样,
三个档看起来"都像选中"。已按 WebUI 改成中性。
### ③ 三级文字在深色下 2.82:1(**71 处**,全局最大的一个)
WebUI 的 `.dark` 段注释逐字写着这个坑:
> 「`gray-400/500`(次要文字)→ **提亮**。深底上的浅色 gray-400
> 只有约 2:1 对比度,远低于 WCAG AA 的 4.5:1 —— **看得见但读不动**。」
鸿蒙用的系统三级色 `ohos_id_color_text_tertiary` 深色下是 `rgb(102,103,105)`
压深面 **2.82–3.05:1**,而它被用在 10–11px 的**小字**上
(时间戳、农历、空态、辅助说明)。
**不换掉系统令牌**("系统拥有的维度"纪律照旧),加 `textSubtleFor()`:
深色下改用二级色(实测 `rgb(166,167,167)` ≈ 7.15:1,
且与 WebUI 的 `gray-500` 同档位)。71 处接入。
**设备复扫(修后)**:通信/日历/联系三页 **176 段文字全部 ≥3:1**。
## 三、欠账与底本
- `baseline.sha` 第 4 次重算。仍**先逐个取证**:
`git diff --quiet HEAD -- <f> && echo STALE || echo INTENTIONAL`
⇒ 三个全是 `INTENTIONAL`(我改的),不是变异残留。
把这条取证命令也写进文件,下次照抄。
- `dangerDark/approveDark/warnFgDark` 已登记进 `SELF_OWNED_COLORS`
+ `Theme.ets` 登记表(各带理由与 WebUI 出处)。
`run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。
**未验**:①「我的」页(底部头像按钮,扫描脚本没找到入口)没扫;
② 浅色下的对比度没扫(本轮全部针对深色)。
|
|||
| 13b557a742 |
跨端: 品牌蓝深色下没提亮(24 处字/图标看不见)+ 补深色可读性设备判据
## 一、真 bug:WebUI 深色下把品牌蓝**提亮**了,这边没有 WebUI 的强调色是**双通道**(`tailwind.config.js` 的 `backgroundColor` /`textColor` 覆盖 + `index.css` 两段定义): | 通道 | 用途 | 浅色 | 深色 | |---|---|---|---| | `--s-blue-600` | **实心按钮底** | `37 99 235` | `37 99 235`(**同值**)| | `--c-blue-600` | 内容/交互的**蓝字与图标** | `37 99 235` | **`128 175 249`** | `index.css:353` 写了理由:主按钮底跟着变「会让主按钮在深色页面上 失去『这是主操作』的视觉重量」;而蓝字必须提亮,否则深底上读不动。 鸿蒙只有一个 `Theme.accent = '#2563EB'` ⇒ 24 处字/图标在深色下 对比度 **2.61:1**(设备实测:管理页返回箭头 `‹`),低于 WCAG 图形下限 3:1。 **取证方式**:在跑着的 WebUI 上用 CDP 读**计算样式**(不是读 CSS 源)—— 浅色 `37 99 235` / 深色 `128 175 249`,实测确认。 ## 二、修法:加前景专用的深色档 + 唯一入口 - `Theme.accentDark = '#80AFF9'`(= WebUI `.dark --c-blue-600`) - `Theme.accentFor(dark?)` 作为**前景**唯一入口 - **当背景的 20 处保持 `Theme.accent` 不动**(跟 WebUI 的 `--s-*` 一致) 24 处 `.fontColor/.iconColor(Theme.accent)` → `Theme.accentFor()`。 ## 三、顺手修掉「转述一层就会漏」这个结构问题 `accentSoftFor(isDark)` 原本的约定是"页面算好深浅色传进来"。 给 `accentFor` 做准备时一数:**8 处**直接用了 `Theme.accentSoft`(没走入口) —— 约定**已经漏了**,而漏掉的症状正是上一轮那个"深色下白底卡片刺眼"。 于是把两个 `For()` 的参数都改成**可选**:`AppStorage` 是 ArkTS 全局键值存储, 静态类可以直接读(原来"拿不到 Context"的理由不成立)。 ⇒ `Theme.isDarkNow()` 成为唯一判断点,调用方不必再各自转述。 ## 四、设备判据:深色可读性**扫一屏** 原来只有悬浮球那一条(单个点)。这个 bug 类一天撞到**两批**(12 处 + 24 处), 逐处写判据追不上 ⇒ 改成把当前页所有小段文字都量一遍对比度。 三个实现要点(第一版全踩了,都写进注释): - **不能只取中心一个像素**:中心多半落在笔画之间 ⇒ 读到的是底色, 报出一片 ratio=1.00 的假红。改成**框内网格扫描取极值**(最亮=底/最暗=墨)。 - **整屏解码一次**:每点 spawn 一次 ffmpeg 太慢 ⇒ 新增 `readPixels()`(157ms 解整屏,比逐点快三个数量级)。 - **只判"有真实墨迹"的框**(`hi.L - lo.L >= 0.02`),否则跳过而不是判红。 实测:修前 1 处低对比(2.61:1),修后 **40 段文字全部 ≥3:1**。 ## 五、判据自身的三个修正 - `accentSoftFor(dark:)` 的签名断言跟着放宽成 `dark?`,并**补上 `accentFor` 的**。 - **孤儿令牌判据从"一跳"改成"走整条链"**:`KEY_IS_DARK` ← `isDarkNow()` ← `accentFor()` ← 24 处页面。只查一跳时它假红 —— ★ **"有没有人用"是可达性问题,不是邻接问题**;加中间层(抽 `For()` 入口) 恰恰是我们鼓励的写法,而旧判据会因此假红。 - 手写色登记表补 `accentDark` 一行理由。 ## 六、`baseline.sha` 重算(先核过不是残留) 三个文件哈希对不上。逐个 `git diff --quiet HEAD -- <f>` 取证: - `AdminUsersPage.ets` / `SettingsPage.ets` —— 本次**有意编辑**; - `api/AppearanceApi.ets` —— **与 HEAD 逐字节相同** ⇒ 底本取完后被**合法改过** (提交 `f811c98`),属 `stale` 不是 `residue`。 按该文件自己那条纪律(「重算必须是一次有记录的动作」)在文件里记了理由。 `run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。 |
|||
| 3123d83979 |
判据: 把「面当字」的防线从死名单改成判矛盾(三次迭代都记下来了)
上一版加的静态防线只 grep `fontColor(Theme.surface)` —— **一个名字**。 那是死名单:以后加了新令牌它不会跟上(本仓已记录过多次这类错法)。 改成从源码推: **条件 A**:值是 `Resource` 且指向 `sys.color.*` ⇒ **跟随系统主题翻转** **条件 B**:既当 `backgroundColor` 又当 `fontColor/iconColor` ⇒ **被当成「面」也被当成「字」** 判决 = **A ∩ B**,不需要维护任何名单。 ## 三次迭代(每次错得很典型,所以都写进注释了) - **① 死名单**(只盯 `surface`):新增令牌不会跟上。 - **② 只要 A**("是翻转的 Resource 就不许当前景色"):**太宽** —— 实测立刻 报出 20+ 处"违规",全是 `textMuted` / `textSubtle` / `textPrimary`。 那些**本来就该**当前景色(语义就是文字色,跟随主题翻转是对的)⇒ 假红一片。 - **④ 只要 B**("矛盾就报"):**也宽** —— `accent` / `danger` / `approve` / `warnFg` 两边都用是正常设计(蓝底白字主按钮 + 白底蓝字返回箭头)。 和 `surface` 的**关键区别**:那些是**饱和的字符串常量**(不翻转、语义是"色"), 而 `surface` 是**中性面 + 跟随主题翻转**(当字色 = 把底板当墨水,深色下必然看不见)。 - **最终 = A ∩ B**:恰好命中 `surface` 这一类,且**不随新增令牌失效**。 允许清单现在是**空表**(修完 `surface` 后没有例外), 并写明「已审过、不需要进这张表的」那几个令牌与理由。 ## 变异验证(两个都做了,第二个是关键) 1. 把一处 `.accentFg` 改回 `.surface` ⇒ **判红**;还原 ⇒ 绿。 2. **造一个判据从没见过的新令牌**(`surfaceAlt`,同样 `Resource` + `sys.color.*`), 让它同时当背景与前景 ⇒ **判红**(报文里点名 `Theme.surfaceAlt`);还原 ⇒ 绿。 —— 这条比 ① 重要:它证明判据**不是靠记住 `surface` 这个名字**在工作。 ## 顺带确认 `pageBg` / `surfaceMuted` / `wallpaperScrim` / `border` / `overlay` 这五个 同类"会翻转的面"**一处都没被当字色用过** —— 这一类现在全清了。 `run-all.mjs` → `checks=513 pass=513 fail=0 skip=0 red=0 broken=0 unreported=0`。 |
|||
| 21132647bc |
跨端: 12 处「深色下字看不见」的真 bug + 判据基建补上「看像素」这一层
## 一、判据基建:本目录终于能**看像素**了 此前只能靠 `dumpLayout` —— 那是**结构化描述**,报的是"组件声明了什么", 不是"屏幕上画成什么样"。两者会分叉,而观感类结论只能在像素上得出来。 新增 `lib/harmony-device.mjs`:`screenshot()` / `pixelAt()` / `hexToRgb()` / `closeColor()`(用 ffmpeg 转 1×1 原始 RGB,不引依赖)。 **它当场证明了它的价值**:`cross-client-theme` 新增的设备判据 用真实像素抓到下面这个 bug —— 静态判据全绿时它藏得好好的。 ## 二、真 bug:**12 处**把 `Theme.surface` 当前景色用 `Theme.surface` 是 `sys.color.ohos_id_color_list_card_bg` —— 一个**跟随系统主题翻转**的 Resource:浅色近白、**深色近黑**。 - 浅色下当白字用**碰巧对**(白字压蓝底) - **深色下字变成黑的**,压在品牌蓝 / danger 红 / warn 琥珀上**几乎看不见** 设备现场:写邮件悬浮球是品牌蓝 `#2563EB`,截图里那个铅笔图标**几乎是隐形的**; 读圆心像素得到 `rgb(32,34,36)`。往左偏 50px 读到底色才见 `rgb(36,99,235)`。 `Theme.accentFg`(`#FFFFFF`)的注释原话就是「品牌底上的文字」—— 为这个场景存在, 却**一处都没用**。 修:12 处 `fontColor/iconColor(Theme.surface)` → `Theme.accentFg` (`MainPage` 10 + `InboxPage` 1 + `SessionsPage` 1)。改完全仓 0 处残留。 另在 `Theme.ets` 给 `surface` / `accentFg` 都补上"能当什么、不能当什么"的注释。 ## 三、判据(两条,都做了变异验证) 1. **设备条**(`cross-client-theme`):读悬浮球像素 —— ① 品牌色**真的画成** `#2563EB`(声明 ≠ 渲染); ② 球上图标与底色 **WCAG 对比度 ≥3:1**(压在上面的东西得看得见)。 把 `.accentFg` 改回 `.surface` ⇒ **判红**;还原 ⇒ 绿。 2. **静态防线**(同文件):全局 grep「`fontColor/iconColor(Theme.surface)`」一处不许有。 设备条只能看一处,而这个错法有 12 处 —— 静态防线管住整类。 ## 四、判据自身踩的三个坑(都写进注释了) - **采样点撞上图标**:第一版取球心,读到 `rgb(32,34,36)`,差点当成"品牌色没渲染"。 截图一看球是蓝的,深色那点是**铅笔图标**。⇒ 往中心左偏 30% 球宽。 - **假设错了 FAB 的位置**:按"屏幕右下角"找(`x1 > 屏宽*0.6`), 实测 `[942,1997]`(`x1=942` vs 阈值 1910)⇒ 永远找不到、**静默跳过**。 原因是列表窗格是**左栏**,球在"左栏的右下角"。⇒ 形状只用站得住的那部分(下半部)。 - **设备判据要自己搭现场**:不加自导航时它**永远跳过**(前面的判据把前台留在管理页), 而那看起来像"功能没了"。加自导航后立刻开始工作并抓到 bug。 ## 五、欠账 - `harmony-maildetail-missing-three` → **count 0(结算)**:三块都做完了 (转发 `b7c5d8b` / 改名建议 `c2f35d1`+`e79a86a` / 往返预算 `ac62daf`)。 如实记着**未验**的那点:预算条的**点击**没在设备上走通 (模拟器顶部 155px 是系统手势区,折叠头部恰在其中)。 - `static-criteria` 5:`cross-client-theme` **升级了一半**,仍留在名单里 —— `.ets` 那半只有悬浮球这一处上了设备,其余令牌仍是静态对齐。 - `debt-visibility` 登记 `cross-client-theme` 1 处边界声明(带出处)。 `run-all.mjs` → `checks=513 pass=513 fail=0 skip=0 red=0 broken=0 unreported=0`; Go 侧 `./internal/repo/...` 通过。 |
|||
| ac62dafde1 |
跨端: 往返预算编辑上线(详情页缺的第三块)+ 修折叠头部点不中的真 bug
审计发现详情页缺三块功能之三:**往返预算**(WebUI `BudgetEditor`)。
`MailApi.setBudget` 早就有了,缺的是 `getBudget` 与 UI。
## 一、预算条(对齐 WebUI,两态)
显示态是徽标(`已用/上限 来回` / 不限时「预算不限」/ 用尽时标红),
点它进入编辑态:输入框 + 保存 + 重置 + 取消。
★ 「用尽」的判定**走服务端的 `remaining`**,不自己拿 `used >= max` 算:
不限时服务端给 `remaining: -1`,那个式子在那种情形下会算出"已用尽"。
两端都得用服务端的口径。
★ 「保存」与「重置计数」走**同一条 PUT**(服务端支持两者同时给,
它的注释原话:「加到 20 并从头算」是一次很自然的操作,
拆成两个请求只会让前端多一次往返)。
**契约实测**(不是形态检查):
GET → {"max_rounds":0,"used_rounds":1,"remaining":-1,"unlimited":true}
PUT {"max_rounds":7,"reset":false} → {"max_rounds":7,"remaining":6,"unlimited":false}
PUT {"max_rounds":0,"reset":false} → {"remaining":-1,"unlimited":true} ← 0 = 不限
## 二、真 bug:折叠头部的展开区只有 **14px** 高
设备实测 dump:`Row [1277,112][3122,126]` —— 可点区高度 = 文字高度(14px 字号)。
而全屏之后**状态栏也在 y=112 那个区间** ⇒ 点它反复触发**系统手势(下拉通知)**
而不是展开头部。我为此试了七八次,每次都回到桌面。
而收起态头部里藏着**这个会话的全部旋钮**(收件人/时间/抄送/权限档/预算)——
点不中就等于那些都看不到。
修:给那一行 `height(36)`(与左边返回键同高)。实测可点区
**14px → 43px**(`[1277,112][3122,155]`)。
★ 判据 + **变异验证**:去掉 `.height(36)` ⇒ 判红;还原 ⇒ 绿。
## 三、判据
`harmony-logic` 新增「折叠头部展开区高度 ≥32vp」一条(含变异自检)。
`run-all.mjs` → `checks=511 pass=511 fail=0 skip=0 red=0 broken=0 unreported=0`。
**未验**:预算条在设备上的**观感与点击**(模拟器顶部 155px 是系统手势区,
而头部恰在其中 —— 坐标式点击在那里会被系统抢走;真机上头部在状态栏之下)。
预算条的**数据链路**已用 curl 逐条实测(见上),但"点保存按钮后徽标变化"
这一步没在设备上走通。
|
|||
| e79a86ab79 |
跨端: 改名建议条接到详情页(端到端实测:点「接受」别名真的改了)
上一批做完了接口层,这批接 UI —— 详情页现在真的显示建议条并能操作。 ## 做了什么 提示条放在正文之前(与 WebUI 同位置):Agent 给的理由 + **后果说明** (「改名后需用 name@path.xxx 寻址;旧别名立即失效……」)+ 「接受」/「忽略」。 蓝底(`accentSoft`)而不是警告色:它不是故障,是一个**待决定**; 深色下走 `accentSoftFor`(就是前一批修的那个主题感知浅底)。 `doAcceptRename` 走 `PUT /sessions/{id}/alias`(与"人手改别名"同一端点), `doDismissRename` 走专用 dismiss 端点 —— 服务端记下被驳回的别名, 否则每次打开会话都要重新点一次「忽略」。 ## 途中三处**我自己的**错误(都值得记) 1. **写错了请求体字段名**:我按记忆写成 `session_alias`,而服务端 `updateAliasRequest` 的 tag 是 **`alias`**(`sessions.go:95-97`), WebUI 也是 `{ alias }`(`client.ts:537`)。而它的 `Decode()` 是 `DisallowUnknownFields()` ⇒ 传错名字**直接 400**(不是"被忽略")。 这是今天第三次同源错误(`AppearancePayload` / `ForwardMailRequest` / 这次)—— **请求体字段名必须去服务端 struct tag 里读,不能凭记忆写**。 2. **判据跟着实现一起错**:我把判据也写成"断言 `session_alias: alias`" —— 等于把这处错误**锁进两处**。判据已改成断言 `this.alias = alias`, 并加了一条反向断言"不许出现 `session_alias: alias`"。 ★ 这是"判据引自己的实现当依据"的又一次实证。 3. **UI 写完忘了接数据**:提示条的 UI 做完、编译过,但**没有调用 `getRenameProposal`** ⇒ 页面上永远不显示(服务端明明有建议)。 —— 正是本仓反复出现的"写好了但没人调用":静态判据查得出"代码里有这个方法", 查不出"页面从没调过它"。 ⇒ 补上 `loadRenameProposal()`,并**单独一个 try**(建议条是附加信息, 它失败不该把正文也变成错误态)。 ## 端到端验证(真数据) 造一封带标记的真邮件投进一个真会话 → 服务端识别(`rename_proposed: "rename-verify"`) → 详情页出现建议条(dump 读到理由 + 后果说明 + 两个按钮) → 点「接受」→ 库里别名真的变了: 处理四桥冒烟测试邮件 → rename-verify ## 判据 `run-all.mjs` → `checks=510 pass=510 fail=0 skip=0 red=0 broken=0 unreported=0`。 `hvigorw assembleHap` 成功;前端重建 + 重打包。 **未验**:「忽略」按钮的端到端(服务端记下驳回、不再重复弹)—— 接口层已有判据钉住路径,但没在设备上真点过。 |
|||
| c2f35d1023 |
跨端: 会话改名建议的接口层(详情页缺的第二块,服务端三个端点齐)
审计发现详情页缺三块功能之二:**Agent 的改名建议条**(WebUI `MailView.tsx:324`
的 `RenameProposalBar`)。这一批做**接口层**,UI 接线下一批。
## 为什么这件事不只是"少个提示条"
WebUI 那段的注释写得很清楚:
> Agent 干到一半自己改掉,人上一秒记住的地址下一秒就失效。
> 提议 + 人确认,既让 Agent 表达意图,又保证寻址稳定性由人掌握。
也就是**寻址稳定性**的设计 —— 别名是人的寻址入口,Agent 只能**提议**。
## 做了什么
1. `model/SessionRename.ets` —— `RenameProposal`(`alias` / `reason`,
字段名与服务端 json tag **逐字对齐**)
2. `api/SessionApi.ets` —— 三个动作:
· `getRenameProposal(sessionId)` → `{"proposal": {...}}` 或 `{"proposal": null}`
· `acceptRename(id, alias)` → **`PUT /sessions/{id}/alias`**
· `dismissRename(id)` → `POST /sessions/{id}/rename-proposal/dismiss`
3. 判据(`harmony-logic`,+1 条):字段名、响应外壳容忍 `null`、
接受走别名端点、驳回走专用端点、方法名与 WebUI store 同名、+ 变异自检。
★ **接受为什么不另开端点**(服务端注释原话):
「那条路径已经有唯一性校验与 409 处理,复制一遍只会多一个出错的地方」。
所以"接受建议"与"人手改别名"是**同一条路**,而"驳回"是另一条
(它不是改别名,是"别再问了"——服务端记下被驳回的别名,
否则每次打开会话都要重新点一次「忽略」)。
## 端到端验证(真数据,不是形态检查)
造了一封带标记的真邮件投进一个真会话:
POST /mail/send {reply_to: …,
body: "内容正文。<!-- agentmail:rename-session alias=\"rename-verify\" reason=\"验证改名建议链路\" -->"}
→ 200 {"rename_proposed":"rename-verify", …}
然后:
GET /sessions/{id}/rename-proposal
→ {"proposal":{"alias":"rename-verify","reason":"验证改名建议链路"}} ✓ 形状与我的一致
SELECT body FROM mails …
→ "内容正文。" ✓ 标记被剥掉
两件事都验到了:**服务端识别建议**,且**标记从人读的正文里剥离**
(HTML 注释在 Markdown 渲染器里会变成可见文本,所以必须剥,不能指望渲染器吞掉)。
## 判据
`run-all.mjs` → `checks=510 pass=510 fail=0 skip=0 red=0 broken=0 unreported=0`。
`harmony-logic` 30 → 31。`hvigorw assembleHap` 成功;前端重建 + 重打包。
**未做**:UI 接线(详情页的提示条)。接口层已完成并验证,
但"页面上真的显示建议条并能点接受/驳回"要下一批。
|
|||
| b7c5d8b1e7 |
跨端: 邮件转发上线(详情页缺的入口)+ 修两个真 bug(动作球重叠 / 键盘挡住按钮)
审计发现详情页缺三块功能之一 —— **转发**。服务端 `forward.go` 完整、
`MailApi.forward()` 也早就写好了,缺的只是这一页的入口。
## 一、转发(对齐 WebUI `ForwardBar`)
字段与占位文案逐条对齐:收件人 / 抄送(**可折叠**,默认收起)/ 说明 / 引用原文。
`subject` 留空让服务端自动加 `Fwd: ` 前缀(它处理了 "Fwd: Fwd:" 无限叠加)。
★ 请求体用了**专用类型** `ForwardMailRequest`,不复用 `SendMailRequest`:
服务端 `forwardRequest` 只认五个字段,而它的 `Decode()` 是 `DisallowUnknownFields()`
⇒ 多带一个(`body` / `reply_to` / `attachment_ids`…)就 **400**。
—— 这正是我今天在 `AppearancePayload` 上刚犯过的那个错,**端点一个类型一个请求体**。
★ 顺带发现:`MailApi.forward` 的签名**原本就写错了**(参数类型是 `SendMailRequest`)
—— 一直没被发现,因为**从来没有调用方**。"写好了但没人用"的代码,
连它自己的类型对不对都没人验过。
## 二、真 bug ①:两个动作球**几乎完全重叠**
转发球与回复球**各自**写在 `Stack({alignContent: BottomEnd})` 里、各带一个 margin。
实测 dump 的 bounds(密度 2.875):
转发 [2984,2010][3122,2148]
回复 [2949,1998][3110,2159] ← 重叠区 x ∈ [2984,3110]
屏幕上只看得到一个球,**转发入口等于不存在**。
根因:`Stack.alignContent` 把**每个**子元素都摆到同一个角,margin 只是各自微调。
修法:用 `Row({ space: 12 })` 包住两个球、由 Row 带 margin 到角落。
**设备实测**(修后 dump):`[2776,2021][2914,2159]` 与 `[2949,1998][3110,2159]`,不重叠。
## 三、真 bug ②:键盘一弹,「转发」按钮就被顶出屏幕
转发弹层第一版**没有高度**(只有 `padding(16)`)⇒ 尺寸由内容决定。
而 ArkUI 默认的键盘避让是 `KeyboardAvoidMode.OFFSET`(整体上移)——
上移之后 `TextArea` 与「取消 / 转发」按钮**跑到键盘下面**,点不到。
实测截图:只看得见收件人输入框 + 键盘。
修法两半(缺一不可):
① 弹层给明确高度 `height('60%')`(与回复弹层一致,它一直没出问题);
② 说明框改 `layoutWeight(1)`(不是固定 `height(70)`)—— 键盘顶上来时它自己缩短,
把按钮留在屏内。
**设备实测**(键盘弹出时 dump):`取消 [1196,2054][1426,2158]`、
`转发 [2880,2054][3110,2158]` 都在屏内(屏高 2232),且 `clickable=true`。
## 四、判据(这两条固化了上面两个形状)
`harmony-admin` 新增两条**静态形状**判据(它们抓的是写法,不需要设备):
1. **同一 `Stack` 里的多个圆形按钮必须被 `Row` 包住**(否则重叠);
2. **底部弹层必须有明确高度** + 会撑高的子元素用 `layoutWeight`
(否则键盘一弹按钮就被顶出屏幕)。
★ 为什么用静态判据而不是设备判据:这两个 bug 的**形状**在源码里就看得见
(`Stack` + 各自 margin / 弹层缺 `.height`),而设备判据要摆出"键盘弹出"这个态,
成本高且不稳。静态判据在这里是**更快更准**的那一层。
(设备判据仍保留在别处,验"真的能打开、真的渲染出来"。)
## 五、验证
`run-all.mjs` → `files=32 ran=32 checks=509 pass=509 fail=0 skip=0
red=0 broken=0 unreported=0`(`harmony-admin` 28 → 30)。`hvigorw assembleHap` 成功。
**设备实测**:转发球与回复球分开显示(各自图标可见);
点转发球 → 转发框打开(「转发「…」」+「抄送」折叠开关 + 取消/转发);
键盘弹出后按钮仍在屏内可点。
**未验**:真发一封转发(收件人输入在自动化里不稳 ——
`uitest inputText` 是**追加**而非替换,且 `keyEvent Back` 会退出页面而不是收键盘。
这条留待真人操作窗口,与 `harmony-p4c-boundary-decls` 那笔同性质)。
|
|||
| 1bf687f506 |
判据: 图片上传链的设备判据(用真实素材)+ 静态欠账从 6 减到 5
继续升级到期的静态判据。这一批做 `harmony-imageprep`(上传链), 并顺手把已完成的 `harmony-admin` 移出欠账名单。 ## 一、图片上传链:用**真实素材**验压缩决策的输入 `run-all.mjs` 的 `STATIC_ONLY` 里那条登记写着 「上传链的设备侧:`@ohos.multimedia.image` + 相册要设备才能真跑」。 上面 30 条判的都是 `model/ImagePrep.ts` 的纯逻辑(阈值、单调性、边界…), 它们全绿时有一件事从未验过:**那些数字与设备上真实的图片对得上吗**。 新判据用**用户真上传过的那张壁纸**(`GET /me/appearance/image` 取回, 1402×1122 / 152570 字节 —— 不是合成图),断三个跨端事实: ① 设备上读到的像素尺寸 = 决策时用的尺寸; ② 真实体积在客户端上限之内;③ 该尺寸走 `planCompress` 首档**不缩小** (长边 1402 < 2560)+ `judgePick` 放行。 ★ **为什么不合成图**:纯色能压到几 KB、噪声几乎压不动,用它们验阈值 会得到"怎么都对"的假绿。真实照片的行为才是要验的那个。 ★ **诚实标注了没做的那一半**(写在判据注释与 `DEBTS.json` 里): 本判据用的是**设备上的 `file`/`ls`** 这一独立来源读素材属性, **没有**跑 `image.createImagePacker()`。跑它需要一个**用户选图**入口 (`DocumentViewPicker`,要人操作系统选择器),自动化里没有稳定路径; 而编一个"绕过选择器直接调 `packJpeg`"的测试专用入口,会是**只有测试在用的代码** ——那种代码不会被真实场景触到,验它等于验一个不存在的东西。 ## 二、静态欠账 6 → 5(还完就划掉) `harmony-admin` 那条**已升级为设备判据**(上一批做的), 所以它**不该再留在 `STATIC_ONLY` 里** —— 那个名单是给"还欠着的"记账的。 留着会让余额虚高,而这正是这个机制要防的(欠账不显形就等于没有)。 ## 三、途中被两条"登记一致性"判据拦了两次(都按它们给的方向修) 1. `debt-visibility`:我在 `harmony-imageprep` 里新增了两处边界声明 ("未覆盖/未验"这类词),而余额里没登记 ⇒ 红。 **按它要求的顺序做**:先补 `docs/DEBTS.json`(`harmony-p4c-boundary-decls` 那一笔的 note 里写明"只做了一半"),再把登记次数 4 → 6。 2. `commit-hygiene`:`DEBTS.json` 说 `static-criteria=6`,实测 5 ⇒ 红。 —— 这条正是"可见的那个数字是副本,漂移了必须两边一起改"。 改数字之外还在那一笔里写了**为什么减**(admin 升级并移出名单)。 ★ 第三处被拦很有意思:`debt-visibility` 是**按词表数自己**的判据, 我第一次修时在那个文件里写了一句话里含"仍未覆盖",于是它把自己数多了 1 处 (12 → 13)。那一刻是**判据在正确地工作**("多一处即红")—— 我写的其实是**引用**另一笔账,不是新的边界声明,所以改成了不带判定词的措辞。 ## 四、判据 `run-all.mjs` → `files=32 ran=32 checks=507 pass=507 fail=0 skip=0 red=0 broken=0 unreported=0`。`harmony-imageprep` 30 → 31; `harmony-admin` 移出静态名单(仍在套件里,28 条)。 `hvigorw assembleHap` 成功;前端重建。 **剩余到期未升级**:`harmony-appearance`(已有 2 条设备判据)、 `harmony-logic`、`cross-client-theme`、`appearance-defaults` —— 逐条来。 |
|||
| fc295893cb |
跨端: 深色模式下的品牌浅底不跟随 —— 12 处「选中/未读」底色在深色页上刺眼
## 真 bug(设备实测) 深色主题下,「我的」页**选中**的那张账号卡片仍是接近纯白的浅蓝 (实测像素 `(255,255,255)` 级别的浅底压在 `(32,34,36)` 的深色页上)。 根因:`Theme.accentSoft = '#EFF6FF'` 是**写死的浅色**,而它被当作 「选中态背景」用在 12 处(未读邮件行、选中账号、分段选中、登录页模式切换…)。 **为什么只有 WebUI 没这个问题**:它有 CSS 变量的**反转发**机制 —— `index.css:113` 的 `--c-blue-50: 239 246 255` 在 `.dark` 段(`:475`)被换成 `28 37 54`(深蓝黑)。ArkTS 的 `static readonly` **一个常量一个值**, 没有那层机制 ⇒ 静态常量必须自己提供两个取值。 ## 修法 1. `Theme.accentSoftDark = '#1C2536'`(对齐 WebUI `.dark --c-blue-50` 的 `28 37 54`) 2. `Theme.accentSoftFor(dark)` 作为**唯一入口** —— 不在页面里各自 `isDark ? a : b`:那样每处都会各写一遍,迟早漏一处 (WebUI 那条"由 test/theme.test.mjs 逐档断言"就是为防这个) 3. **深浅色从哪来**:只有 `MainPage` 算得出(它读 `resourceManager` 的 `colorMode`)。所以走 `AppStorage` 单向发布(与徽标、windowInsets 同一套): MainPage 算 → 写 `agentmail.appearance.isDark` → 各窗格 `@StorageProp` 读。 6 个文件、12 处,全部改用 `accentSoftFor(this.isDarkNow)`。 **设备实测**:深色下「我的」页账号卡片与收件箱未读行都变成深蓝底, 像素 `(32,34,36)` 与页面底一致(不再刺眼)。 ## 判据(这条是新加的,形状值得记) `cross-client-theme` 新增:**品牌浅底必须有深色变体**。断三件事: 1. 深色变体存在,且**取值从 WebUI 的 `.dark` 段反推**(不是随手挑一个深色); 2. 有按主题选值的**入口**(防"页面各自写三元"); 3. **用到它的地方真的走那个入口** —— 扫描所有 `backgroundColor(… accentSoft …)` 并排除 `accentSoftFor`,把漏改的位置**逐行报出来**。 第 3 条在我改到一半时**当场列出了剩下 9 处**(`CalendarPage:1172`、 `ComposePage:255`、`LoginPage:327`…)—— 这就是它该有的样子: 不是"断言存在某个常量",而是"断言没有一处漏改"。 ★ 写这条判据时踩了自己一次:JS 模板串里嵌了反引号包围的标识符 (`` `accentSoft` ``),直接 SyntaxError。改用字符串拼接。 ## 判据 `run-all.mjs` → `checks=506 pass=506 fail=0 skip=0 red=0 broken=0 unreported=0`。 `hvigorw assembleHap` 成功;前端重建。 **未验**:日历/登录页在深色下的观感(只逐处改了底色,没逐页截图)。 |
|||
| a18014e1e3 |
判据: 管理页设备判据(到期静态判据的第一条升级)+ 修设备判据基建的三个真 bug
`run-all.mjs` 的 `STATIC_ONLY` 里登记着 6 条"只能静态验"的判据,前提是
「本工作区能装、能点设备」。设备现在可用 ⇒ 它们**到期**了(欠账当场变红)。
这一批升级第一条:`harmony-admin`(用户管理页)。
## 一、新设备判据:管理页真的能打开、列表真的渲染
上面 27 条静态判据判的都是逻辑与接线形态。它们全绿时,
"管理页能不能打开、列表能不能渲染"**一句都没验过** —— 而它最容易坏:
路由注册对了但入口没接上、接口回来了但列表没渲染。
判据自己搭现场:拉起应用 → 进「我的」→ 滚到底 → 点「管理」→
断言真的在管理页(出现「新建用户」)且**渲染出用户行**。
## 二、途中撞出三个**判据自身**的 bug(都已修,都写了教训)
1. **`tapText` 从来没成功点过任何东西**(`lib/harmony-device.mjs`)
`findByText` 返回的是**数组**,我当单个节点用了 ⇒ `node.attributes` 恒
`undefined` ⇒ 恒返回 `false`。症状极隐蔽:调用方以为"没找到那个文案",
实际是帮手自己坏了。修:从数组里挑,且**优先挑可点的那个**
(同名文案常常一个可点、一个不可点)。
2. **判据之间互相干扰**(新增 `backToMain`)
每个设备判据都是写操作,会把前台留在它操作完的那一页。`harmony-admin`
把人留在管理页 —— 那是 `pushUrl` 出去的独立 `@Entry` 页,**没有侧栏**
⇒ 后面的 `cross-client-gesture` / `harmony-appearance` 找不到侧栏、
双双 skip(skip 原因写的是"宽屏侧栏找不到",看起来像功能没了)。
`launchOurApp` 解决不了(`aa start` 只切前台,不弹栈)⇒ 新增 `backToMain`。
3. **"找不到元素"要先分清是功能缺失还是判据没摆好现场**
本判据连栽四种形态,每一种都伪装成"功能缺失":
· 锚点文案错(入口是「管理」,我按「用户管理」找 —— 后者只是说明的一部分)
· 元素在滚动下方(dumpLayout 只报可见节点)
· 文字节点不可点(`.onClick` 在包住它的容器上,ArkUI 的常态)
· **滚动步长跨过了它**(诊断打印现场才发现:停在了「系统通知」那一带,
而「管理」只占 ~0.04 屏,一次 0.2 屏的滑动必然越过)
⇒ 最后改成"**先滚到底、再小步回扫**"(只管往前找在不均匀列表上必漏),
并加了 `AGENTMAIL_ADMIN_DEBUG=1` 的诊断入口把现场打出来。
## 三、判据
`run-all.mjs` → `files=32 ran=32 checks=505 pass=505 fail=0 skip=0
red=0 broken=0 unreported=0`(连跑两次稳定)。
`harmony-admin` 27 → 28 条。`hvigorw assembleHap` 成功;前端重建。
**设备实测**:管理页从「我的」页打开,显示「管理 / 用户管理 3 / 新建用户」
与三行用户(jianf 管理员 / gui-lab 用户 / test 用户)。
**仍到期未升级**:`harmony-appearance`(已加 2 条设备判据,但登记里其余部分仍静态)、
`harmony-logic`、`cross-client-theme`、`appearance-defaults`、`harmony-imageprep`
—— 按"每条缺什么设备侧验证"逐条来,不为了消数字而凑。
|
|||
| ff172ed2c1 |
判据: 修 AmIcon 尺寸检查的假红 —— 它跨过 } 吃到了下一个组件的修饰符
这条判据抓的是真形状(`AmIcon({…}).width(48)` 会让图标贴左上角),
但正则写宽了:`[\s\S]{0,200}?` 允许**跨过 `}`**,于是这种**正确**写法被判红:
Row({ space: 4 }) {
AmIcon({ … })
Text('新建')
}
.height(26) ← 这是 Row 的,不是 AmIcon 的
实测(日历工具栏新加的「+ 新建」按钮):报 `CalendarPage.ets:1223 → .height(`,
而那一行链在 `Row` 上。**是判据误报,不是代码错。**
修法:扫描区间**不允许出现 `{`/`}`**。`AmIcon({…}).width(48)` 里参数花括号已被
`)` 收尾,而跨到下一个组件的路径会被排除。
★ 修完**立刻做了变异验证**(这类放宽最容易顺手把判据改废):
把真实的 `AmIcon({…})` 改成 `AmIcon({…}).width(48)` ⇒ **判红**;
还原 ⇒ 绿。既要"不误报",也要"仍能抓真错",两半都得验。
|
|||
| f655453424 |
跨端: 审计补强 —— 邮件行的「抄送 N」+ 断点差异登记 + 两处"看起来有其实没有"的澄清
继续「全面对齐 WebUI 和鸿蒙」。这一轮做的是**逐页对照审计**(子代理通道被
session daemon 的端口占用堵死,改为自己逐处读源码对照)。
## 一、补上邮件行的「抄送 N」(真缺失)
WebUI `MailList.tsx:350` 行上有「抄送 N」,鸿蒙**完全没有**。而数据一直在:
`cc_list` 服务端确实返回(实测回包字段列表里有),只是鸿蒙的 `MailLike`
接口漏了这个字段 ⇒ 一封抄送给多个人的邮件在列表里看不出任何区别。
修法:接口加 `cc_count`,`MailSummary` 加 `cc_list` + `cc_count`,
收件箱/发件箱两处填充派生值,行上按 WebUI 的位置渲染。
**设备实测**(造了一封带 2 个抄送的真邮件):行上出现「抄送 2」,
位置与 WebUI 一致(主题行下方、灰字)。
★ 接口用**数字**而不是 getter:`MailSummary implements MailLike`,
而 ArkTS 的 interface 里不能声明 getter(编译报 "incorrectly implements interface")。
## 二、有意**不抄**附件标记(发现 WebUI 那段是死代码)
WebUI 行上还有一个 📎 + 数量的标记(`MailList.tsx:351-355`)。但核对服务端
**实测回包**:`GET /me/mail/inbox` 既没有 `attachments` 也没有 `has_attachments`
⇒ `mail.attachments?.length ?? 0` **恒为 0**,那个标记在 WebUI 上**从不出现**。
所以鸿蒙这一轮**有意不抄它** —— 照抄一个不工作的东西,只会多一处
"看起来有、永远不亮"的代码。要做这个功能得先让服务端在列表回包带上附件计数
(一次 JOIN 的事),那是独立的一件事,已登记进 `docs/DEBTS.json`
(`mail-list-attachment-count`)。
★ 这一条与鸿蒙 `MailSummary.has_attachments` 那个字段一起处理掉了:
它还留在那里会误导人(服务端永不返回它 ⇒ 恒 false)。
## 三、两端宽屏断点不同 —— 登记 + 判据(此前**无任何记录**)
审计点名要核实的这条确认成立:
WebUI: `NARROW_QUERY = '(max-width: 1023px)'`
含义 = 「三栏(60 导航 + 320 列表 + ≥520 详情 ≈ 900px,再加余量)放不下就退化单栏」
鸿蒙: `isWide = width >= 768`
含义 = 「要不要显示**侧栏**」(鸿蒙内容区是一个窗格,没有并排三栏)
**含义不同,所以数值不同本身不算错** —— 这与手势阈值同一条口径
(语义各自成立时,数值不必强求一致)。但**用户可见的后果**是:768–1023 宽
(常见竖屏平板、窄窗口)下 WebUI 是单栏+底栏、鸿蒙是侧栏+内容,
同一宽度在两端长得不一样。
处理:① 登记进 `docs/DEBTS.json`(`wide-breakpoint-divergence`,
带三个待定选项);② 加判据 —— 它**不**要求两端取值相同,而是要求
「取值可读 + 含义写清 + 差异被登记」三件事同时成立。
★ 写这条判据时又踩了同一坑:用 `code()` 读注释 ⇒ 永远红。
`criteria-hygiene` 判据的头顶就写着"判代码用 code、判理由用 prose"。
## 四、判据
`run-all.mjs` → `checks=505 pass=505 fail=0 skip=0 red=0 broken=0 unreported=0`
(cross-client-theme 15→16)。`hvigorw assembleHap` 成功。
**未验**:附件标记(有意不做);抄送行在**深色**下的对比度未单独验。
|
|||
| f811c9887a |
跨端: 三个真 bug(外观保存 400 / 改档位界面不动 / 内容列溢出屏幕)+ 设备判据
这一轮从「全面对齐 WebUI 和鸿蒙」开始,先做设备层判据升级,结果**判据一上线就连撞三个真 bug**
—— 它们全都是静态判据照不到的形状:**数据对、界面不动**。
## 一、外观保存从来就没成功过(PUT 400)
`payloadFromLocal` 复用了 `AppearanceResponse` 当请求体,而那个类型是 **GET 的响应**:
带着 `has_image` / `image_bytes` / `saved`。服务端的 `Decode()` 是
`DisallowUnknownFields()`(严格,**有意为之**)⇒ **每一次保存都被拒收(400)**。
症状极隐蔽:本地 `@State` 立刻变 ⇒ 肉眼看着像成功了;只有看 hilog 的 HTTP 状态码
才发现 400。修法是加 `AppearancePayload`(**恰好**服务端 `models.Appearance` 的五个字段)。
★ 这是"两端共用同一个类型"的代价:请求与响应本来就不该同形。
★ 服务端严格是**对的** —— 它帮我们抓到了这个错误。修客户端,不是放宽服务端。
## 二、改了档位,页面背景一点不变(两层原因)
**第一层**:`SettingsPage` 存进 store 了,但 `MainPage` 的 `bgPlan` 只在启动时算一次,
之后没人动 ⇒ 发布一个 `AppStorage` revision(计数器,不是布尔 —— 布尔 true→true
不发变化通知),`MainPage` 用 `@StorageProp + @Watch` 接住。
**第二层(更隐蔽)**:接上之后**还是不动**。因为 `bgPlan` 是 `@State BackgroundPlan`,
而 **ArkTS 的 `@State` 观察不到类内部字段**的变化 —— 渲染读的正是
`this.bgPlan.kind` / `.layers`。hilog 一对证据同一次启动相差 100ms:
Appearance: sync: bgKind=preset … hasImg=true ← 数据是对的
Wallpaper: kind=none layers=0 active=false ← 渲染读到的还是旧值
修法:加 `@State bgContentRev: number`,每次算完 plan 就 +1,**并在 Builder 的
条件表达式里消费它**(ArkUI 按"这个 Builder 读了哪些 @State"决定是否重渲染;
只加计数器而渲染不读,等于没加 —— 判据同时断这两半)。
★ 同一个坑本仓出现过(`AppearanceStore` 的注释里写着这句),这次换了地方发作。
## 三、「我的」页右端内容被顶出屏幕(追了很久的 `56.000000` 之谜)
真相有**两层,两层都值得记**:
1. `dumpLayout` 里 `Slider` 节点的 `text='56.000000'` 是**无障碍文本**
—— 屏幕上根本没这串字(截图可证)。**dump 的 text ≠ 看得见的字**。
2. 真正的问题是那个**看得见的** `Text('56%')` 落在 `x=3250`,而屏宽 3184
⇒ **它在屏幕外**。用户只看得到滑杆、看不到数值。
根因:`MainPage` 里"侧栏 + 内容列"是 `Row` 并排,内容列写 `.width('100%')`
—— 在 Row 里 `100%` 是**父容器全宽**,与侧栏的 60vp **相加** ⇒ 必然溢出。
实测内容列 `[229,28][3357,2204]`,右边缘超出屏幕整整 173px(= 60vp)。
修法:改 `.layoutWeight(1)`(吃剩余空间)。修完实测 `[229,28][3156,2204]`,
`Text('56%')` 落在 `[3049,1803]` —— 屏内。
★ 为什么值得一条设备判据:**同一处错误在不同 pane 上表现不同**
(日历页自己算宽度就没露出来),很容易被当成"某一页的样式问题"去调。
## 四、设备判据基建(这一轮加的能力)
- `lib/harmony-device.mjs` 新增 `launchOurApp` / `ourAppInFront` / `tapText` / `swipe`。
`swipe` 里 clamp velocity 并写明那个坑:`uitest` 的 velocity 越界**不报错**,
只回一句 "out of range, the default value will be used",静默换成默认 600。
- 三条新设备判据(`harmony-appearance`):壁纸档位真的切换 / 窗格内容不得超出屏幕。
- 修了一个**元问题**:设备判据在套件里**恒跳过**(要求"现场已经摆好"),
只有手动摆好才通过 ⇒ 那等于没有判据。现在它们**自己搭现场**
(拉起应用 → 导到目标页 → 操作 → 复位)。`cross-client-gesture` 与
`harmony-appearance` 都改成了这样,套件里 `skip=0`。
- 途中撞出的两个判据自身缺陷(都写了注释):
· `root0` 用**切页前**的快照 ⇒ 套件里红、单独跑绿(通过与否取决于跑之前那一屏)
· 侧栏项筛选没排除**品牌标** ⇒ 想点「日历」却点到「通信」
## 验证
`run-all.mjs` → `files=32 ran=32 checks=503 pass=503 fail=0 skip=0
red=0 broken=0 unreported=0`(含设备判据:gesture 9、appearance 27)。
`hvigorw assembleHap` 成功;前端重建 + 重打包(`build-stamp` 7/7、`packaging` 5/5)。
设备实测(HATriple 3184×2232):
· `PUT /me/appearance` 从 **400 → 200**(服务端访问日志),
库里 `jianf` 的记录从空变成 `bg_kind=preset / bg_dim=56 / bg_blur=3`。
· 壁纸真的透出来了:预设档缝隙 `#E0E2E4`、不设档 `#FFFFFF`(像素级对比)。
· 「我的」页 `56%` / `3px` 正常显示在屏内。
**未验**:壁纸在真机上的观感(渐变是否好看、压暗 56% 是否合适);
这一轮只验了"数据通了、界面响应了、内容没被裁掉"。
|
|||
| 4a5318ca28 |
跨端: 手势补设备判据(真滑、标题真变)+ 修三处"设备判据假红"的典型错法
上一提交把滑动翻页做完了,但只验到"代码形态对"。**真装上跑时一次都没触发** ——
这一条补上设备实测,并把途中撞出来的三类错法记进判据注释。
## 一、为什么必须有设备判据
第一次装上跑:**代码全对、手势一次都没触发**。原因是起点 x=2600 落在
**右栏**(日程面板 `Column [2207,112][3184,2204]`)—— 事件根本没进网格列。
只有静态判据的话,结论会是"手势已实现、判据全绿",而用户真去滑时一动不动。
所以判据断的是**界面真的翻了**(滑动前后各 dump 一次,断言月标题变一格),
不是断"日志里有 turned=true" —— 后者只证明判定通过、证明不了有人会动。
## 二、途中撞出来的三类错法(都写进注释了)
1. **`uitest` 的 velocity 越界会被静默替换**
我传 150(想表达"慢一点"),它只回一句
`The swipe velocity out of range, the default value will be used.`
—— 不报错、不改退出码,只是默默换成默认 600。于是"慢滑"变成"更慢的滑",
看起来像手势没生效。**传合法值(200~40000)+ 读回执**才能避免。
新增 `lib/harmony-device.mjs` 的 `swipe()` 帮手(与 `tap()` 同族,
内部 clamp,并在注释里写了这个坑)。
2. **滑动是有副作用且不可撤销的写操作 ⇒ 不能盲目重试**
第一版写"不生效就再发一次(最多 3 次)",结果实测**把日历一次翻了 3 格**
(标题跑到 2026年11月)。重试不是"再试一次",是"再翻一页"。
只有幂等操作才允许盲目重试。改成:**只发一次 + 等足够久**(最多 8 秒轮询)。
3. **坐标不能用"到边界差一点"的比例**
取 `width * 0.68` = 2165,距网格列右边界 2207 只有 42px ⇒ **一次都不触发**;
同一台设备、同一份代码,起点改 2000 立刻生效。
起点贴边时触摸点会被判到相邻的右栏。改成取**列中段**
(`0.62` / `0.13`,两端各留几百 px 余量)。
另:本判据第一版用"滑一次 + 固定等 2500ms + dump",**单独跑通过、接进
run-all 后失败**(设备繁忙时不够)。固定等待是设备判据最常见的假红来源 ——
改成轮询到标题变化。诊断留了 `AGENTMAIL_GESTURE_DEBUG=1` 门控,
失败时能一次看到"前台/坐标/轨迹",不用事后手动复现。
## 三、设备实测结果(HATriple 3184×2232)
· 月档:左滑 9月 → **10月**(`dx=-556.5 dy=0 ms=624 turned=true`);
右滑回 **9月**(`dx=+556.5`)。
· 周档:`2026年9月14–20日` → 左滑 → **`9月21–27日`**(正好 +7 天,
证明步长走的是 `stepDaysOf` 而不是写死 1)。
· 斜滑(dx=400 dy=800):`PanGesture({direction: Horizontal})` 在系统层
就没识别 ⇒ 比鸿蒙侧的 `SWIPE_AXIS_RATIO` 更早拦住(正确行为)。
**仍未验**:56vp / 1.4× / 700ms 这三个数**手感是否合适**,只能真人滑过才知道。
我验的是"判定逻辑 + 接线 + 真能翻页"。
## 四、验证
`run-all.mjs` → `files=32 ran=32 checks=498 pass=498 fail=0 skip=0
red=0 broken=0 unreported=0`。
(含新设备判据:cross-client-gesture 9 条,其中第 9 条是真滑。
`build-stamp` 7/7、`packaging` 5/5 —— 按判据要求重建 + 重打包,没有改记录迁就。)
|
|||
| 28e3da76d4 |
跨端: 左右滑动翻页(P6 第 3 步)+ 还 gesture-semantics 债 + 修跑不起来的判据基建
★ 这一轮从用户一句「滑动手势呢?」开始。查下去发现它不是"顺手加个手势",
而是 `docs/DEBTS.json` 里挂着的一笔债 —— `gesture-semantics` 的原话是:
「P6 第 3 步:鸿蒙侧出现滑动手势代码时**立即建**判据
(此前建 = 只有一端存在的假判据)」
也就是**先有手势、再钉语义**。WebUI 2026-09-14 就有滑动翻页(用户当时
亲口提的),鸿蒙一直没有 ⇒ 之前建判据会是空真(∀x∈∅)。
## 一、手势本体(两端语义逐项对齐,数值各自定)
按 `HARMONY-ALIGN-PLAN.md:118-128` 显式选的 **(b) 口径**:
「手势的物理量本来就不该强求同值……该对齐的是**语义层**」。
· 判定逻辑放**纯逻辑层** `model/Calendar.ts` 的 `judgeSwipe`(可被 node 直跑,
写在 .ets 里就跑不了判据,语义没法被单测钉住)。四道门:位移 / 纵向优先 /
快滑窗口 / 方向。
· 阈值**不引用** WebUI 的 40 / 1.5 / 600(那是把巧合当契约),
各自定为 56vp / 1.4× / 700ms,并在注释里写出取值依据。
· 接线在 `CalendarPage.ets`:`PanGesture({direction: Horizontal})` +
`onActionStart`(记时 —— `GestureEvent` **没有时间戳字段**,我查了 SDK
的 gesture.d.ts 确认)+ `onActionEnd`(读 offsetX/offsetY)。
· ★ 挂在**网格列**上而不是整页:右栏(日程/编辑器)里有输入框与可滚内容,
整页挂会让「在表单里横划一下」变成翻月。
· ★ 翻页复用 `shiftRange()`(与 ‹ › 按钮**同一个来源**)—— WebUI 的注释
专门交代过:各写一套的话,阈值、边界、三档行为迟早分叉。
手势回调里**不准**直接改 year/month(锚点是唯一真相,年月只能由
`shiftRange → syncYearMonthFrom` 派生)。
**有意差异(记录在案,不是漏做)**:WebUI 在周/日档会额外检查「触点是否落在
可横向滚动的区域里」,是则让给滚动条(用户 2026-09-14 报过这个冲突)。
鸿蒙周档是「一行 7 格按 layoutWeight 等分」、**不横滚** ⇒ 该条件不适用。
哪天加了横滚必须同时补上它。
## 二、判据(8 条语义契约 + 行为层)
新增 `cross-client-gesture.test.mjs`:两端都真有手势 / 方向映射逐项相同 /
纵向优先 / 快滑窗口 / 复用同一翻页函数 / 无边界回弹 / 有意差异被记录 / 自检。
**只比语义、不比数值**,并反向断言鸿蒙的阈值常量不得直接取 WebUI 的那三个数。
`harmony-logic.test.mjs` 加行为判据:真跑 `judgeSwipe`,把四道门各自验一遍
(只钉字符串的话,一个 return 写漏了照样全绿)。
## 三、顺手修掉的三处**判据基建**缺陷(不修就没法验证上面这些)
1. `run-all.mjs` 只认 `# pass N`,而 node v24 打的是 `ℹ pass N`
⇒ **21 个文件被记成"没自报条数"**、套件在 HEAD 就恒红(memory 里记过这条,
修法也记过,今天终于落进代码:4 个正则加 `(?:#|ℹ)`)。修完 `unreported` 27 → 0。
代价是暴露出一批此前被"没自报"掩盖的真实问题(下面 4~6 条)。
2. `harmony-nav` 的设备判据 `navItemsOf`:宽屏过滤条件从「左边缘靠左 1/6」
改成「**整个盒子在侧栏轨道内**」。旧条件把**日历网格的格子**
(实测 `[229,511][366,794]`)也当成导航项,一屏数出 11~12 个。
这是设备实测抓出来的 —— 我第一版还以为是"底部簇混进来了",
打印真实数据才发现是隔壁页面的格子。
3. `harmony-logic` 两处:从 `NAV_ITEMS` / `NAV_SIDEBAR_ITEMS` **各自的数组**里取
label。原先把全文件 `label:` 一网打尽 ⇒ 得到 7 个(4+3 混在一起),
任何一边改对了它都会红。
## 四、被判据拦住后的正经修法(每条都按判据自己给的方向改,不改判据迁就代码)
· `cross-client-theme` A2 拦住我:新增的 `navActiveBg`/`navBrandFg`/`badgePlain`
未登记;又拦住我:`sseColorOf` 里四个裸色值。→ 抽成 `Theme.sseConnected` 等
四个令牌(取值对齐 WebUI 的 Tailwind 类)+ 登记 + 在 Theme.ets 的表里写理由。
· 同一条判据的"死令牌"检出:`Theme.durBase` 声明了却从没人读。
**查 WebUI 才发现壁纸淡入是真有的动效**(`index.css:742-752` 的
`.app-backdrop` 从 opacity:0 → 1,180ms)⇒ 补上而不是删令牌
(删掉等于把差异抹平、还说成"清理")。reset 在 `animateTo` **外**,
与 `calPaneIn` 同一条纪律。
· 玻璃登记:`NavItemBuilder` → `SidebarItem`(重构后按最近的 @Builder 命名),
登记同步跟上。
· `harmony-admin` 的退出判据:退出逻辑抽成 `api/Logout.ets` 的 `performLogout()`
(两个入口——「我的」页与侧栏底簇——必须做同一件事,尤其"先注销推送 token"
那一步)。判据相应改成**追到实际执行处**(两半都断:按钮调了 + 函数真清了全部),
并写明"别再退回直接匹配 SETTINGS_PAGE 的写法"(那会随重构假红,
下一个人只会去改判据而不看行为)。
· `harmony-widescreen` 的连接点色值:色值搬进 Theme 后,判据改成断
「令牌定义对了 + 侧栏真的用了它」两半(只断任一半都有假绿形态)。
· `align-refs`:`CalendarView.tsx` 变了,按判据要求**读一遍差异**再更新登记
(差异只有农历小字的灰阶档位 gray-300→gray-400/500,**骨架未变**)。
· `criteria-hygiene`:`harmony-contacts` 自造了一个 `code()`、`harmony-widescreen`
裸用 `readFileSync` ⇒ 都改用 `lib/read.mjs` 的共享入口。
途中撞出一个**判据自己的 bug**:`code()` 的块注释正则
`/\*[\s\S]*?\*\//` 会把注释里出现的 `/*`(如 `/」**` 这种中文夹星号)
当成块注释起点,一路吃到几十行后的 `*/`,把中间的 import 全吞掉 ——
于是 hygiene 判据假红"没 import"。改掉那处写法后正常。
## 五、验证
判据面:`run-all.mjs` → `files=32 ran=32 checks=497 pass=497 fail=0
skip=0 red=0 broken=0 unreported=0`。
其中新/改判据:gesture 8、nav 18、widescreen 7、logic 30、admin 27、
cross-client-theme 15、criteria-hygiene 6。
构建:`hvigorw assembleHap` 成功;前端 `npm run build` + 重新打 AppImage/deb
(`build-stamp` 7/7、`packaging` 5/5)。
**设备实测(HATriple 三折叠 3184×2232,hdc 连 127.0.0.1:5555)**:
· 农历在格子里真的显示(1=二十 / 7=廿六 / 19=**初九** / 11=八月),与 WebUI 一致;
这条同时验证了**服务端农历路由已部署**(之前线上是 404)。
· 日历左右两栏:编辑器出现在**右栏**、左栏月份仍可见(单栏模式下编辑器会整页盖掉它)。
· 侧栏 3 项 + 底部一簇;徽标回到图标右上角。
**仍未验(如实标注)**:滑动翻页的**手感**(阈值 56vp/1.4×/700ms 是否合适)
只能真人滑过才知道;我只验了判定逻辑与接线形态。动画同理 ——
机制已验证(`animateTo` 驱动 + reset 在窗口外),但"看起来顺不顺"未做取样验证。
docs:`DEBTS.json` 销掉 `gesture-semantics`(并记结算说明)、
`HARMONY-ALIGN-PLAN.md` P6 从「✅(滑动翻页除外)」改为 ✅。
|
|||
| e8b260dd70 |
维护: 底本重算(4 个文件漂移 = **底本过期**,不是变异残留)—— 逐个核实后按本文件的协议记一行"为什么"
pi `df62ad3d` 那封的 §四 我复核后**已经在 `d07494e` 修好了**(父提交正是 pi 报信时的 HEAD `93dabb4`):`if (r.red) reds.push(r.red)` + `DIAG[*].blocksGreen` 让 `summary.py` 的 status 真的进了 `verdict`。同刻 A/B(只切 baseline 一个变量)实测: 残留态红清单 diff **恰好 +1 行**(`(summary.py)baseline-residue ——…`),`red` 10→11; stale 态 diff **0 行**。⇒ "stale ⇒ 提示(0) / residue ⇒ 红(进 verdict)" 这条口径成立。 这轮顺手核"每个文件的登记/实际读数"时,撞上底本自己的状态: `baseline=3/7(底本过期…)`。逐文件核过 —— 4 个漂移**全部**是 `git diff --quiet HEAD -- <f>` 为空的**提交态**(各自有明确的提交): · AdminUsersPage.ets ← |
|||
| d3140c213c |
补充: 给"条数登记校验"加锚点(自检 5b)—— ★ 而我第一版锚点自己写成了**空真**
`788d7cc` 把条数校验挪进 `else { … }`(红绿都跑)之后,**它没有自检**:
下一个人完全可以再挪回 `else if` 后面,那时**什么都不会红**
(红文件又收不到条数回执),而缺口**只在文件恰好红时隐形** ——
正是它上次潜伏到 `c523c21` 的原因。⇒ 补自检 5b。
做法(锚点落在**实际发生的比较**上,不许落在源码文本上,§16.3):
在 `else { … }` 里每次比较都 `countCheckRan.add(file)`,
5b 从 `records`(谁真的自报了条数)**独立重算**应当被评估的集合,再要求它被覆盖。
**不读 `reds`、不看那条校验自己的输出** —— 否则就是"读数器自作证"。
★★ 而**我第一版 5b 是空真的**(自捉,如实记):
我原来比的是"凡**条数不符**的文件都必须被记录过" ——
**而这条修复本身就把 harmony-admin 的登记数对齐了 ⇒ 那个集合恒空 ⇒ 断言恒真。**
变异测试当场抓到:把 `countCheckRan.add` 挪回"只绿才走",**5b 一声不响** ——
那一刻我才发现它不是"通过",是"**没有对象**"。
改成比 **"所有自报了条数、且没崩的文件"**(红绿都含):红文件必在其中 ⇒ 非空,
且"红文件被漏记"必被抓。另加**反空转**:集合为空而并非全部 broken ⇒ 自检自己报失效。
变异验证:挪回"只绿才走" ⇒ 5b 报出 6 个红文件名
(cross-client-theme / build-stamp / align-refs / harmony-push / criteria-hygiene / harmony-admin);
基线不报 ✓。修后全套(树内):files=31 ran=31 checks=487 pass=476 fail=11 red=10
broken=0 unreported=0。
`CRITERIA.md` 补两条可复用教训:
① **"拿现有数据试一遍"要试到"数据非空"** —— `∀x∈∅` 的判据看起来和真判据一样绿;
② **修好一件事会同时消灭它自己的测试对象** ⇒ 锚点不许建立在"当前的错误状态"上,
要建立在**恒在的集合**上("谁自报了条数",而不是"谁条数不符")。
|
|||
| 788d7ccb20 |
修复: 条数登记校验**只在"绿"的那条路上** —— 文件越红,它的登记数越没人守(假绿方向)
设备在场时跑全套,顺手核了每个文件的『登记条数 vs 实际条数』,发现:
narrow-layout 登记=64 实际=88 exit=0 绿 ⇒ 校验生效(报了)
nav-merge 登记=8 实际=9 exit=0 绿 ⇒ 校验生效(报了)
background 登记=43 实际=44 exit=0 绿 ⇒ 校验生效(报了)
harmony-presets 登记=5 实际=6 exit=0 绿 ⇒ 校验生效(报了)
harmony-admin 登记=22 实际=27 exit=1 ★ 红 ⇒ 校验**走不到**(grep 0 命中)
根因:这条校验原来写在**最后一个 `else`**("退出码 0"那条路)里,而
`r.status !== 0` 会**先在 `else if` 里 reds.push 并跳过它**。
⇒ **文件越红,它的条数登记越没人守** —— 假绿方向:
harmony-admin 那多出来的 5 条判据**不在"被删会红"的保护内**,
而它恰好是红的 ⇒ **只要它一直红,缺口就一直是隐形的**;
等它修绿那天校验才第一次生效,那时多出来的几条可能早被删了。
(同族:`unlisted`/`blind`/`baseline=` 的结论到不了 `verdict`,§16.1。)
★ 而且这个缺口**已经真的吃过一次**:`c523c21`("邮件详情与「我的」页 1:1 对齐")
给 harmony-admin **+5 条判据**(它自己的 commit message 就写着 "+5"),
**却没同步登记数**,而当时该文件是红的 ⇒ 5 条新判据至今裸奔。
修法:把条数校验移进 `else { … }`(红绿都跑;先 push 退出码红,再判条数)。
⚠️ broken(崩了/一条条数都没自报)**不在这里**判 —— `diedWithoutReporting` 已吃掉它,
再叠一条"没找到自报条数"只是噪音:**"判据没答 ≠ 判据答错了"**(§17)。
并做那个"显式、可复核的编辑":登记数 22 → 27(和
|
|||
| a0e950109a |
修复: 判据注释说"这个值来自 Go 源",实际**一个字节没读** —— 服务端上限真漂移 29 pass/0 fail 一个字不变
pi 报的那格(4b 的 if(false))已由并发会话的自检 4c 补上,我变异验证通过(三种恒假写法都红)。
这轮顺着"第 2 列现在每次运行都可见"去读那 6 条理由,挖出**同族的另一条缝**:
`harmony-imageprep` 的
/** 服务端壁纸上限(appearance.go 的 appearanceMaxBytes() 默认值) */
const SERVER_LIMIT = 4 << 20;
注释说它来自 Go 源,而它一个字节的 Go 源都没读。实测(同刻 A/B):
把 Go 里那个默认值改成 8<<20(真漂移)⇒ 本文件 **29 pass / 0 fail 一个字都没变**
⇒ 这条判据存在的全部理由(客户端上限要留在服务端那道门之内,否则必然 413 /
白扔分辨率)在服务端那道门真动了时**不会红**。危险处在于注释让读者以为已对齐。
修法(三件):
① 从**真源头**解析,且**两处都读、要求相等** ——
⚠️ 我第一版只读了 appearance.go 的 `return 4<<20`,那是**兜底分支**;
生产里 config.C 非 nil ⇒ 生效值来自 config.go 的
`MaxAppearanceBytes: getEnvInt64("AGENTMAIL_MAX_APPEARANCE_BYTES", 4<<20)`。
"读了源"还不够,还得问"读的是不是生效的那一处"(同族缝的下一层)。
② 解析失败必须红,**不许静默回退到硬编码**(回退 = 把"我读不到"变成"值是对的")。
③ 新增一条判据钉住"真的读出来了":两处都无 err、生效值等于 config 那处、等于兜底那处、且 >0。
并**如实标出标签范围**:这证明"与默认值对齐",**不证明**"与运行值对齐" ——
AGENTMAIL_MAX_APPEARANCE_BYTES 可覆盖;别把本条读成"413 已不可能发生"。
变异验证(每个只动一处):config.go 默认值→8MB ⇒ fail=2;
appearance.go 兜底→8MB(两处不一致)⇒ fail=1(恰为"两处相等"那条,
余量那条**故意不红**,因为生效值没变 ⇒ 所以"两处相等"必须单独存在);
config.go 那行删掉 ⇒ fail=2(不许静默)。基线 30 pass / 0 fail。
另:`STATIC_ONLY` 第 2 列里那句「+ 服务端,三样本机都没有」是**假话** ——
`server/internal/handler/attachments.go` 在本机、其 Go 测试 `-run Attach` 跑得通、
且本判据根本没连服务端。已改成"欠的只有设备侧那一半"(列每次运行都播报,假话会被读出来)。
注册条数 29→30 已同步。全套:files=31 checks=396 pass=384 fail=12 red=9 broken=3(跑在隔离 worktree)。
`CRITERIA.md §16.4`:通用规则 —— **注释里写"这个值来自 X"不构成读 X**;
凡"必须与别处一致"的判据先问:它真读了别处,还是抄了一份?
|
|||
| 36ef15a8f2 |
跨端: 顶栏不再自己铺白条 + 日历改左右两栏 + 常驻窗格动画真的会播(用户三处实测指出)
用户三条反馈,逐条对应:
① 「底栏数字为什么显示在图标下面?」
WebUI 的徽标是 `absolute top-1 right-[22%]`(脱离文档流、浮在图标右上角),
我写成了 `Column` 的第三个子节点 ⇒ 参与竖向布局、掉到文字下面。
改用 `Stack({ alignContent: Alignment.TopEnd })` 锚在**图标**上。
(顺带撞了 skill 里明写的坑:Stack 没有 `.justifyContent()`。)
② 「一个横着过去的白条,我真的服了」/「期望:融进背景」
WebUI 的顶栏**自身没有底色** —— 只有 `border-b border-gray-200`
(`ContactPanel.tsx:64`、`CommTabs.tsx:40`、`CalendarView.tsx:295`),
底色由所在面板给;壁纸开启时那层面板是玻璃色(`index.css:876`)。
鸿蒙三个窗格顶栏写死了 `Theme.surface`(实心白)⇒ 无论壁纸开没开,
顶上都是一条不通明白带。改成与**页面底**同一口径
(`bgActive ? Transparent : surface`)+ 补下边框。
③ 「日历页面和webui布局完全不同」
WebUI 是**左右两栏**(`CalendarView.tsx:452-457`):左 `flex-1` 网格、
右 `400px` 常驻面板(日程 / 编辑器 / 小时网格三态互斥)。
鸿蒙原来是**单栏竖堆**。重搭为两栏,`paneWide` 由 `.onAreaChange`
量本页**自己的**宽度(不是屏幕宽度 —— 宽屏下这一页已被侧栏占掉一截);
编辑器改占右栏位置(不再整页盖掉正在看的那个月)。
④ 「最严重的动画问题你一点也不该改」
日历是**常驻挂载**(`visibility` 控制,因为它里面 today 要随时间重算、
也要保住"正在看哪个月"),而 `.transition()` 只在**挂载/卸载**时触发
(SDK 原话 \"when it **appears and disappears**\")⇒ 挂在它上面的
`.transition(paneRiseIn())` **一帧也不会播**,切过去是硬弹。
WebUI 踩过同一个坑并把错法写进了 `index.css:1305-1320`
(「只挂了类,却没让触发窗口出现 ⇒ 类挂着、动画永远不播」),
它的解法是 `html.view-switch` 重放窗口。ArkUI 对应物是
`animateTo` + 显式 `calPaneIn` 属性(`opacity` + `translate`)。
★ reset 必须在 `animateTo` **外**:写进回调里会与同帧的 1 相抵,
渲染层只看得见最终值 ⇒ 动画退化成一个瞬移。
顺带修:
· 宽屏侧栏 = 3 项(通信/日历/**联系**)+ 底部一簇(头像/主题/退出),
与底栏的四项(含「我的」)**不是同一份清单** —— WebUI 的 Sidebar 与
NarrowNav 本就不同(`Sidebar.tsx:26-44` vs `NarrowNav.tsx:37-40`+143)。
新增 `NAV_SIDEBAR_ITEMS` / `ME_PANE_INDEX` / `SIDEBAR_ITEM_*`。
· 退出登录抽成 `api/Logout.ets` 的 `performLogout()`:侧栏底簇与「我的」页
两个入口必须做同一件事(尤其"先注销推送 token"那一步),复制一份就会不一致。
· 徽标 `'plain'` 档底色:WebUI 是石板灰 `--c-chrome-600`(#475569),
我写成与未读共用红色 ⇒ 「联系」的徽标看起来像"有未读"。
· 主题快捷开关(侧栏底簇):对齐 `ThemeToggleButton` —— 从 `system` 翻转时
落到**当前生效值的反面**(不是回 system;那可能毫无变化、让按钮看起来坏了)。
· 「浓度」→「压暗」+ 数值带单位(原先屏上印 `56.000000`;WebUI 是 `suffix="%"`)。
判据(8 个套件全绿:logic 28 / nav 18 / widescreen 7 / window 9 /
arkts 5 / contacts 5 / calendar 30 / system-api 5):
· `harmony-widescreen` ② 重写:**回读 `Sidebar.tsx` 数 `short:` 的个数**
要求鸿蒙同数,并断言底部一簇三键真的被调用(`this.onToggleTheme()` ——
第一版写成 `/onToggleTheme/`,变异测试当场证明它不咬:属性**声明**还在,
正则照样匹上)。新增 ⑧:三档色调各自底色,期望值从 `--c-chrome-600` 读出。
· `harmony-nav` 动画条重写:改判**机制真的存在且被驱动**
(旧断言 `.visibility(...).transition(...)` 锁的正是那个 bug ——
判据引自己写的注释当依据,就会把错误锁死)。新增 reset-在-animateTo-外
这条断言(否则动画退化成瞬移)。
· `harmony-nav` 设备条:`navItemsOf` 的宽屏过滤从"左边缘靠左 1/6"
改成"**整个盒子在侧栏轨道内**" —— 旧条件把日历网格的格子
(实测 `[229,511][366,794]`)也当成导航项,一屏数出 11~12 个。
新增 `navRailItemsOf`:导航轨贴顶、底部簇在屏底,按位置切一刀。
· `harmony-logic` 两处:从 `NAV_ITEMS` / `NAV_SIDEBAR_ITEMS`
**各自的数组**里取 label —— 原先把全文件 `label:` 一网打尽,
得到 7 个(4+3 混在一起),任何一边改对了它都会红。
设备实测(HATriple 三折叠,3184×2232):侧栏 3 项 + 底簇 / 底栏徽标回到
图标右上角 / 日历左右两栏与 WebUI 并排同构。
server/go.mod:补
|
|||
| 8a3d66a7c6 |
修复: pi 报的两条 STATIC_ONLY 发现 —— 第 2 列**没有读者** + static= 与"到期"是**两个量**(闸可被静默关闭)
pi 单独发来它答应我的那两条(在**当前 HEAD** 上重测),我逐条复现、修掉,
并在过程中**自己连错两次**(都被变异抓出来,已写进 `CRITERIA.md §16.3`)。
## 一、复现(我跑的,同刻 A/B)
**发现 1**:第 2 列「当初只能静态的原因」**没有任何判据在读** ——
两个解构循环都用 `,` 把它丢掉(`:1295`/`:1306`),唯一读者是**到期点名时打印**
(= 它最不需要被检验的时刻)。把它改成假话 ⇒ `red` 与红清单**零变化**。
**发现 2**:把 6 条探针全改指恒 `false`(`run: () => false`)⇒
**到期点名 1 → 0**,而 **`static=6` 一字不变**。
根因:`:2987` 的 `static=${STATIC_ONLY.length}`(余额)与 `:1605` 的
`if (dueStatic.length > 0)`(到期)是**两个量**,只有后者决定"到期"发不发生。
★ 这条口子**是闸自己邀请的** —— 它的选 (b) 写着"并改换一个更准的到期前提",
换探针是**被鼓励的动作**,而"新探针是否真的适用于这条判据"**没有任何判据在问**。
## 二、修法
1. `static=` 改成 **`static=6(其中已到期 6 条)`** ⇒ 关闸读得出来(Y→0 而 X 仍 6)。
2. **每次运行都全表播报** `STATIC_ONLY`(文件 + 原因 + 是否到期**并排**)⇒ 第 2 列有读者。
3. **自检 4b** 钉住"那一跳真的存在"。
## 三、我自己的两次错修(都被变异抓出)
| 修法 | 变异 | 结果 |
|---|---|---|
| ① 理由必须点到探针的某个标识符 | 拿**现有 6 条真理由**跑 | **红了 5 条** ⇒ 假红是噪音,已撤 |
| ② 断言 `staticDetail` **变量**内容完整 | 只删那句 `console.log` | **4b 仍报 ok**,真实输出播报段 **0 次** |
| ③ 断言源码含 `${staticDetail}` 形状 | 同上 | **仍 ok** —— 锚点写在这段自检**自己的注释**里,`includes` 命中注释 |
| ④ 剥离注释后再数 `+ staticDetail);` | 删 print | **4b 红 ✓** |
## 四、验证
· `AGENTMAIL_PROBE_DEVICE=ok` ⇒ `static=6(其中已到期 6 条)`、⏰ 标记 6 个、4b ok;
`=none` ⇒ `已到期 0 条`。
· 变异 2(改指恒 false):基线 `已到期 6 条`/点名 1 → 变异 `已到期 0 条`/点名 0 ⇒ **关闸可见**。
· 变异 4(删 print):4b **红**,红句"可执行代码里没有任何一处把 `staticDetail` 拼进输出"。
· 全套隔离跑:`^RED=0`、自检 4b 打印 1 次、播报段 1 次。
· 全仓 grep:**没有**任何下游解析 `static=` 的旧形状 ⇒ 格式改动安全。
## 五、文档
`CRITERIA.md` 新增 **§16.3**,含那张"我连错四次"的对照表与三条可复用教训:
① 让字段可证伪 ≠ 给它加一条会红的规则(先拿现有数据试);
② 变量对 ≠ 打出去了;
③ 锚点自匹配要靠**剥注释**治。以及通用规则:
**一个没人读的字段,先问它该被谁读 —— 给它读者;不该被读就删掉。**
★ 文件:`client/electron/test/run-all.mjs`、`CRITERIA.md`。
|
|||
| 3c53510b2c |
修复: **同一优先级写了两遍 ⇒ 两个顺序** —— diag 与 rc 排序相反,组合态下 UPSTREAM_RC 不变式为假(pi 实测)
pi 顺着我这几轮新分的"1 还是 2"往下试,找到一条**两条判定链排序不一致**的口子 ——
它正好长在我刚分的那个岔路口上。**我复现了,是当前代码的真 bug。**
## 一、口子
`summary.py` 里同一个文件有**两份**优先级表,而且**顺序相反**:
```
diag(原选择处):unlisted/ghosts 排第一 ⇒ 组合态报 manifest-mismatch
rc (原返回处):blind/unreadable/baseline-unrunnable|unknown 排第一 ⇒ 组合态退 2
```
⇒ 两类**同时**成立时(`unlisted` + 单文件不可读 / + 跑不了 `sha256sum` / + git 答不了),
`diag=manifest-mismatch`(`UPSTREAM_RC` 表里 **1**)而**真 `rc=2`**
⇒ `run-all` 那条不变式 `UPSTREAM_RC[diag] === rc`(`:1173`)**在其上为假**。
**实测**(裁 `PATH`、root 可达):修前 `diag=manifest-mismatch`、`rc=2`、表值 `1` ⇒ 不一致。
★ 后果**不是假绿**(`manifest-mismatch.blocksGreen=true` ⇒ 照样红、note 也转印),
而是**严重度被低估**:2 档的码被 1 档的码**盖住** ⇒ 读者以为"只要改清单",
而真相是"**连数都没读成**"。**rc 通道从此不可信。**
## 二★★ 根因:12 个案例**个个只动一维** ⇒ 不变式只在**对角线**上验过
`exitcodeSelfTest` 里**确实**有那条不变式,但它只跑自己构造的案例,
而那些案例每次只动**一个**维度(`unlisted` / `ghosts` / `blind` / `unreadable` / 各 `baseline-*`)
⇒ **组合(off-diagonal)无人可达**。
## 三、修法(两件,缺一不可)
1. **一条链推两个结果**:`summary.py` 里按同一顺序算出 `(diag, rc_want)` **一对**,
`RESULT` 行用它、`sys.exit` 也用它 ⇒ 排序不可能再漂移。
★ 为什么**不是**"把两条链顺序改成一致":那还是**两份**表,下次加条件两处又会各自漂移
—— 正是本仓反复消的"**一份事实两处实现**"。
2. **显式走一遍 off-diagonal**(新组合案例):
`未列入清单 + 跑不了 sha256sum ⇒ diag=baseline-unrunnable(2 档优先)且 rc=2`。
⚠️ 这条**不能**只靠"每跑必断不变式"代替:组合态下若 `diag` 又被低档码占住,
那条断言就**永远验不到 2 这一档**。
## 四、验证
· **修后组合态**:`diag=baseline-unrunnable`、`rc=2` ⇒ 与 `UPSTREAM_RC` **一致** ✓。
· **新案例**:`ok 组合:未列入清单 + 跑不了 sha256sum ⇒ … rc=2(真打出 diag=baseline-unrunnable,UPSTREAM_RC=2 ✓)`;
`--only-selftest=exitcode-selftest` **rc=0、^RED=0、ok=15**。
· **变异**(把单链 2 档与 1 档换序 = 复现修前两条链)⇒ **rc=1、^RED=2**:
`组合:… rc=1(期望 rc=2 且输出含 "diag=baseline-unrunnable"(真打出 diag=manifest-mismatch,UPSTREAM_RC=1 ✓))`
⇒ 排序本身被锁住;且**连带**抓出 `盲读` 那条(换序后盲读也走 `manifest-mismatch`)。
· **全套隔离跑**(worktree,只带本笔三个文件):`^RED=0`、因果红 0、对照红 0。
(`fail=10`/`red=10` 是并发会话的跨端线与设备竞争,与本笔无关。)
## 五、文档
`CRITERIA.md` 新增 **§16.2**(§16.1 的镜像:那边是"结论到不了",这边是"两个都到了但互相矛盾"),
含那条通用规则:
> **凡"多条判定链各自挑一个代表"的地方,都要问:它们挑的是不是同一个?**
> 只测**单维**永远证明不了这件事 —— **对角线上的绿,对组合态没有发言权。**
★ 文件:`client/electron/test/mutants/summary.py`、`run-all.mjs`、`CRITERIA.md`。
|
|||
| 271e0a8ee4 |
文档: **光进默认路径不够 —— 结论必须到得了"决定颜色的那一格"**(§16.1,同一条缝的三个落点)
pi 顺第一/二例的线实测出**第三例**,与我在 `4b841e0`/`6ee9902` 修的那两例 **是同一句话的第三次出现**,而 `CRITERIA.md` 里**一个字都没有** —— 修复在代码里、形状没进规范 ⇒ 下一个人只会修好其中一例。本节补上。 ## 缝的形状(三次都一样) > `summary.py` 把结论说对了、退出码也说对了,**但没有一条通道把它送到 > "决定 `verdict` 的那一格"** ⇒ 读者看到的是一个**看着完全正常**的读数。 ## 三个落点(每次位置都不同,所以必须分开记) | # | 落点 | 现象 | 修法 | |---|---|---|---| | 1 | **正则匹配就走不到 `status`** | 盲读时照样打 `RESULT mutants=0 …`(匹配)⇒ `status=2` 从没被读;`sp.stdout` 全文件只一处引用 ⇒ 三行 `✗✗` 一个字都到不了读者 | 盲读改打 `NO-READING` + **先判 `status`** | | 2 | **部分可读**(`chmod 000` 单个 job) | `blind` 为假、数字是真算的 ⇒ 那行**匹配正则且看着正常** ⇒ 修法①**对它无效** | 只有**先判 `status`** 兜得住 ⇒ 两层**各治一例**,不是叠保险 | | 3 | **`unlisted`/`ghosts` 退 0** | 警告已算出,却被 `whyLines: status !== 0 ? whyLines : []` **丢掉** ⇒ 磁盘 13 个 job、套件报 48、"未列入清单"在套件输出 grep **0** 次 | 退出码按**性质**分:`unlisted`/`ghosts`⇒**1**(清单该改=数据)、`blind`/`unreadable`⇒**2**(权限=环境);转印放宽成 `(note \|\| status !== 0)` | ★ **第 3 例的尖处**:`unlisted`/`ghosts` **不是**环境问题,是**清单该改**。 按 `env-defaults.sh:25`"失败要说清是环境问题…不要让它冒充代码缺陷" —— **反向也成立**: **别让"清单没跟上"冒充环境**。故归 **1** 不归 **2**。 ## ★★ 为什么会连着踩三次 三例的**上游判别都做对了**(`unlisted` 算出来了、`blind` 判出来了、`unreadable` 分出来了), 错的全在**最后一跳到 `verdict`**。⇒ 通用规则: > **每加一条"发现问题就报告"的逻辑,都要问一句: > 它的结论最终喂给了哪一个决定颜色的变量?** > 只 `print` 不算到达;落到 `reds`/`brokens`/`dueFailed`/`selfCheckFailed` 之一, > 或让退出码变成下一层会读的那个值,才算。 ## 验证 · **已锁住**:`--exitcode-selftest` 有真跑案例(`['未列入清单 ⇒ 1', …]`,`:1062`)、 `--verdict-selftest` 有判定案例(`['manifest-mismatch(status 1)⇒ 不许绿', …]`,`:1976`)。 · **变异验证**:把 `unlisted or ghosts` 从 `return 1` 改回 `return 0` ⇒ `--only-selftest=exitcode-selftest` **rc=1、2 条可归因红** (`未列入清单 ⇒ 1:rc=0(期望 rc=1 … UPSTREAM_RC[manifest-mismatch]=1 与真跑出来的 rc=0 不符)`)。 ⇒ 这条缝**有判据守着**,不靠下一个人再读一遍源码。 · **端到端 A/B**(干净 worktree,同刻对照): | | A:12 文件(清单一致) | B:13 文件(1 个未列入清单) | |---|---|---| | `diag=` | `baseline-stale` | **`manifest-mismatch`** | | "未列入清单" 在套件输出 | grep **0** | grep **1** | | `(summary.py)manifest-mismatch` 红 | **0** | **1** | · 本提交**只改文档**:`client/electron/test/CRITERIA.md`(新增 §16.1);隔离 worktree 跑全套 `^RED=0`。 |
|||
| d8277a5977 |
跨端: 导航项徽标两侧补齐(我上次"撤回"错了 —— WebUI 是有的)
上一笔我凭"两侧导航都没有徽标"把刚写好的徽标**撤回**了。那是错的:
`client/electron/src/components/Sidebar.tsx:88-95,111-125` 明确有——
```
const badge = isComm ? unread + pendingPerms
: modes.includes('contacts') ? contacts.length : 0;
const badgeTone = isComm && pendingPerms > 0 ? 'perm' : isComm ? 'unread' : 'plain';
```
并且是 `absolute top-0.5 right-1` 压在导航项右上角,`>99` 显示 `99+`。
我当时只看了底栏 `NavItem`(那里确实没有),就把结论推到了"两侧都没有"。
## 补的东西
- 新增**纯逻辑** `model/NavItems.ts` 的 `navBadgeCount` / `navBadgeTone` / `navBadgeText`,
逐条对齐 WebUI 的口径:
· **通信** = 未读 + 待决策(两类"要动手"合起来);
· **联系人** = 联系人数;日历/「我的」= 0(没有徽标);
· 色调:待决策**橙**(有人卡在那儿等)优先于未读**红**(只是还没看);
· 负数当 0(计数来自网络,不假设它干净);`>99` → `99+`。
- **两侧**(底栏 `MainPage.NavItem` + 宽屏 `WideSidebar.NavItemBuilder`)都接上,
且读**同一组** AppStorage 键 —— 两处各算一套,数字迟早对不上,而用户同时看得到它们。
- 计数发布走 `AppStorage`(单向:窗格写、导航栏读),与 `KEY_WINDOW_INSETS` 同一套机制。
不把这两个数提到 `MainPage`:那样"从没进过通信页"也会去发请求。
- 徽标位置对齐 WebUI 的 `absolute top-0.5 right-1`(压在项的右上角)。
★ 第一版我排在文字**下面**,截图一眼可见那颗 3 掉到了「联系人」标签底下、
还把 48vp 的项撑高了 —— 方阵节奏乱掉。
## 判据(harmony-widescreen 6 → 7)
第 ⑦ 条**直接执行**鸿蒙侧的纯函数,且期望值在测试里**独立算一遍**
(不复用被测函数,否则是"用实现验实现")。
★ 接线部分我写错过一次,变异测试当场拆穿:第一版只判
`assert.match(src, /navBadgeCount\(/)` —— 把**渲染处**的调用换成 `0`
(徽标永远不显示),文件里仍留着一处调用,判据照样全绿。
⇒ 改成判**把值交给 Text 的那一行**,并且认出两侧写法不同(底栏走 helper
`Text(this.navBadgeOf(key))`,侧栏就地内联 `Text(navBadgeText(navBadgeCount(...)))`)。
**4 个变异方向全咬**:通信漏算待决策 ⇒ 红;色调优先级写反 ⇒ 红;
侧栏渲染处换空 ⇒ 红;底栏渲染处换空 ⇒ 红。
设备实测:侧栏「联系人」显示红 **3**(3 个联系人),位置在图标右上角。
|
|||
| 7b3028342a |
跨端: 宽屏侧栏根本不像 WebUI —— 因为我上一版"复刻"的依据是编的
用户:「你自己看看宽屏的侧边栏和webui有哪怕一丁点的相似之处嘛?」
并排截图(WebUI 1100×700 @2x vs 三折叠展开态 3184×2232)之后,差异一眼可见:
| | WebUI | 鸿蒙(改前) |
|---|---|---|
| 文字标签 | **有**(通信/日历/联系) | 没有 |
| 选中态 | **浅蓝底块** | 只换颜色 |
| 「我的」 | 底部头像按钮进入 | 甩给 `onSettings` → **pushUrl 推页** |
| 品牌标颜色 | `#475569` 石板灰 | 品牌蓝 |
| 项间距 | 48px 项 + 4px gap,**贴顶一簇** | `layoutWeight(1)` 等分铺满(395px/项) |
## 根因:`WideSidebar` 里那段"复刻 WebUI"的注释是**编的**
```
* WebUI 的 `Sidebar`(60px 宽)是**纯图标轨**(无 label 文字)……
* 选中态:图标变色(`navFgActive`),**不加背景块、不加指示条、不加文字**
* (用户 2026-09-16:「底部导航栏不允许有文字」⇒ 侧栏同样按纯图标走)
```
两条都错,而且都能在源码里当场证伪:
- `Sidebar.tsx:110` 明明有 `<span className="text-3xs leading-none">{short}</span>`
—— 通信/日历/联系三个标签一直都在;
- `index.css:1590` 的 `.nav-item[data-active='true'] { background-color: … }`
就是底块,而且 CSS 注释**专门**说了侧栏必须有它:
「宽屏侧栏是 48px 宽的竖条,图标底下那一块底色是它**唯一的选中线索**,
所以"只变色"不能无差别推广到所有 `.nav-item`。」
我犯的错是**把底栏那条纪律套到了侧栏上**:用户 2026-09-14 说「底部导航栏选中
对应的文字和图标变色即可」、2026-09-16 说「底部导航栏不允许有文字」——
两句都针对**底部导航栏**,而侧栏是另一种东西(`index.css:1595-1606` 把这个区别
写得很清楚)。更糟的是我把这个错误**写进了判据**(`harmony-widescreen` ②③ 与
`harmony-nav` 的宽屏分支),于是判据锁住的是我编的理由,一路全绿。
## 修
- 侧栏项 = **图标 + 文字标签 + 选中底块**(`navActiveBg` = `--nav-active-bg` #DBEAFE,
判据**直接读 WebUI 的 CSS** 取值,不写死、更不引自己的注释)。
- 品牌标:`navBrandFg` = `#475569`(**像素取证**:2x 截图里品牌标附近最常见的墨色
是 `rgb(71,85,105) ×206` = `--nav-fg-muted`,即中性石板灰,**不是**品牌蓝);
尺寸/圆角按 WebUI `w-10 h-10 rounded-xl`(40×40、圆角 16);点它回收件箱。
- 项**贴顶一簇**(`Column({ space: 4 })` = WebUI 的 `gap-1`),不再 `layoutWeight(1)`。
- 删掉单列的"设置"入口(`onSettings` 回调一并删除)—— 那正是用户 2026-09-17 报过的
「我的页面完全没有遵守 nav 的导航规则」(push 页 ⇒ 侧栏整条消失)。
「我的」由 `ForEach(NAV_CONTENT_ITEMS)` 覆盖(该常量**含第 4 项**,
走 `onSelect(3)` = 窗格,与底栏同一套)。
- 补避让:侧栏原先**完全没有** `topInset` ⇒ 全屏之后品牌标被状态栏时钟压住。
## 判据(并修掉它们锁住的错误)
- `harmony-widescreen` ②③ **重写**:从"纯图标 / 只变色"改成
"有文字标签 / 有选中底块 / 不许留 `onSettings`",并读 WebUI `index.css` 拿真实色值。
- `harmony-nav` 宽屏分支:原来断言「侧栏项**不该有文字**」—— 同一条编造。
改成"图标(Path)画出来了 **且** 文字命中源码 `NAV_ITEMS`"。
- `harmony-nav` 宽屏形状阈值 `boxH > screenH*0.08` 是**错的**:48vp 项在密度 2.875 下
是 138px,而阈值要求 >178px ⇒ 四项全被滤掉(当时"通过"只是因为项被另一个 bug
压成了 39vp)。改成 `*0.04`,并补一条"必须有文字"把**品牌标**(40vp 无文字的可点方块)
排除在外。
**变异测试 3 个方向全咬**:去掉文字标签 ⇒ 红;去掉选中底块 ⇒ 红;Theme 色值写错 ⇒ 红。
★ 顺带记一条**我差点犯的错**:我一度按 density 3.5 换算,算出"60vp 侧栏被压成 49.4vp",
去查 flex 压缩、加 `.flexShrink(0)` —— 全是假的。实测密度是 **2.875**
(`138px ÷ 48vp = 2.875`),侧栏 173px ÷ 2.875 = **60.2vp**,与声明完全一致。
**没有压缩,是我除错了。** 已撤回那笔改动并把口径写进注释。
harmony-widescreen 6/6、harmony-nav 18/18、harmony-window 9/9、harmony-arkts 5/5、
harmony-contacts 5/5、harmony-calendar 30/30、harmony-system-api 5/5、harmony-logic 28/28。
|
|||
| 009ea172b7 |
跨端判据: 导航判据第一次在宽屏下跑就误报 —— 它只认底栏,而宽屏导航是左侧栏
三折叠展开态(3184×2232,宽高比 1.43)实测 `harmony-nav` 的行为判据变红:
底栏要渲染出 ≥1 个带文字标签的可点导航项(实际 0)
## 红得没错,但没用
`navItemsOf` 里写着 `+m[2] < screenH * 0.75 ⇒ 丢掉`,也就是**只找屏幕下 1/4 里的
可点容器**。而宽屏按设计导航就是**左侧栏**(`WideSidebar`),实测形状
`Column [28,985][201,1380]` —— 一条都落不进"下 1/4",于是取到 0,
判据转身说"导航没挂 / 被盖住 / 全不可点"。
★ 这条判据**从来没有在宽屏下跑过**(宽屏分支此前从未真正运行 —— 这正是本轮
建三折叠模拟器要解锁的东西)。第一次跑就误报,说明"红"也需要先确认
它在判什么,不能一看红就去改被测代码。
## 修:分模式判,且**两侧的契约本来就不同**
| | 窄屏(底栏) | 宽屏(左侧栏) |
|---|---|---|
| 形状 | 屏幕下 1/4 的可点容器 | 屏幕左 1/6 内、宽 < W/4、高 > H*0.08 的可点容器 |
| 文字 | **必须有**(标签就是底栏的主体) | **必须没有** |
侧栏无文字不是"放宽",是两侧本来就不一样:WebUI 的 `Sidebar`(60px)是纯图标轨,
且用户 2026-09-16 明确说过「底部导航栏不允许有文字」⇒ 侧栏同口径。
实测侧栏项子树只有 3 个节点 `Column → Stack → Path`,**没有任何 Text**。
拿"有文字"去要求纯图标轨,只会永远红。
⇒ 宽屏改为判**图标确实画出来了**(每个导航项子树里有 `Path`)且**不该有 Text**
(有文字就说明有人往纯图标轨里塞了标签 —— 那正是被否掉的方案)。
★ 两个条件分开写,**不能**只把"下 1/4"放宽成"下 1/4 或左 1/6":宽屏下内容区
卡片也在左侧(x 很小)且可点,放宽就全被当成导航项 ⇒ "导航项数 ≥1" 恒真,
判据等于没有。所以还要加宽度条件把内容卡片(宽 900+)排除掉。
★ 宽高比用 **> 1.2** 而不是绝对 vp:源码 `isWide` 判的是 `width >= 768`(vp),
而 dumpLayout 给的是 px,换算要写密度 —— 而密度是设备属性,写进来就是第二份
真相(这条判据刚因为"包名写死"吃过一次一模一样的亏)。
## 自检补上宽屏那支
`navItemsOf` 的形状判断改了之后,**宽屏那一支此前没有任何自检覆盖** ——
而自检没覆盖的分支就是下次回归不会响的那一支。按实测形状造合成树
(3184×2232、`Column [28,985][201,1380]`、子树只有 Path),并断言:
侧栏取到 2 个而不是 3 个(第 3 个是内容卡片,被误当导航项的话
"导航没挂"就永远判不出来);窄屏样本不许被误判成宽屏。
## 两个模式都实测过
- 窄屏(fold single,1008×2232,比例 0.45):底栏 4 项,标签
通信 / 日历 / 联系人 / 我的 全部命中源码清单。
- 宽屏(fold triple,3184×2232,比例 1.43):侧栏 5 个图标项,零 Text、各有 Path。
18/18 绿。
|
|||
| 4ff6b10260 |
跨端: 联系人页补齐「写信 / 归档」—— 顺带撞出三个只在跑起来才现形的 bug
用户:「你自己看看这些页面和webui有哪怕一丁点的相似之处吗?」
把两个客户端**同一个宽度**并排看之后,缺的很具体:WebUI 联系人卡片有
「写信 / 归档」,鸿蒙一个都没有。补的过程撞出三个缺陷,都不是"代码不合法"——
编译器与既有判据全绿:
## ① 归档打的是**服务端不存在**的路由(死函数)
`MailApi.archiveContact` 打的是 `DELETE /me/contacts/{name}/{path}`:
- `grep 'me/contacts' server/cmd/server/main.go` **零命中** ⇒ 按钮接上去就是 404;
- 全仓**没有任何调用方** ⇒ 它是个从没跑过的死函数,所以"没有归档按钮"这件事
一直没暴露这个错。
服务端真实形状是 `POST /api/v1/contacts/archive`(`handler.ArchiveContact`),
body 二选一 `{session_id}` / `{address}`。**跟 WebUI 同口径用 session_id**:
address 会随别名变化,只有 session_id 是会话的身份。
## ② 用已存 token 恢复登录**永远进不去主界面**(最恶劣的一个)
`LoginPage.tryRestore` 验完 token 就结束了 —— 设了 `loggedIn = true`
(界面出现「登录成功:jianf」)却**从无跳转**。本文件另外两条成功路径
(`aboutToAppear` 快速路径、`doLogin` 末尾)都有跳转,唯独这条没有,
而它**恰恰是老用户最常走的那条**(重启时 token 还在,`doLogin` 根本不会被调用)。
症状最坏的地方是它**看起来是成功的**。实测(模拟器 + jianf 的永久 key):
日志只有 `→ GET /auth/me` 然后什么都没有。补齐 ① 跳转 ② 账号登记
③ SSE 连接(后两条是 `doLogin` 有而这里缺的,少了它们进主界面是个瘸的状态,
而且因为 `aboutToAppear` 的快速路径靠账号命中,下次启动还会重走这里 ⇒ 永远进不去)。
修后实测:启动即进主界面(可见文本变成「发件箱/授权/收件箱 · 2 组 · 50 封」)。
## ③ 时间戳原样印出来
卡片直接印 `last_activity` ⇒ 屏上是 `2026-09-18T02:50:35.49065Z`。
WebUI 是 `09/18 10:50`(`toLocaleString('zh-CN', {month,day,hour,minute})`,**本地**时区)。
加 `MailGrouping.shortTimeOf`(走 `Date` 取本地字段;**不许 `.slice()`** ——
那是拿 UTC 的月/日当本地时刻,UTC+8 的 09-15 00:30 会显示成 09-14 16:30)。
实测:`112 封 · 09/18 10:50`,与 WebUI 逐字一致。
## 补的界面(三折叠模拟器 3184px 展开态实测)
- 两种视图**各一处**「写信 / 归档」(WebUI 两视图同语义),WebUI 用 `.reveal`
悬停显形,**鸿蒙不能照抄**:`index.css` 那段注释已经踩过这个坑
(触摸设备没 hover ⇒ 按钮透明却仍可点,一个看不见却按得动的破坏性按钮更糟),
WebUI 的修法是只在真支持悬停的设备上隐藏 ⇒ 鸿蒙这两个按钮**常显**。
- 归档先确认,**两视图共用同一个确认框**(WebUI 原话:换个视图就换套确认 UI
只会让人对「自己点了什么」更没底);文案逐字一致。
- 列表视图的 meta 行原来放 `last_preview`,于是同一联系人在两视图里的关键信息
不一致 ⇒ 改成与卡片视图同源(`N 封 · 时间`)。
- ★ 一处只有跑起来才会发现的坑:列表视图的 ListItem 钉死 `.height(85)` +
`clip(true)`,而确认框比 85 高 ⇒ **按钮被裁掉、点不了也退不出**。
确认态下高度交给内容自己定。
## 判据(新增 harmony-contacts,5 条;4 个变异方向都跑过)
判的都是"按下去会发生什么",不是"按钮在不在":
① 归档打的是服务端真有的路由(同时钉住**没有**再用那条不存在的 DELETE —— 只钉前者的话,
加个平行实现也能全绿);② 两视图各一处动作且去向正确;③ 确认框只有 1 个定义、
两视图都调它、文案逐字一致、确认态下点卡片不开会话;④ 时间戳走了格式化且
函数本身不走 slice;⑤ **切出 `tryRestore` 的函数体**判它自己含跳转
(只判全文件出现次数的话,另外两条路径里那两句就够让它变绿)。
|
|||
| 0e5eec61bd |
跨端: 登录页那个"emoji"是 Unicode 符号 —— 顺手修好图标贴左上角(共 4 处)
用户两条反馈,都是**看着界面**报出来的,而编译器与所有既有判据全绿:
①「为什么登陆页不是app图标,而是一个emojy?」
②「你自己看看那个图标的位置正常吗?」
## ① `Text('✉')` 被系统渲染成彩色 emoji
登录页的品牌标识原先写的是 `Text('✉')`(Unicode U+2709)。HarmonyOS 字体链里有
**彩色 emoji 字体**,U+2709 自带 emoji 字形 ⇒ 渲染成一枚黄白色风信封 emoji:
- `.fontColor(Theme.accent)` 对彩色 emoji **无效**(界面显示的是 emoji 自带颜色);
- 与底栏/侧栏那些 `AmIcon` 线描图标不是同一套视觉语言;
- 实测截图硬证:大屏下那枚 emoji 比旁边的文字还显眼。
而本仓**早就有** `ICON_PATHS.brandMark`(就是 App 图标上那个信封,专为品牌标识画的)。
换成 `AmIcon({ iconName: 'brandMark' })` 即可 —— 走 `Path.stroke()`,跟主题色走。
## ② 图标贴在卡片左上角(量出来的)
`AmIcon` 内部那个 `Stack` 是 `iconSize` 那么大、**默认靠左上**排版。调用方写
`AmIcon({…}).width(48).height(48)` 想要个大点的可上色盒子时,**外层盒子变大、图标不动**。
实测(三折叠 3.5 密度,`dumpLayout` 读的**实际 bounds**):
卡片 [1523,521][1661,659] 138×138px
图标 [1526,524][1589,587] 63×63px ← 左边距/上边距都只有 3px
⇒ 图标中心偏 10vp
**不是一处**:全仓扫出 4 个同样写法。修法是套一层
`Stack({ alignContent: Alignment.Center })`(仓里回复球与悬浮加号本来就是这么写的,
所以它们一直是对的):
| 位置 | 图标 | 盒子 | 原状态 |
|---|---|---|---|
| 登录页品牌卡 | brandMark 24 | 48×48 | 贴左上 ✗ |
| 用户管理刷新键 | repeat 20 | 40×40 | 贴左上 ✗ |
| 联系人视图切换 | cardView 18 | 40×40 | 贴左上 ✗ |
| 收件箱组头箭头 | chevronRight 12 | 20 槽位 | 贴左 ✗ |
| 回复球 / 悬浮加号 | chatBubble / compose | 56×56 | 本来就对(尺寸在外层 Button 上) |
修后实测:偏移 **−34.5px → 0.5px**(0.14vp,亚像素级)。
## 判据(harmony-arkts 3 → 5 条)
- **图标不许用 Unicode 符号充当**:扫 `Text('…')` 里单个符号的情况。
★ 只框 U+2600–U+27BF / U+2B00–U+2BFF / U+FE0F,**刻意不含基本箭头段**(U+2190–U+21FF)——
`→` 在正文里是标点不是图标,框进来会误伤大量正常文案。
(我第一版把箭头段也框了,结果自检自己先红 —— 断言写错就是写错,不靠放宽它来「修」。)
- **尺寸不许直接链在 `AmIcon` 上**:把"图标贴左上角"这个坑的**形状**钉住,
并自检"套了 `Stack` 的正确写法不许被误判"。
**两个变异方向都跑过**:放回 `Text('✉')` ⇒ 红 ✓;给 `AmIcon` 直接加 `.width(48)` ⇒ 红 ✓。
|
|||
| fbe7879981 |
跨端: 鸿蒙日历补「月/周/日」三档 —— 原来只有月视图
用户列的缺失之一(WebUI `CalendarView.tsx` 的 `type Scale = 'month'|'week'|'day'`)。 鸿蒙这边 `CalendarPage.ets` 自己的注释里就写着"没做"。 ## 三处必须一起改,所以档位进 model 而不是页面里一串 if 档位切换同时改变三件事,任一漏改都**不报错、只是静静地不对**: | | 月 | 周 | 日 | |---|---|---|---| | 标题 | `2026年9月` | 跨月时两头写月份 `9.28 – 10.4` | `2026年9月18日 周五` | | 翻页步长 | ±1 **月** | ±7 天 | ±1 天 | | 显示格子 | 整月网格 | 一行 7 天 | 一行、只亮一格 | 所以 `model/Calendar.ts`(纯逻辑,node 直接跑)新增: `CalendarScale` / `addDaysIso` / `weekDaysOf` / `rangeTitleOf` / `stepDaysOf` / `scaleLabel`。 页面只调用,不在渲染里重写 —— 重写就是"三处里漏改一处"。 ★ 步长按档走是**必须**的:周档按 1 天翻看起来像日档、日档按 7 天翻会跳过一周, 两者都不报错。判据钉"三档步长两两不同"。 ★ 周/日档的 7 天必须与月网格用**同一个** `weekStart`:不一致的话同一日期在两档下列位置 不同,用户看到的是"切个视图日期就跳位了"。 ★ 日档的格子仍摆在一行的**星期列**里(不是居中大字):上下翻日时格子不会在屏幕上跳。 ## 实现要点 - 档位状态叫 `calScale` 而**不是** `scale` —— ArkUI 的 `CustomComponent` 已有 `scale` 修饰符,同名直接编译失败(实测报 `Property 'scale' ... is not assignable to ...`)。 判据钉住这个命名。 - 周/日档下 `year/month` 由锚点 `selectedIso` 推出来(`syncYearMonthFrom`), 不允许两处各自保存"当前是几月" —— 否则会出现"标题写 9 月、周视图显示 10 月那周"。 - `monthKey()` 加上**档位**前缀:切档时那 7 个 iso 就是月网格里的 7 个, 不带档位的话键完全重叠 ⇒ 一个节点都不被替换 ⇒ 过渡静默不播、旧 `inMonth` 残留。 - 空态补 `layoutWeight(1)`:实测周视图下"这一天没有日程"贴顶、**下半屏是一大片空壁纸**。 ## 判据(harmony-calendar 23 → 30 条) 7 条新增,每条对着一个具体错法。**变异自检跑过两个方向**: - 周档步长改成 1(看起来像日档)⇒ 判红 ✓ - 跨月周标题漏掉结束月份(`9.28 – 4`)⇒ 判红 ✓ ## 实测(模拟器,逐档截图) 月/周/日三档切换后:周档标题 `2026年9月14–20日` + 一行 14~20,日档标题 `2026年9月18日 周五` + 只亮 18,月档回到整月网格。三张截图逐一核过。 |
|||
| af2d2b5cad |
跨端: 鸿蒙动画补齐 —— 并且发现原来的「逐字一致」是假的(令牌存在 ≠ 动画用了它)
用户:「A,同时把鸿蒙app的动画补齐」。 ## 先纠一条错的前提(这是本轮最有价值的发现) `Theme.ets` 的注释与 `harmony-nav` 的判据**都**写着:「三个数与 WebUI **逐字一致**: `--ease-out-soft: cubic-bezier(0.22,1,0.36,1)`、`--dur-fast: 120ms`、`--dur-base: 180ms`」, `paneRiseIn()` 就按 `durBase(180)` + `easeOutSoft` 做。 三个令牌**确实存在**(`index.css:310-312`)—— 但这句话把「令牌存在」当成了「动画用了它」: | WebUI 里 | 真实用途 | |---|---| | `--dur-base: 180ms` | **只**用在壁纸淡入(`:747`) | | `--ease-out-soft (0.22,1,0.36,1)` | **只**用在 transition(壁纸、控件变色) | | **所有 @keyframes 动画** | 硬编码 **150ms / 200ms** + `cubic-bezier(0.22, 0.61, 0.36, 1)` | 全仓 `(0.22,0.61,0.36,1)` 出现 **5 次**(rise-in×3 / cal-in×2),`(0.22,1,0.36,1)` 只出现 **1 次**(令牌定义处)。**是两根不同的曲线** —— 旧代码的面板入场比 WebUI 慢 30ms 且曲线偏软。 所以令牌拆成两组,各对齐各的:控件类 `durFast + easeOutSoft`;动画类 `durRise(150)/durMenu(140)/durCal(200) + easeRise(0.22,0.61,0.36,1)`。 ## 补的动画(对齐 WebUI 三个 @keyframes) - `menuIn()` —— 弹层:下移 4vp + 缩到 0.985 + 淡入(`@keyframes menu-in`)。 ★ 适用范围照搬 WebUI 注释那条窄口径(「只给真正是弹层的东西」):那条规则曾挂着 `glass-control`,于是每次切视图**所有按钮与输入框一起淡入位移**,09-15 被摘掉。 挂到写邮件页的账号候选列表上。 - `calendarSlide(forward)` —— 日历翻月:从 ±12% 横向滑入(`cal-in-next/prev`)。 方向由新增 `@State slideForward` 带进 `animateTo` 闭包;网格键加 `monthKey()` 前缀 保证月份一变所有键全变(否则"跨年同名月"那类边角会静默不播)。 - 写信页整页 `rise-in`、回复框 `rise-in`(WebUI 挂在回复框本身,不是外层遮罩 —— 挂遮罩上会让整个屏幕一起位移,看起来是"页面在动"而不是"框弹出来")。 **实测确认真的会播**(不是"编译过了"):按本仓记录的手法把 `durCal` 临时改成 8000 做慢动作,连拍三帧 —— 截图硬证**两张月历同时在屏**(九月淡出、十月从右侧 12% 滑入), 验完还原成 200ms。 ## 判据:从「令牌存在」改成「动画真的用了那个令牌」 `harmony-nav` 那条判据原文锚在令牌上,所以它对上面那个 bug **一辈子全绿**。 重写为: - 从 WebUI `index.css` **读出** `rise-in` 的真实时长与曲线(`150ms` + 那条 bezier), 再断言鸿蒙的 `durRise` 与曲线字面量与之逐字一致 —— 而不是"仓库里有没有 180 这个数"; - ★ 把 `paneRiseIn()` 的**函数体抠出来**单独断言它引的是 `durRise/easeRise`, 且**不得出现** `durBase/easeOutSoft`。 **这一步不能省 —— 我第一版就漏了它**:断言了令牌存在、也断言了曲线字面量存在, 但没断言函数用了它们;于是把函数体换回旧的错值后判据**仍然全绿**。 变异自检抓住了这一点,补上函数体断言后同一个变异 ⇒ 判红 ✓。 与今天修的另一处同源:**判据要锚在"这个东西被用在哪",不是"它存在"**。 |
|||
| a87a88ea2a |
跨端: 全屏是**窗口级**的 —— 光给 MainPage 让位,等于把黑边换成顶栏压字
上一提交(cac026e)把黑边消掉了,但**只给 `MainPage` 加了避让**。
实测截图硬证:写邮件页的「取消」与时钟「09:49」重叠、「发送」与 wifi/电量图标重叠。
## 根因:`setWindowLayoutFullScreen(true)` 不只作用于当前页
它是**窗口级**的:一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页
(写邮件/会话/收件箱/邮件详情/用户管理)都从 y=0 开始画。
所以上次那个错与更早那次(6861934 只删全屏不留避让)**同源**:
都是"同一件事只做了一半"。上次少的是**步骤**,这次少的是**页面**。
## 改法:把"消费避让"变成每个 @Entry 页都得做的事
- `model/WindowInsets.ts` 加 `topInset(insets)`:取 0 时(未全屏/取不到)
表达式的值与旧代码**逐字相同** ⇒ 没全屏的环境行为不变,不会把谁顶下去。
- 五个页各按自己的形状让位:
- 固定 56vp 顶栏(写邮件/会话/收件箱/用户管理):
`height(56 + topInset(...))` **与** `padding(… top: topInset(...))` 一起加 ——
只加 padding 会把固定的 56 切掉 39(按钮压扁),只加 height 则内容仍贴 y=0。
- 满高容器(邮件详情):`padding({ top })` 加在 `@Entry` 包装层。
★ **不能加在 `MailDetailView` 里面**:它同时被 `MainPage` 的 Navigation 内嵌复用,
而那层已经让过位了 —— 加在里面就变成让两次(39vp 变 78vp)。
这类"同一组件两种入口"的坑与"悬浮加号要放在 Navigation 内部"同源:
**让位的量取决于它被挂在哪一层**。
## 判据(harmony-window 8 → 9 条)
接线⑤ 枚举**所有** `@Entry` 页并要求它们消费避让 —— 口径是
"有人在窗口上开了全屏 ⇒ 每个 @Entry 页都得让",而不是"检查 MainPage 做了没有"。
后者在新增一个推上来的页时会静默逃掉,而"新增一个页"正是最常发生的事。
豁免要带**可机器复核**的理由(不再是"这个页先不管"):
- `Index.ets`:DevEco 模板欢迎页,不在 `main_pages.json` 流程里;
- `LoginPage.ets`:根容器 `.align(Alignment.Center)` ⇒ 结构上碰不到 y=0。
但"居中"是可能被改掉的性质 ⇒ 判据**断言那个居中写法仍然存在**,
谁把它改成贴顶,这条先红,逼他回来重新想这个页要不要避让。
**变异自检两个方向都跑过**:
- 把 ComposePage 避让整个拿掉(重演"只给 MainPage 加")⇒ 红 ✓
- 只加 padding 不加 height(会压扁按钮)⇒ 红 ✓
★ 期间还修掉两处"判据锚在当时的字符串上"(不是放宽,是它把"加一个正当的避让"
与"真犯那个错"判得一模一样):
- `harmony-nav` ④ 的留白断言、`harmony-widescreen` ④ 的 navReserve 断言,
都改成剥注释后验**形状与不变量**,而不是写死字面表达式。改完变异仍咬得住。
|
|||
| cac026e9e2 |
跨端: 上下黑边真的消了 —— 全屏 + 避让是"同一套东西的两半",上次只删了一半
用户第三次报同一条:「你再看看页面底部,那么大的黑色,你看从头到尾都没
修好,你能不能好好看看我给你的示例工程怎么处理上下黑边的」。
## 根因:上一次把"两半"当成了"一件事",删掉一半就以为修好了
`6861934` 的注释白纸黑字写着「★ **刻意不用** `setWindowLayoutFullScreen(true)`」,
理由是「实测过:它确实也消掉黑带,但会连状态栏区域一起吃进布局,于是页签栏
被时钟/电量盖住(截图硬证「07:43」与「收件箱」重叠)」。
那次实测**是真的**,结论**下错了**:被盖住不是"不该全屏",而是
**只做了全屏、没做避让**。示例工程里这两件事本来就是**同一套东西的两半**:
common/.../util/WindowUtil.ets → setWindowLayoutFullScreen(true)
+ getWindowAvoidArea(TYPE_SYSTEM /
TYPE_NAVIGATION_INDICATOR)
features/mine/.../view/MineView.ets:251 → .margin({ top: statusBarHeight + …,
bottom: naviIndicatorHeight })
只做前半 ⇒ 内容跑到状态栏底下没人让(那次退回的原因);
只做后半 ⇒ 黑边照旧(这三次报修的原因)。退回的代价是**黑边留了三天**。
## 实测(模拟器 1256x2760,四页一致)
修前:顶部纯黑 136px、底部纯黑 60px + 手势条 20px
修后:四页**纯黑段均为 0**;y=0..135 是壁纸(时钟浮在上面,正是示例工程的效果)
y=2662+ 壁纸铺到底、底栏浮在手势区之上
## 改了什么
- `entryability/EntryAbility.ets`:拆出 `setupFullScreenWindow()`,
在 `loadContent` **回调里**调(与示例工程同一位置 —— `px2vp` 要用 `getUIContext()`,
那要有已加载内容才拿得到)。全屏 + 读两个避让区 + 订 `avoidAreaChange`。
`setWindowSystemBarEnable(['status'])` 保留(状态栏要看得见),但**不再靠它**消黑边。
- `model/WindowInsets.ts`(新):纯逻辑 `insetsFromAvoidArea()`,不 import SDK ——
判据才能在 node 里直接喂样本验换算。形参叫 `toVp` 而**不是** `px2vp`:
后者是 SDK 已废弃的全局函数名,`harmony-system-api` 按名字扫,同名形参会误报。
- `pages/MainPage.ets`:`@StorageLink(KEY_WINDOW_INSETS)` 订阅;状态栏高度当
**内容层**的 `padding-top`(**不是**根 Stack —— 壁纸必须铺到屏幕四边,根上加
padding 会把壁纸一起缩进去,黑边只是换个地方出现);底栏与内容末尾让开手势条。
reserve 收成**一个** `recomputeNavReserve()`,宽度变化与避让变化两个触发点共用
(转屏只改避让不改宽度,各写一份迟早漏一个)。
## 判据:`test/harmony-window.test.mjs`(8 条)
钉的是"两半必须同时存在"——**只钉一半的话,"退回某一半"照样能全绿通过**,
而那次退回恰恰就是删了一半。纯逻辑 3 条(换算/取不到就是 0 不猜/键名是常量)
+ 接线 4 条(全屏在、避让在、布局真消费了值、纯逻辑模块不许 import SDK)
+ 设备行为 1 条(全屏没把应用搞成白屏)。
**变异自检两个方向都跑过**(这是本轮最该记的一步):
- 删掉 `setWindowLayoutFullScreen`(重演 6861934)⇒ 2 条红 ✓
- 删掉 `getWindowAvoidArea`(只全屏不让位)⇒ 1 条红 ✓
★ 第一版判据**锚错了**:正则直接扫全文,而注释里正好有 `setWindowLayoutFullScreen(true)`
这个串 —— 把真正的调用删掉后判据**仍然全绿**。锚落在"代码对自己的描述"上了。
加 `stripped()` 剥注释后,变异才咬得住。这条与仓里那条"判据的锚不能落在
被守对象的自述上"是同一件事,这次是它的实例。
## 顺带修的两条既有判据(不是放宽,是它们把"当时的字符串"当成了"要守的坑")
- `harmony-nav` ④:留白断言写死了 `bottom: NAV_BAR_BOTTOM` 那个字面串。
它本来要守的是"留白靠 padding 不靠 margin"(margin 在 ArkUI 里加在宽度外面,
100% + margin 会顶出父容器)—— 那是另一件事。改成剥注释后验三段在不在、
离底是否**从** `NAV_BAR_BOTTOM` **起**。变异(padding→margin)仍判红 ✓
- `harmony-widescreen` ④:同上,写死了 `? 0 : NAV_CONTENT_RESERVE`。
改成"宽屏必为 0、窄屏含 NAV_CONTENT_RESERVE(可再加避让)"。
两处都是"加一个正当的避让"与"真犯那个错"会红得一模一样 —— 那就不再是守坑,
是守字符串。
另:`align-refs` / `build-stamp` / `packaging` 三条 broken 是前端 `99a2d7a`
(另一个人改的 `CalendarView.tsx`)带来的,与本轮无关,留给他。
|
|||
| 5c2ada60a2 |
文档: **"孩子管不着的东西"在因果判据里不存在** —— 比"边际递减"更硬的停止理由(§6.1.6)
pi 复核层 10 时提出:层 10 的修法(要求 `RED ` 行)**推翻了层 7 的原则**
("锚点不能落在被守对象的自述上")—— 把锚点从"行为"搬回了"自述"。
**这条我核对成立**:`:2674` 的原则与 `:2774` 的修法同住一个文件、且方向相反。
⇒ 停止理由从"成本 > 收益"升级为**"层 11 的修法必须推翻层 7 或层 10,两者互斥"**。
## ★ 但我把 pi 的建议修法①也测了 —— 得到比"原则互斥"更强的结论
**pi 的 ① 不是逃逸口。** 它主张"改坏**孩子读的外部依赖**(而非孩子自己的源码)
⇒ 通道没有可检的信号"。实测(`exitcode-selftest` + 假体 + 通道):
```
通道只盯外部 summary.py ⇒ 干净树 rc=0 ✓,改坏外部 ⇒ rc=1 ⇒ ① 有效
通道**同时**盯外部 summary.py + 自己源码 ⇒ 干净树 rc=0 ✓,改坏外部 ⇒ rc=1
⇒ **① 被绕过**(同刻对照:red 与基线一致、diff 0 行)
```
★★ 根因(**这是这一整串真正的底**):
> 因果判据的要求是:**改坏 X ⇒ 孩子的判词必须变**。
> ⇒ 这要求 X **因果上游**于孩子的判词 ⇒ **孩子必须读 X**。
> ⇒ 孩子读得到 X ⇒ **孩子能观察 X 被改坏**(同 uid、可读,`summary.py` 是 `-rw-r--r--`)。
> ⇒ **"孩子管不着的东西"被因果判据自身的定义排除掉了。**
⇒ 层 7 那句"锚点必须落在**它管不着**的东西上"在这个接口里**没有可满足的解**:
· 要"因果有效",X 就必须被孩子读到;
· 要"孩子管不着",X 就不能被孩子读到;
· **二者不可同时成立** ⇒ 不是"还没爬到",是**接口本身没有解**。
★ 唯一出路是**换接口**(父进程自己算、或进程外观测)—— 那不是"再修一条判据"。
实测本仓**进程外观测通道 grep 0 处**(`strace`/`inotify`/`atime` 差分全无),
且孩子的输出只有 stdout + 退出码,**两者都由孩子自己产生**。
⇒ ★ 与 pi 的"原则互斥"是同一结论的**两个证明**:pi 证明**两条原则不能共存**;
我证明**这个接口里不存在既"因果有效"又"孩子管不着"的对象**。
**两者都指向"停",而后者不依赖"哪条原则优先"的判断。**
## ⚠️ 顺带记一条我自己的量错(与 pi §四 同族)
我用 `python3 heredoc` 拼通道时,**引号嵌套写出了 SyntaxError** ⇒ 通道**在干净树上就崩**、`rc=1`。
我差点把它读成"通道生效"。**是 stderr 里的 `SyntaxError` 救了我。**
⇒ **"量到 ≠ 量对了"**:任何构造出的攻击,**第一句读数必须是"它在干净树上无症状吗",
且必须看 stderr,而不是只看 rc。**
## 验证
· 干净 git worktree(HEAD=7ca6ae1,仅覆盖本文件):`^RED` **0 条**、
因果红 0、对照红 0、不可归因红 0。
· ⚠️ 工作区当前 `red` 偏高是**并发会话的漂移**(`harmony-window.test.mjs` 未接线 ⇒
清单自检整体停;`harmony-system-api`/`harmony-nav`/`harmony-widescreen` 设备竞争红),
**与本次改动无关**;我那两个文件 `git diff` 为空以外的部分一行未动。
· 本提交**只改文档**:`client/electron/test/CRITERIA.md`(新增 §6.1.6)。
|
|||
| 7ca6ae14c6 |
修复: **对照能被"按内容分流"的通道骗过** —— 红必须是"报出来的"不是"退出来的"(层 10)
pi 2026-09-18 报的层 10。**成立,我复现,读数与它逐字相同。**
**同意 pi:这一串到层 10 为止,不再往上找。**
## 一、缺陷:对照只问"输入→输出是否非平凡",没问"那个输出是不是被守对象产生的"
pi 的构造:快入口**读自己的源码**、发现"我被改坏了"就 `process.exit(1)`。
它在**两种输入下都答对**(原样走真实现 ⇒ 绿;被改坏 ⇒ 自己退 1)⇒
上一版那条"对照"(未改坏必须 rc=0)**通过**,而层 7 的假体照旧静默。
实测:**`red=9`、红清单 diff 0 行** —— 与 pi 的读数逐字相同。
★ 根因:**对照只问"输入→输出映射是否非平凡",没问"那个输出是不是被守对象产生的"。**
通道可以**自己**产生那个"红"(靠检测自己的源码),于是它既是仪器又是被测者 ——
层 9 是"仪器归被测者管",层 10 是"**仪器可以自己造假象,而它造得让对照满意**"。
## 二、修法:把"红"从退出码改成可归因的报告
· ① 干净孩子必须 **rc=0 且 0 条 `RED ` 行**(健康的自检不该喊红);
· ② 改坏后的孩子必须 **rc=1 且 ≥1 条 `RED ` 行**(它得说出**为什么**)。
· ⇒ 只会 `process.exit(1)` 的通道**什么也没说** ⇒ ①过②不过 ⇒ **红**。
实测 pi 的分流通道 + 假 ok×13 ⇒ **红**:
`因果判据的红不是报出来的:exitcode-selftest 改坏后 rc=1 却一条 RED 行都没有`
★ 一般化:**"它红了吗"必须落在"它说了什么"上** ——
退出码是**任何人**都能给的(连崩了都给),而 `RED ` 行是被守对象**自己的判断**。
## 三、⚠️ 残留:通道打一行假 `RED ` 仍能过
我实测了:通道把 `process.exit(1)` 换成
`console.log('RED 假报告…'); process.exit(1)` ⇒ 同样两问都答对、`red=9`、0 条因果红。
**成本只高了一行 `console.log`。** 这与 pi §二的根因完全同形:
**通道能分辨两种输入,就答对两问。**
· pi 的 ① 方向(改坏**孩子读的外部依赖**)能穿透这个分流 —— 但我实测:
**只有 `exitcode-selftest` 读外部 `summary.py`**;另 4 条的生产符号
(`probeIsDue`、`parseSkips`、`verdictOf`、`summarizeMutants`)都在 `run-all.mjs` **自己里**。
⇒ **① 不 generalize**,它只堵 1/5。
· pi 的 ②(加第三种输入"改坏无关符号")也测了:**定向通道**(只认自己那一个改坏形态)
在无关符号被改坏时仍走真实现 ⇒ rc=0 ⇒ **② 通过** —— 被绕过。
## 四、定价与停止(§6.1.5)
层 5→10 六轮,每轮都真实、每轮读数都硬,但**边际收益在递减**:
| 层 | 攻击成本 | 修法后剩余 |
|---|---|---|
| 5/6 | 一处编辑 | 同步删多处仍静默 |
| 7 | 写假体 | 锚点到表外 |
| 8 | 删表 | 新自检漏配因果对被②抓 |
| 9 | 桩化通道 | 通道恒 rc=1 被对照抓 |
| 10 | 读自己源码的分流通道 | **打一行假 RED 仍过**(成本 +1 行) |
★ 三个结论:
1. **校准价值在层 7~9 已拿满** —— 层 10 只是在**重复同一课**。
2. **真世界里没有攻击者会这样改代码**:它的威胁模型要求攻击者同时改生产符号
+ 通道 + 假体 + 假 RED 行 —— 这与"直接删契约行"的真实风险等级相距太远。
3. **留下的东西是可复用的**(§6.1.4 的判据 + §6.1.3 的"每加一条防线回答两问"),
**其余的建议停在这里**:不承认它封死,也不再往上爬。
⇒ **同意 pi:这一串到层 10 为止。** 注意力转回到到期那 6 条判据的升级。
## 五、变异验证(都已还原)
| 变异 | 结果 |
|---|---|
| 层 10 pi 的分流通道 + 假 ok×13(修复前)→ **复现** | `red=9`、红清单 diff 0 行 |
| 层 10 pi 的分流通道 + 假 ok×13(**修复后**) | **红**:`rc=1 却一条 RED 行都没有` |
| 假 RED 行通道 + 假体(残留) | `red=9`、0 条因果红 —— **残留,已登记** |
| 通道恒 rc=1(层 9 的对照) | 红红 5 条(未被破坏) |
| 回归 层6/7/8/5/契约行删掉/M43/M45/M47/M48 | 各 1 条 |
| **`CAUSAL` 5 对逐条直验**(含新可归因判据) | 改坏后**每条** rc=1 |
| 干净树假红检查 | `^RED` **0 条**、对照红 0、不可归因红 0 |
## 六、状态
· 五个自检单独跑全 rc=0;默认跑 `checks=462 pass=458 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**。
· 本提交含 `client/electron/test/run-all.mjs`(可归因判据)与
`client/electron/test/CRITERIA.md`(新增 §6.1.4 + §6.1.5)。
· 提交前 `HEAD=5afb2c1`。
|
|||
| 5afb2c1550 |
修复: **判断据的那个人也归判据管** —— T 全桩后与基线逐字相同、层 7 攻击完整放回(层 9)
pi 2026-09-18 报的层 9。**成立,我复现,读数与它逐字相同。**
## 一、缺陷:因果判据的**观测通道**住在它自己那张可编辑的表里
层 7 我为了把因果判据的成本从 4.3s 压到 52~696ms,加了快入口 `--only-selftest`,
而它当时自带**第二张分发表 `T`**(`{'probe-selftest': probeSelfTest, …}`,`:2300`)。
我在上一封把它如实标成"**副作用、不是防线**" —— **层 9 就长在那个副作用上。** 实测:
```
T 全换成 () => 1 桩 ⇒ red=9,与基线**逐字相同**、因果 RED **0 条**
T 全桩 + 假 ok×13(层 7) ⇒ red=9、**红清单 diff 0 行**
⇒ 层 7 的攻击被完整放回,而代价只是"把 T 里 5 个值换成 () => 1"。
```
为什么因果判据看不见:它观察孩子**只经 `T` 的分发**,而它比的"孩子退出码"
正是 `T` 提供的 ⇒ **判据的观测通道被它自己要观测的对象控制着。**
⇒ 层 7 问"**被测者**的话可信吗",层 9 问"**测量者**的话可信吗"。
★ 也是本仓老形状的又一次:`T` 与 `SELFTESTS` 是**同一事实两份实现**。
(pi grep 证实:`T` ↔ `SELFTESTS` 的交叉约束 **0 处**。)
## 二、修法一:消除"两份实现"本身 —— `T` 不该单独存在
`SELFTESTS` **上移**成唯一的一份登记(原来它在块内、且在快入口**之后**),
快入口从它 `find` 派生 ⇒ **"桩化 `T`"这个动作没有落点**。
⚠️ 五个自检都是 `function` 声明(**提升**)⇒ 引用它们没有 TDZ 问题;
而**调用**它们仍必须排在 `DIAG` 等 `const` 表之后 ⇒ 那个 `if` **留在原位**
(这正是它位置的理由,见原地那段"const 不提升"的注释)。
★ 验证:`grep -c "const T = {"` ⇒ **0**;5 个 `--only-selftest` 全 rc=0;默认跑不变。
## 三、★★ 修法二(我加的,比 pi 的建议更硬):给因果判据加**对照**
pi 的 ② 是"若保留两份则加交叉约束(键相等 + **值必须是同一个函数对象**)"——
那是对的,但它**只堵 `T` 这一个形状**。我顺着想了一层:
> **只要判据只有"改坏 ⇒ 必须 rc=1"这一问,任何"恒 rc=1 的通道"都能让它绿。**
> 不管是表、是 wrapper、还是环境变量。
⇒ 加一问:**没改坏的时候,它必须 rc=0。** 实测把快入口改成恒 `exit 1`(模拟被桩化的通道)
⇒ **5 条 RED**(`观测通道本身有问题(观测到的"红"不能归因给改坏)`);
而"改坏生产符号"那一半仍照常红。
★ 这是"锚点必须落在被守对象管不着的地方"的**行为**版本:
**我不需要知道通道长什么样,只要求它在两种输入下给出两种答案。**
★★ 因此它比交叉约束**更强**:我把**重新引入一张 `T2` 并桩化**的版本也测了
⇒ 同样 **5 条对照红** —— **它堵的是"形状",不是"那一个洞"。**
⚠️ 代价:每对自检跑**两次**(默认跑 5.1s → **6.8s**,可接受)。
## 四、变异验证(都已还原)
| 变异 | 结果 |
|---|---|
| **层 9(a)** `T` 全桩(修复前) | `red=9`、逐字相同、因果红 0 —— **复现** |
| **层 9(b) 最尖**:`T` 全桩 + 假 ok×13(修复前) | `red=9`、红清单 diff **0 行** —— **复现** |
| **层 9(b) 重放(修复后)**:假 ok×13 | **红**:`把 exitcode-selftest 守的生产符号改坏之后,它仍然全绿` |
| **通道恒 rc=1**(模拟桩化) | **红 5 条**:`观测通道本身有问题` |
| **重新引入 `T2` 并桩化** | **红 5 条**(同上 ⇒ 堵形状,不只堵那个洞) |
| 回归 **层8** `CAUSAL=[]` / **层6** 掏空 / **层5** 删整条 / 契约行删掉 | 各 1 条 |
| 回归 M43 / M45 / M47 / M48 | 各 1 条 |
| **`CAUSAL` 5 对逐条直验**(含新对照) | 改坏后**每条** rc=1 |
| 干净树假红检查 | `^RED` **0 条**、对照红 0 |
## 五、★ 一条我认为该长期留着的判据(pi 建议,我采纳并写进 `CRITERIA.md`)
> **每加一条防线,要同时回答两问:**
> ① 它守什么?(设计意图)
> ② **它自己新增了哪些"必须被守"的东西?**(副作用)
>
> 层 9 就是第②问没问的产物 —— `--only-selftest` 在**性能上是净收益**
> (4.3s → 52~696ms),却往被守集合里加了一个**没人守**的成员。
⇒ 与"每根轴的底看起来都像整体的底"是同一现象的两种说法:
**防线是会长出新的被守对象的。**
## 六、⚠️ 残留(不声称层 9 封死)
· **多处同步删仍可静默**(层 5~8 的老残留,本轮未动):删 `CAUSAL` 对 + `SELFTESTS` 登记
+ CLI guard + `--only-selftest` 分支 + 函数体 + 契约项 ⇒ 仍静默。
· **对照本身也可能被绕过**:如果有人把对照那一段源码删掉,它同样没人守 ——
这与层 8 同形。我**没有**给对照再套一层(会无限递归),只把这条如实写在这里。
★ 现在的**净收益**是:绕过它需要改**两处**(删对照 + 桩化通道),而不是一处。
## 七、验证与状态
· 五个自检单独跑全 rc=0;默认跑 `checks=462 pass=458 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**。
· 本提交含 `client/electron/test/run-all.mjs`(`T` 消除 + `SELFTESTS` 上移 + 因果对照)与
`client/electron/test/CRITERIA.md`(新增 §6.1.3)。
· 提交前 `HEAD=a023286`。
|
|||
| a023286158 |
修复: 守着锚的那张表**自己没人守** —— CAUSAL = [] 与基线逐字相同,层 7 的攻击被完整放回(层 8)
pi 2026-09-18 报的层 8。**成立,我复现,读数与它逐字相同。** ## 一、缺陷:我上一条那句"表本身也被真跑锚住了"**只对了一半** 被守的是"表里的锚文本在源码里唯一",而 **"表里该有几条"没有任何东西在问**。实测: ``` CAUSAL = [](整表清空) ⇒ red=10,与基线**逐字相同**、因果 RED 0 条 只留 1 对(删掉另外 3 对) ⇒ red=10,那三条自检**从此失去因果锚**、无任何提示 CAUSAL = [] + 假 ok×13 ⇒ red=10、因果/空壳 RED **0 条** ⇒ **层 7 的攻击被完整放回来了,而代价从"改函数体"变成"删掉表里一行"。** ``` ★ 根因:**锚点不能落在"被守对象的自述"上**(层 7), 而这里是 **"守着锚的那张表自己的内容"没有锚** —— 上一轮是"判据自述",这一轮是"**锚表的自述**"。 ★ 新轴:**守护者自己进入了被守集合** —— 前几层问的都是"**被守的东西**怎样", 层 8 问的是"**守它的东西**怎样"。 ## 二、修法:覆盖面必须有一个**不在表里**的来源 用**两个**,都是已有的、不必新写表: · ① `contractEntries` —— `CRITERIA.md` 契约行(**外部文件**,与层 5 **同一个锚点**; 一个外部来源同时守两件事); · ② `SELFTESTS` 的**键**(源码结构)—— 让"新加了一条自检却没配因果对"也能被抓。 ⚠️ **空真的坑**:只写"每个 `CAUSAL` 项都对应一个真自检"(反向)**挡不住 `[]`** —— `[]` 恰好满足那个空真("每个"在空集上恒真)。⇒ 必须**正向**要求覆盖。 ★ 与本仓那句同族:**"判据存在" vs "判据在路径上"**;这里是 **"表非空" vs "表够长"**。 ## 三、⚠️ 我实现时踩的坑:**两个命名空间别混**(被我自己刚写的判据当场抓住) `wired` 装的是**函数名**(`probeSelfTest`),`CAUSAL`/契约行装的是**旗标名**(`probe-selftest`)。 我第一版拿 `wired` 比 `CAUSAL` ⇒ **5 条全报"幽灵名"、另 5 条全报"没有因果锚"** (一次红 2 条、方向相反)。⇒ **判据没错,是我把两套名字当成了一套。** 修法是改用 `SELFTESTS` 的键(旗标名,与 `CAUSAL` 同命名空间)。 ★ 教训:**同一个东西在两个地方有两套名字时,"名字相等"不是可传递的。** 同一次还抓出第二个我自己的 bug:第 5 对的锚文本在 JS 源码里只写了 `\d`, 运行时会**塌成 `d`** ⇒ 锚文本永远匹配不上(唯一性报 0,而不是 1)。 ★ 这两处都是**新判据抓新判据**——判据互相盯着是有效的。 ## 四、补第 5 对(`mutants-line-selftest`) 层 7 时表里只有 4 对,`mutantsLineSelfTest` 一直没有因果锚。 ★ 我实测了三个候选锚:`未列入清单`(7 处)、`baseline-stale`(22 处)改坏后 **rc=0** ⇒ **它不读那两处**(那是别的函数在用);只有读 `RESULT` 行那个正则改坏后 rc=1。 ⇒ 这也是"**关系表写错就会被真跑抓住**"的又一次实例。 ## 五、变异验证(都已还原) | 变异 | 结果 | |---|---| | **层 8(a)** `CAUSAL = []` | **红**:`这些自检**没有因果锚**:…(要求 5 条,因果表只有 0 对)` | | **层 8(b)** 只留 1 对 | **红**(缺 4 条) | | **层 8(c) 最尖**:`CAUSAL=[]` + 假 ok×13 | **红**(修复前 0 条) | | 幽灵名(CAUSAL 放不存在的自检名) | **红 3 条**(幽灵名 + 覆盖不全 + 因果判据退出码 2) | | **方向②** 新自检 + 登记 + 契约项但**不给因果对** | **红**(`要求 6 条,因果表只有 5 对`) | | **`CAUSAL` 5 对逐条直验**(从源码解析表,防我手抄错) | 改坏后**每条** `--only-selftest` rc=1 | | 回归 层6 掏空 / 层7 假ok×13 / 层5 删整条 / 契约行删掉 | 各 1 条 | | 回归 M43 / M45 / M47 / M48 / 名字级未接线 | 各 1 条 | | 干净树假红检查 | `^RED` **0 条**、因果红 0、覆盖红 0 | ## 六、⚠️ 残留(如实登记,不声称层 8 封死) · **多处同步删仍可静默**:把 `CAUSAL` 对 + `SELFTESTS` 登记 + CLI guard + **`--only-selftest` 快入口那一项** + 函数体 + 契约项**全部**删掉 ⇒ 实测 `red=9`、`^RED ` **0 条**(与基线相同)、`自检段:4 个自检`。 ⇒ 与层 5/6/7 同形:我把成本从一次编辑抬到**五六处**,**性质没变**。 · ★ 顺带如实记一笔:**我的层 7 修法自己新增了一个引用点**(`--only-selftest` 的名字表)—— 它让删除**更贵了一点**(要一起删),但它本身也是一个新的"必须同步删的地方"。 这不是设计意图,是副作用,写在这里以免下一轮把它当成"防线"。 ## 七、验证与状态 · 五个自检单独跑全 rc=0;默认跑 `checks=462 pass=458 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red`、 `mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**。 · ⚠️ 口径变化**不是**本提交造成的,是**另一个会话**的提交 `f2cddf4` (`harmony-deviceprobe` 期望条数 8→**11**,加了 3 条包名漂移回归锁)⇒ `checks 459→462`; 同一次也修掉了 `harmony-nav` 的**设备占用红**(那条红曾连红 42 轮)⇒ `fail 5→4`、`red 10→9`。 ⇒ 我这次没有引入新的红,也没有消掉别人的红。 · 本提交含 `client/electron/test/run-all.mjs`(第 5 对 + 覆盖判据)与 `client/electron/test/CRITERIA.md`(新增 §6.1.2)。 · 提交前 `HEAD=f2cddf4`。 |
|||
| f2cddf41d2 |
修复: 包名从 com.agentmail.harmony 改成 com.jianf.agentmail 后,有一条行为判据**永远走"设备忙"跳过**
被发现的方式值得记:那条判据从没红过(它从不执行断言),是**"跳过也要有界"**
那条闹钟把 42 轮连续跳过顶成红,才露出来的。这正是设界要抓的形状 ——
判据既不算红也不算绿 ⇒ 永远不必被升级。
## 根因:包名有四处字面量,改名只改了三处
AGC 拒绝 `com.agentmail.harmony`(`harmony` 是包名保留字),于是改成
`com.jianf.agentmail`(见 `docs/ALIGN-REFS.json` 的 `agc.packageName` 与
`align-refs.test` 那条一致性判据)。但那个字面量在判据里是**各自抄的**:
· `AppScope/app.json5` ← 唯一权威
· `harmony-deviceprobe.test.mjs:24` ← 改了(它判 AGC 匹配)
· `harmony-nav.test.mjs:810` ← **没改** ⇒ 前台判定永不成立
· `lib/harmony-device.mjs:280` 注释 ← 没改(只是注释,但同样在传播旧名)
于是 `foregroundBundle(hdc) === 'com.agentmail.harmony'` 恒假,行为判据每次
都记一次"设备忙"。账本 `.tmp/harmony-busy-skips.json` 实测数到 **45**。
## 修法
1. `lib/harmony-device.mjs` 新增 `ourBundle()` —— 从 `AppScope/app.json5`
**唯一权威处**读包名(惰性、读不到返回 null,由调用方决定)。
2. `harmony-nav.test.mjs` 的比较改成 `fg !== ourBundle()`。
3. 清除 `.tmp/harmony-busy-skips.json` 里那 45 轮**假忙计数** ——
它们不是设备争用,是字面量漂移,留着会污染"跳过余额"的含义。
## 判据(防止这一类漂移再发生)
`harmony-deviceprobe.test.mjs` 新增 3 条(8 → 11,已同步 run-all 棘轮):
· 判据目录里**不许再出现包名字面量**(除 lib/ 唯一读取处与本文件——它要拿
真机样本 `fixtures/aa-dump-l-*.txt` 里的包名做**逐字**对照,属具名例外)
· 判据自检:ourBundle() 读得出值、且不带 `harmony`(AGC 保留字)
· 变异自检:把一条消费方改回硬编码 ⇒ 必须判红(已验证)
且注释里的历史说明**不许误红**(`align-refs` 正靠注释解释为什么换名)
验证:harmony-deviceprobe 11/0、harmony-nav 18/0(行为那条**真跑过**、
不再是跳过)、criteria-hygiene 6/0。
|
|||
| 72f35ff9cd |
修复: 条数**可以伪造** —— 打满 13 条空 ok 就绕过了层 6 的下界(层 7:锚点不能落在自述上)
pi 2026-09-18 报的层 7。**成立,我复现,读数与它逐字相同。** ## 一、缺陷:下界判据的输入是"自我报告" 层 6 我加的是"比条数下界",而条数是从**输出**里数的 —— **数的是它"打了几条",不是它"验了几条"**。于是把 `exitcodeSelfTest` 的体换成 "循环打 13 条空 `ok`、`return 0`"(不打任何真断言): ``` 基线: checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red 假 ok×13: checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red ← 逐字相同 下界相关 RED: 0 条(13 ≥ 下界 12 ⇒ 相等,无从对比) ``` **最尖形式**:假 ok×13 **+** `UPSTREAM_RC['baseline-residue']=99`(= 它本该抓的那个) ⇒ 0 条 RED、0 次提及 UPSTREAM。 **对照**(不掏空、只改值)⇒ `red=9→10`,真报"与真跑出来的 rc=1 不符"(⇒ 掏空是唯一原因)。 ★ 根因:**下界判据的输入是"自述"** —— 判据自己说它做了多少,而它可以撒谎。 **锚点不能落在"被守对象的自述"上,必须落在它管不着的东西上。** ## 二、三种"看起来更硬"的替代我都否掉了(理由:仍在自述这一侧) · **黄金输出**(逐字节比对期望输出)= 又一份**手写数据**(本仓刚证过那族可被同步改); · **扫源码找关键词**= "**提到 vs 就是**"(本仓反复证过它脆); · **毒化环境变量**(该模式下每条自检必须报红)= 把"必须红"的断言写在**自检函数体内** ⇒ **假体不打它**。★ 我实测过:模拟毒化后假体 `red=9`、下界红 0 —— 与层 7 同一问题。 ## 三、修法:**因果锚点** —— 改坏被守的生产符号,那条自检必须红 ``` · 假体(空 `ok`)在符号被改坏时仍然打 ok ⇒ 它不红 ⇒ **红**(判据抓它); · 真自检读了那个符号 ⇒ 符号坏 ⇒ 它报 RED ⇒ 绿。 ⇒ 这是"自检真的读了那个符号"的**行为**证据,不是它的自述。 ``` 实现: · 快入口 `--only-selftest=<名>`(**实测 52~696ms/条**,对比 `--X-selftest` 要跑整套 suite 的 4.3~4.9s); · 隔离用 `mkdtempSync` + `cpSync(join(HERE), …)`(**只拷 `test/`,1.6MB / 6ms**); · `CAUSAL` 表:`probe-selftest`→`probeIsDue`、`skip-selftest`→`parseSkips` 的正则、 `verdict-selftest`→`verdictOf` 的返回式、`exitcode-selftest`→`UPSTREAM_RC['baseline-residue']`。 ★ **关系表本身也被真跑锚住**(这是它与"下界数字"的关键区别): 表里每一对都必须实测"改坏了它真会红";把符号写错(写成它不读的)⇒ 那条自检改坏后**不红** ⇒ **红**。⇒ **表自己也被因果判据守着。** ## 四、我实现时踩的两个坑(都记在注释里) ① **必须拷整个 `test/`**:run-all 的**自检 1/2** 要 `readdirSync(test)` 与 `SUITE` 对齐, 缺文件会让它在跑自检**之前**就 `exit(1)` ⇒ **读到的是假红**。 我第一版少拷东西时就这么读错过:`rc=1` 看着像"改坏生效",其实是清单自检停了整套。 这与本仓"隔离要用**最小**夹具"不冲突 —— 最小**自洽**的夹具,不是最小字节数。 ② **锚文本的唯一性必须在除去 `CAUSAL` 表本身**的源码里数: 我第一版没排除 ⇒ `probe`/`verdict` 立刻报"锚文本不唯一",而真因是**表里那一行字面量**, 不是生产里有两处。★ 又是那句:**"提到"与"就是"在文本上长得一样。** ## 五、变异验证(都已还原) | 变异 | 结果 | |---|---| | **层 7** 假 ok×13(exitcodeSelfTest) | **红**:`把 \`exitcode-selftest\` 守的生产符号改坏之后,它**仍然全绿** ⇒ 它没在读那个符号(或它是个空壳)`,`red=9→11` | | **层 7 最尖**:假 ok×13 + `UPSTREAM_RC`=99 | **红**(同上,修复前 0 条) | | 回归 **层6** 掏空 | 1 条(`只打出 0 条断言`) | | 回归 **层5** 删整条 `skipSelfTest` | 1 条(契约要求而 `SELFTESTS` 缺) | | 回归 **M43 / M45 / M47 / M48 / 名字级未接线** | 各 1 条 | | 干净树假红检查 | `^RED` **0 条**、因果红 **0 条** | | `CAUSAL` 表 4 对逐条直验 | 改坏后每条 `--only-selftest` rc=1(未改坏 rc=0) | ## 六、⚠️ 残留(如实登记) · **跨文件同步删 `CAUSAL` 表项 + 对应生产符号**仍可绕过(与层 5/6 同形:我把成本从 一次编辑抬到两次,性质没变)⇒ **不声称层 7 封死**。 · 另外:`CRITERIA.md` 里那三名"下界"仍可被改小(层 6 的残留), 但**现在它不再单独构成绕过** —— 改小下界后仍要过因果判据(假体不读符号 ⇒ 红)。 ## 七、验证与状态 · 五个自检单独跑全 rc=0;默认跑 `checks=459 pass=454 fail=5 skip=0 red=10 broken=0 unreported=0 verdict=red`、 `mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**。 · ⚠️ `fail=4→5`、`red=9→10` **不是**本提交造成的:新增的是 `test/harmony-nav.test.mjs` 的 设备占用红(连续 **32** 轮被别的会话占着前台,`K=3` 上限后按设计报红)。 **我用 stash 证实过**:在**不含本改动**的干净 HEAD 上同样是 `red=10` + 该文件 `# fail 1`。 · 本提交含 `client/electron/test/run-all.mjs`(快入口 + `CAUSAL` 因果判据)与 `client/electron/test/CRITERIA.md`(新增 §6.1.1)。 · 提交前 `HEAD=3175ee7`。 |
|||
| 3175ee7267 |
修复: **函数体可以被掏空** —— 名字/登记/guard/契约行全在,里面不检查任何东西(层 6)
pi 2026-09-18 报的层 6。**成立,我复现,读数与它逐字相同。**
## 一、缺陷:掏空是零痕迹的
把 `exitcodeSelfTest` 的**函数体**(14616 字节)换成 `{ return 0; }`
(名字、`SELFTESTS` 登记、CLI guard、`CRITERIA.md` 契约行**一个都不动**):
```
基线: checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red
掏空: checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red ← 逐字相同
^RED: 0 条
```
**最尖的形式**:掏空 **+ 把 `UPSTREAM_RC['baseline-residue']` 改成 99**(= 它本该抓的那个)
⇒ `red=9`、**0 行提到 UPSTREAM**。
**对照(证明掏空是唯一原因)**:不掏空、只把值改成 99 ⇒ **`red=10`**,且真报出
`UPSTREAM_RC[baseline-residue]=99 与真跑出来的 rc=1 不符`。
⇒ 我们花三轮把 `UPSTREAM_RC` 锚到"真脚本真跑"上,**这个锚点可以被一次"清空函数体"无声撤掉**。
★ 根因:**层 5 及之前所有防线问的都是"它**在不在**",没有一条问"它**做了没有**"。**
层 5 修的是存在性,而存在性有**两种**失去方式:**名字没了**,和 **名字在、里面是空的**。
(★ 层 5 与层 6 是**两根轴**:层 5 问"还在吗",层 6 问"做了吗"。
它们共同的教训:**每根轴的"底"看起来都像整体的底** —— "到底了"只对当前那根轴成立。)
## 二、修法:给"判据在工作"一个**行为**下界
每跑完一条自检,记下它**真跑出来**的断言条数(`^(ok|RED) ` 行数),
与 `CRITERIA.md` 契约行里声明的**下界**比 ⇒ 掏空 ⇒ 条数掉到 0 ⇒ **红**。
契约项写成 `名字:下界`(棘轮语义:只增不减,掉下来才红)。
**实测条数(确定性:连测 3 次 + 两个相位都相同)**:probe=3 exitcode=13 skip=5 verdict=13 mutants-line=9。
**下界取略低于实测的余量**:`3 / 12 / 5 / 12 / 9`。
★ 下界写在 **`CRITERIA.md`**(外部文件、被 4 个文件引用、自己已被自检 3 守着),
不在 `SELFTESTS` 自己身上 —— 与 §6.1 同一条理由。
★ **为什么不用"恰好等于实测"**:相等的下界会让**任何**一条断言的小改动都变成
"必须同步改数字"(噪音),而留余量只拦"掉到明显不对"的那种(掏空 ⇒ 0、删一半 ⇒ 腰斩)。
诚实说:**这也意味着"改小下界"本身就是一条绕过路径**(见 §四)。
## 三、⚠️ 一个我实测出来的真实约束(不做区分就会在别的机器上假红)
判据**只在"该自检自称绿"时**才比下界。理由:`exitcodeSelfTest` 在**拿不到降权工具**
(`runuser`/`setpriv`)的机器上会 `continue` 掉两个案例、多打一条说明 ——
那是它**故意的**行为(前提构造不出来就报红,不许静默跳过),那种机器上条数本来就不同。
⇒ 若不管"红不红"都比下界,就会在无降权工具的机器上**假红**。
(掏空仍必被抓:掏空后 `bad=0` ⇒ 自称绿 ⇒ 条数 0 ⇒ 红。)
## 四、⚠️ 残留(如实登记):**"把下界改小"是一条绕过路径**
我实测:把五条下界都改成 `1`,再把 `exitcodeSelfTest` 掏空成只打一条假 `ok`
⇒ **`^RED ` 0 条**(下界判据不响)。
⇒ 这是"**数字可以被改小**"那个老形状在**新落点**上的复现 —— 我把下界放到表外,
但**下界本身仍是一个可编辑的数字**。
★ 它比原来**贵一点**(要同时改 `CRITERIA.md` 的数字 **和** 掏空函数体,是跨文件的两处编辑),
但**性质没变**。所以**我不声称层 6 封死了**,与层 5 一样。
真要把下界也锚到行为上,得让"该打多少条"由**真跑对照**决定(例如拿一份已知坏输入要求它红),
那是独立工作。★ pi 也提过"要求每条自检必须能被某个坏输入弄红"(更硬),
我没选它,因为它要为 5 条自检各造一份坏输入,而**"坏输入"自己又成了手写数据**(刚被证过的那族)。
## 五、我写这条判据时自己踩的坑(第 N 次同一形状)
第一版我把 `entries` 声明在 `if (contractMs.length === 1) { … }` 块**内部**,
然后在块外用 `typeof entries === 'undefined' ? [] : entries` 兜 ——
那**永远取到 `[]`** ⇒ 下界判据**静默不跑**。
★ **一个"以防万一"的兜底写法,把判据本身变成了空判据** ——
比不写更坏,因为它**看起来在**。已改成同作用域声明 + 不留兜底。
⇒ 教训与 §6.1 那句"存在性锚点"合起来是同一句:**判据不跑时,谁来喊?**
## 六、变异验证(都已还原)
| 变异 | 结果 |
|---|---|
| **掏空 `exitcodeSelfTest`** | **红**:`自检 --exitcode-selftest **自称绿,却只打出 0 条断言**(契约下界 12)⇒ 它可能被**掏空**了`,`red=9→10` |
| **掏空 + `UPSTREAM_RC` 改 99** | **红**(同上;修复前 0 条) |
| 契约行整体删掉 | **红**(`契约行有 0 条,要求恰好 1 条`) |
| 下界写成非数字 `x` | **红**(`没写下界(或下界不是 ≥1 的整数)`) |
| **下界改 1 + 掏空成假 ok** | **不红** ⇒ §四 的残留,如实登记 |
| 回归 **层5** 删整条 `skipSelfTest` | 1 条(契约要求而 `SELFTESTS` 缺) |
| 回归 **M43** 窄正则 / **M45** 只删样本 / **M47** 标签撒谎 / **M48** 空见证 / 名字级未接线 | 各 1 条 |
## 七、验证与状态
· 五个自检单独跑全 rc=0;默认跑 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**。
· 本提交含 `client/electron/test/CRITERIA.md`(§6.1 补层 6 说明 + 契约行加下界)与 `client/electron/test/run-all.mjs`。
· 提交前 `HEAD=ecefd50`。
|
|||
| ecefd50791 |
修复: 一整条自检(函数体+登记+guard)三处同步删掉是**零痕迹** —— SELFTESTS 没有下界
pi 2026-09-18 报的层 5。**成立,我复现,读数与它逐字相同。**
## 一、缺陷:三道防线都在问"出现的东西对不对"
| 变异 | 结果 |
|---|---|
| **H1** 删整条 `skipSelfTest`(函数体 + 登记 + CLI guard 三处同步) | `red=9 … verdict=red` 与基线**逐字相同**、相关 RED **0 条**,只有报文里 `5 个自检`→`4 个自检` |
| **H1'** 最尖形式:删 `exitcodeSelfTest` —— **守着 `UPSTREAM_RC`"真脚本真跑"锚点的那个** | 同样零痕迹 ⇒ 两轮建起来的真跑锚点可被一次编辑无声撤掉 |
| **H2** 只删登记(函数与 guard 留着) | `red=11`(名字级扫描 + "写了没接线"两条防线抓住)⇒ **不静默** |
| **H3** 只删函数体 | `ReferenceError`、无 `RESULT` ⇒ **响的**,不是静默洞 |
★ 为什么三道防线都看不见 H1:
· **名字级扫描**:整条删掉时那个名字**两边都不出现** ⇒ 无从对比;
· **"写了没接线"**:`declaredSelfTests(源码)` 与 `wired` **两边同时缩小** ⇒ 相等;
· **样本/见证/要求**:样本表**内部**三字段,与 `SELFTESTS` 无关。
⇒ 根因一句话:**`SELFTESTS` 没有任何下界,也没有"必须存在哪几个"的外部清单;
所有防线问的都是"出现的东西对不对",没有一条问"该出现的东西在不在"。**
★ 这与本仓**自检 1/2** 是同一句话的不同对象:那两条判"清单里的文件必须真的存在"、
"`test/` 下每个 `*.test.mjs` 都要在清单里"—— **它们有外部依据(磁盘上的文件)**,
而 `SELFTESTS` 的"该有哪几个"**只由它自己说了算**。**同一句话,换个对象就漏了。**
## 二、修法:把"必须存在哪几条"锚到 `SELFTESTS` 之外
`CRITERIA.md` §6.1 写一段契约(本文件是**另一个文件**,且它自己已被本仓**自检 3** 守着:
存在性 + 关键条目在),并放一行 `<!-- selftests: … -->`;`run-all.mjs` 读它,
要求与 `SELFTESTS` 的键**恰好相等**(两个方向都判)。
⇒ 链条:**被外部守着的文本 → flag 名 → guard → SELFTESTS**,锚点不在表自己身上。
删掉一条自检 ⇒ 名单没跟着删 ⇒ **红**;"名单也一起删"变成**跨文件**编辑(见下 §四)。
## 三、★★ 我没能用 pi 建议的①,因为它的**前提是假的**
pi 建议"拿对外的 flag 契约当锚点",理由写的是"这五个 flag 名是**对外的**(有人在用、有文档)"。
**我实测:不是。** 全仓(排除 `.git`)搜这五个 flag ⇒ **外部消费者 0、文档 0**。
⇒ 所谓"对外契约"**不是已有的,是需要造出来的**;直接用会把锚点建在一个不存在的保证上。
★ 所以我把它落在 `CRITERIA.md`(**确实**是别的文件、**确实**有 4 个文件引用它、
且**确实**被自检 3 守着),而不是落在"它自己新写的一张内部清单"。
## 四、⚠️ 残留(如实登记):**跨文件的同步删仍然静默**
实测:**同时**删 `run-all.mjs` 的三处 **和** `CRITERIA.md` 的那一项 ⇒ `^RED ` **0 条**。
⇒ 本次修法把"改我自己的一张表"抬成了"**改两个文件**",
**难度上去了,但性质没变**(仍是可同步的、无真跑锚点的编辑)。
★ 按这条线的规律,我**不**声称这一层封死了 —— 只是把成本从一次编辑抬到两次编辑。
真要把存在性也锚到行为上,得让"删掉一条自检"在**真跑**里留下痕迹(例如对外契约由
`npm test` 的入口兑现),那是独立工作。
## 五、我写这条判据时踩的坑(同一形状的第 N 次)
契约行我第一版的正则没锚行首行尾,而 §6.1 的**正文里举例提到了**那个标记
⇒ 正则**先命中了注释里那次**(捕获到 `…`)⇒ 干净树上立刻 2 条**假红**。
★ 正是本文件反复写的形状:**"提到"与"就是"在文本上长得一样。**
⇒ 修法:锚 `^…$` + **要求恰好一条**(多出来也红)—— 现在契约行自己也有唯一性判据。
## 六、变异与回归(都已还原)
| 变异 | 结果 |
|---|---|
| **H1 / H1'** 三处同步删 | **红**:`契约里要求存在的自检在 SELFTESTS 里没有:skip-selftest / exitcode-selftest`,`red=9→10` |
| 契约里写一个不存在的自检(反方向) | **红**(`verdict-selftest`) |
| 契约行 0 条 / 2 条 | **红**(要求恰好 1 条) |
| 回归 **M43** 窄正则 | 1 条认不出 |
| 回归 **M45** 只删样本 | 1 条"没有样本"(★ 我第一次跑成 0 条,是**我的脚本**同时删了 `REQUIRED_FORMS` 的名字 ⇒ 那是共漂移不是 M45;改正后 1 条) |
| 回归 **M47** 标签撒谎 / **M48** 空见证 | 各 1 条 |
| 回归 名字级(未接线自检) | 1 条 |
## 七、验证与状态
· 五个自检单独跑仍全 rc=0;默认跑 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**(无假红)。
· 本提交含 `client/electron/test/CRITERIA.md`(新增 §6.1 + 契约行)与 `client/electron/test/run-all.mjs`。
· `HEAD=dc261ad`(提交前)。
|
|||
| dc261ad537 |
修复: 三张手写表互相比对、没有外部锚点 —— 同步删「箭头函数」后 red 与基线**逐字相同**
pi 2026-09-18 报的。**成立,我复现,读数与它逐字相同**;而且我测出
**pi 建议的补法①也堵不住它**,所以换了锚点。
## 一、缺陷:同步修改 = 零痕迹
(a)把「箭头函数」从 `REQUIRED_FORMS` 与 `FORM_SAMPLES` **同步**删掉
⇒ `red=9`、`broken=0`、`verdict=red`,与基线**逐字相同**,样本表相关 RED **0 条**,
只有成功报文里 5 变成 4。
(b)更尖:连 `declaredSelfTests` 里 `const` 那条发现规则**一起**删(四字段全同步)
⇒ 同样 **0 条**。而后果是实的:那种写法的自检将来**既不跑也不报警**。
★ 根因(pi 的概括我认同):`REQUIRED_FORMS`/`FORM_SAMPLES`/见证**三张都是手写的、互相比对**
——三张同步改就一致地"对",**没有任何外部锚点**。
对照 `UPSTREAM_RC`:它当年止住共漂移,是因为它比对的是**真脚本真跑出来的 rc**,
不是另一张手写表。**"具名"这个形式救不了它** ——
`length >= 5` 与 `REQUIRED_FORMS` 在"被同步修改"这一点上**难度相同**(都是一次编辑)。
⚠️ 因此我 M45 注释里那句"具名清单让放弃支持变成**看得见的声明**"**被实测推翻**,
已在该处更正(保留原文与推翻它的读数,免得下一个人再信一次)。
## 二、为什么不用 pi 的补法①:它也堵不住
pi 建议"每条正则都必须有只满足它的样本"。我实测:**样本是规则的字段**,
删规则时它自己的样本一起消失 ⇒ 要求跟着消失 ⇒ 仍然 0 条红。
(我把它单独跑了一遍:`check(两条规则) = []`、`check(只剩 function) = []`,两行都是空。)
## 三、修法:**名字级锚点**(与写法无关,且不落到手写表上)
源码里出现的**任何** `\w+SelfTest` 标识符,都必须在 `SELFTESTS` 里 —— **不解析声明语法**。
于是它与 `function`/`const`/`async`/箭头/空格**全部无关**:
发现规则认不出某种写法时,那种写法的自检名**仍会被这个名字级判据看见**。
★ 唯一例外是样本内容用的假名(`fooSelfTest`),**显式登记**在 `SAMPLE_FAKE_NAMES`
(另加反向检查:登记的假名在源码里找不到 ⇒ 红,登记该清了)。
**这一跳的方向与 `UPSTREAM_RC` 一致**:锚点从"另一份手写数据"挪到**源码事实**
(`SELFTESTS` 里存的是真函数,`fn.name` 是运行时读数)。
## 四、变异验证(都已还原)
| 变异 | 结果 |
|---|---|
| **pi 的 (b)**:删 `const` 规则 + 四字段同步 + 未接线箭头自检 | **红**:`这些 *SelfTest 标识符出现在源码里却没接线(名字级扫描,与写法无关):silentArrowSelfTest`,`red=9→10`(**修复前 0 条 RED、一字不提**) |
| **写法无关性**:**两条规则全删** + 四种写法(箭头/async/名后空格/const=async)的未接线自检 | **红**:四条**全部**点名(`aSelfTest、bSelfTest、cSelfTest、dSelfTest`) |
| 回归 **M47** 标签撒谎 | 仍 **1 条红** |
| 干净树假红检查 | 名字级红 **0 条**、`red=9`(无假红) |
## 五、我踩的坑(第三次同一个)
加这个名字级块时又**用了声明在后面/不存在的变量**:
先 `ReferenceError: selfSrc is not defined`(我早前的编辑把 `selfSrc` 挪走了),
再是 `wired` 的 TDZ。与前两次同族:**新加的接线/判据必须排在它读的所有 `const` 之后**。
⇒ 顺手在这个块前显式定义 `selfSource`,并写清它是**本作用域**的读数。
## 六、验证与状态
· 五个自检单独跑仍全 rc=0;默认跑 `checks=459 pass=455 fail=4 skip=0 red=9 … verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**。
· 残留全清;`git status` 仅本文件;`HEAD=16cbc02`。
|
|||
| 16cbc02421 |
修复: 样本的**标签可以撒谎** —— 覆盖表声称覆盖箭头函数,而那份样本根本不是箭头函数
我自己变异出来的(M47),是 M45 的下一层。M45 修的是"样本**少了**要红",
但**名字对不上内容**当时无人管。
## 一、缺陷:标签是散文,内容没人核
实测:把 `['箭头函数', 'const fooSelfTest = () => {};']` 的**内容**换成
`'function fooSelfTest() {}'`(**标签不动**、样本个数不变)
⇒ 默认跑 **0 条 RED**、`red` 仍是 9(与干净树逐字相同)。
★ 后果我实测过,而且它**让报警自己少说话**(不只是少覆盖):
在上面那个撒谎标签的前提下,把 `const` 那条正则删掉(⇒ **箭头写法真的认不出来了**),
报警只点名 `const = async (…`,**"箭头函数"一个字都不提**(`grep -c` = **0**)。
⇒ 覆盖表**声称**覆盖了箭头函数,而报警里那一格**消失了**。
**该说话的地方没说话,而账面上看不出少了什么** —— 我们这一路最怕的那个形状。
★ 与前面几层的关系(**每修一层,缺口换一层**):
· `957ec5e`:发现规则**看不见某种写法**;
· `8691940`:样本表**可以静默缩水**(M45);
· 本次:样本表**可以撒谎**(M47)—— 表上的名字与内容**没有任何关系**。
## 二、修法:给每条样本配一条**见证**(结构,不是散文)
每个样本从 `[名字, 内容]` 变成 `[名字, 内容, 见证]`,见证是"这条样本**凭什么**算那种写法"的
**机械**判据,并判两个方向:
① 内容必须满足自己的见证(⇒ **标签与内容不符 = 红**);
② 见证必须**能区分**:至少要能否掉另一条样本(否则 `/./` 这种空见证也算"覆盖")。
⇒ 把"这条样本是哪一种写法"从**散文**挪进**结构**。
## 三、变异验证(都已还原)
| 变异 | 结果 |
|---|---|
| **M47** 箭头样本内容换成 function 声明(标签不动) | **红**:`这些样本标签与内容不符:箭头函数 ⇒ 覆盖表在说谎,而报警会因此少说话` |
| **M48** 见证退化成 `/./`(对谁都成立) | **红**:`这些见证区分不了任何东西:箭头函数 ⇒ 见证表自己退化成"永远通过"` |
| 复合:**M47 + 箭头支持真坏** | 两条红都出:先抓撒谎,再点名 `const = async (…`(⇒ 撒谎不再能掩盖覆盖) |
| 回归 **M45** 删样本 | 仍 **1 条报警** |
| 回归 **M43** 退化正则 | 仍 **1 条认不出** |
## 四、验证与状态
· 五个自检单独跑仍全 rc=0;默认跑 `checks=459 pass=455 fail=4 skip=0 red=9 … verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;
自检段现为 `…发现规则在 5 种写法上都有对照(要求 5 种,缺一即红;每条样本都带能区分它自己的见证)`。
· 残留全清;`git status` 仅本文件;`HEAD=8691940`。
· pi 的 `cc8beb7`(它插件那例)我已复核;它目录一行未动(那两处实验都在 `/tmp` 做)。
|
|||
| 8691940505 |
修复: 反面对照的**样本表自己没人守** —— 删掉 5 个样本里的 3 个,默认跑 0 条 RED、静默缩水
我自己变异出来的(M45)。这是 `957ec5e` 新加那道防线**内部**的缺口, 而且缺口长在"**覆盖**"这件事本身上。 ## 一、缺陷:`发现规则在 N 种写法上都有对照` 当时是**散文**,不是判据 实测:把 `FORM_SAMPLES` 的 5 个样本**删掉 3 个**(只留 `function 声明` 与 `const = async`) ⇒ 默认跑 **0 条 RED**、`red` 仍是 9(与干净树逐字相同), 只是成功报文里那半句从"在 **5** 种写法上都有对照"变成"在 **2** 种写法上…"。 ⇒ 它把 N 打出来,而 **N 变小没有任何东西拦**。 而"样本齐不齐"正是这条防线**唯一的防线** —— 样本可以被悄悄删光, 防线的覆盖就静默退回到"只验我恰好留下的那一种"。 **这是"判据存在 vs 判据在路径上"的又一个变体:样本在,但"样本够不够"没有判据。** ★ 与我上一轮那条同源、但换了一层:`957ec5e` 修的是"**发现规则看不见某种写法**", 这次是"**样本表可以静默缩水**" —— 修掉前者之后,后者的存在才显出来。 **每修一层,缺口换一层**,这与 `baseline` 那条线"接缝会移动"是同一个观察。 ## 二、修法:要求清单**具名**,两个方向都判 · 列出 `REQUIRED_FORMS`(要覆盖哪些写法,**具名**); · ① 每种要求的写法都必须有样本(⇒ **删样本 = 红,并指名删的是哪一种**); · ② 每个样本都必须能被认出来(原有方向); · ③ 多出来的样本未登记进 `REQUIRED_FORMS` ⇒ 也红(加了样本却没说守哪种写法)。 ★ 为什么不用 `assert length >= 5`:那个数**自己**就是可以随手改小的常量, 改小它不会留下任何痕迹。具名清单让"我要放弃支持箭头函数写法"变成一次**看得见的声明**。 成功报文也随之带上"(要求 5 种,缺一即红)",让**要求**和**实际**同时可见。 ## 三、变异验证(都已还原) | 变异 | 结果 | |---|---| | **M45** 删掉 3 个样本 | **红**:`这些写法要求覆盖、却没有样本:箭头函数、async function、名与括号间有空格`,`red=9→10`(**修复前 0 条 RED**) | | **M46** 加一个未登记的多余样本 | **红** 2 条:`不在要求清单里` + `发现规则认不出这些写法` | | 回归:**M40/M41/M42** 箭头/async/带空格三种未接线写法 | 各 **1 条报警**(`957ec5e` 的修复未被破坏) | | 回归:**M43** 发现规则退化回窄正则 | **红**:`认不出…` | ## 四、验证与状态 · 五个自检单独跑仍全 rc=0;默认跑 `checks=459 pass=455 fail=4 skip=0 red=9 … verdict=red`、 `mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`; 自检段现为 `…发现规则在 5 种写法上都有对照(要求 5 种,缺一即红)`。 · 残留全清;`git status` 仅本文件;`HEAD=957ec5e`。 · 同期 pi 已把我在它插件里报的那例修好并提交 `cc8beb7`(`--self-check` 改进程内 + 默认路径先跑 + 量纲分三档)。我**复核过**它:干净树 rc=0 且自检真出现在默认路径上,变异 `findDuplicates → []` ⇒ `npm test` **rc=3** 并打出"工具坏了,不是被测代码坏了"。它的目录我一行未动。 |
|||
| 957ec5ec77 |
修复: SELFTESTS 接线防线只认一种写法 —— 箭头/async/带空格三种形式**既不跑也不报警**
pi 2026-09-18 报的。**成立,我实测复现**;这是 `765b77e` 新加那道防线**内部**的缺口。
## 一、缺陷:防线的正则只认一种写法
原实现内联了一条 `^function (\w*SelfTest)\(\)`。四种写法实测:
| 写法 | 是否被扫到 |
|---|---|
| `function fooSelfTest() {}` | **✓** |
| `const fooSelfTest = () => {}` | **✗ 漏** |
| `async function fooSelfTest() {}` | **✗ 漏** |
| `function fooSelfTest (a) {}`(名后有空格) | **✗ 漏** |
端到端后果(我复现):插一个**箭头形式**的未接线自检,其 body 若跑会打 RED ⇒
body 的 RED **0 次**(没跑)、报警 **0 条**(也没说"没接线")⇒ **既不跑、也不报警**。
而这正是这条防线**自己存在的理由**("防写了自检却没接线"):
它对**一种写法**有效,对另一种**沉默**。
⚠️ 我第一遍数"报警次数"时得到 **1**,差点当成"它报了" ——
那 1 次命中的是成功报文里的 `(无"写了没接线")`,**是同一句话的另一个方向**。
精确判据要看 `^RED ` 行:**0 条**。**数命中 = 数到的是词,不是行为。**
## 二、修法:唯一实现 + 反面对照
1. 抽出 `declaredSelfTests(code)`,**唯一实现**,覆盖常见写法:
`^[ \t]*(?:async\s+)?function\s+(\w*SelfTest)\s*\(` 与
`^[ \t]*const\s+(\w*SelfTest)\s*=\s*(?:async\s*)?\(?`。
2. **反面对照**(pi 建议,我照做):5 种合成样本要求**每种都被认出**;
另加反方向"收进了不以 `SelfTest` 结尾的名字"。
样本与生产扫描调的是**同一个函数**(不抄副本)。
3. 成功报文带上"发现规则在 N 种写法上都有对照",让覆盖数**看得见**。
## 三★ 我修的时候引入过一个更坏的机制,实测后回退了
我第一版为"别咬到字符串里的同名文字"加了 `stripStrings`。**实测它更坏**:
`read.mjs` 自己写明 `stripStrings` **不区分正则字面量**,一个**不配对**的引号就能把
后面一大段当字符串吞掉,方向是**假绿**。实测插一条 `const QUOTE_RE = /["']/;`
在自检声明之前 ⇒ 后面**所有真声明消失** ⇒ 未接线的 `hiddenSelfTest` **静默漏掉**,
而 5 个真函数反过来变成"幽灵"(报红,但**报的是错的那件事**)。
⇒ 为防"假红"引入了"假绿 + 误报",**不划算**。已回退为只用 `stripComments`
(代价仅是"字符串里恰好写着 `function xSelfTest()` 会假红"——
按本仓口径:**假红是噪音,假绿是绕过**,选噪音)。回退后复测:那个陷阱消失(`hiddenSelfTest` 被抓)。
## 四、变异验证(都已还原)
| 变异 | 结果 |
|---|---|
| **M40** 箭头形式的未接线自检(**pi 报的那条**) | **红**:`写了但没接线:arrowSelfTest` |
| **M41** `async function` 形式 | **红**:指名 `asyncSelfTest` |
| **M42** 名后有空格 | **红**:指名 `spaceSelfTest` |
| **M43** 发现规则**退化回窄正则** | **红**:`认不出…箭头函数、async function、名与括号间有空格、const = async (…`(⇒ 样本真的会红,不是摆设) |
| **M44** 发现规则**过宽**(连 `applyMutants`/`verdictOf` 都收) | **红**:`收进了不以 SelfTest 结尾的名字:…` + 幽灵清单 |
| 不配对引号的正则字面量(`stripStrings` 陷阱) | 回退前**静默漏掉**;回退后**红** |
## 五、验证与状态
· 五个自检单独跑仍全 rc=0;默认跑 `checks=459 pass=455 fail=4 skip=0 red=9 … verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;
自检段新增"发现规则在 5 种写法上都有对照"。
· 耗时 ~5162ms(与 `765b77e` 的 ~4917ms 同量级)。
· 残留全清;`git status` 仅本文件;`HEAD=765b77e`。
|
|||
| 765b77ea06 |
修复: 五个自检**全仓没有任何入口调用** —— npm test 一个 flag 都不带,判据存在但不在路径上
pi 2026-09-18 报的。**成立,我独立复现了**,读数与 pi 逐字相同。
## 一、缺陷:自检本身到不了任何入口
五个自检(`probe`/`exitcode`/`skip`/`verdict`/`mutants-line`)原来**只**由
`process.argv.includes('--…')` 守着,而**全仓没有任何调用方传那个 flag**:
`npm test` = `node test/run-all.mjs && vitest run && npm run typecheck` —— 一个都不带。
实测:
· 默认跑输出里这些自检的行数 = **0**;
· 把 `UPSTREAM_RC['baseline-residue']` 改坏(表与真脚本不符)⇒ **默认跑**
`checks=459 pass=455 fail=4 skip=0 red=9 …`,与干净树**逐字相同**;带 flag 才 `rc=1`;
· 全仓 `grep` 这些 flag ⇒ **只命中 run-all.mjs 自己内部**的 5 处 guard + 注释,
没有任何调用方、没有任何文档。
⇒ **"跑得动的锚点"确实存在,但它不在任何一条会被人跑的路径上。**
★ 形状与这几轮同族,但**伪装得最好**:guard 看起来就在跑、注释声称"每次跑都钉住"、
`grep` 也找得到那一大段代码 —— **三者都不是"在路径上"的证据**。
前几轮是"结论到不了读者 / 到不了 verdict / 被关掉";
**这次是"自检本身到不了入口"** ⇒ **"判据存在 vs 判据在路径上"**。
★ 而且**方向最危险**:这五个守的恰好是最要紧的几格(`UPSTREAM_RC` 与真脚本一致、
⑤ 双向口径、跳过解析、探针三值、决策顺序)。`--probe-selftest` 从 09-14 就在,
**四天里一次没跑过**。
## 二、修法:进程内接线(pi 建议 ①,但按实测改了做法)
把五个 `if (process.argv.includes(…))` 块改成 `function …SelfTest()`,
`SELFTESTS` 里**直接调用**,红**并入 `reds`**(⇒ 进 `verdict`,不是"只打印不改颜色")。
每个函数后面紧跟一个 CLI 守卫 ⇒ **单独跑时行为完全不变**。
★★ **我没有照 pi 的原话用子进程**,因为它的代价估计不准:
pi 写"多几次进程启动(可忽略)",而我实测 —— **每个 flag 都会先把整个套件跑完**
(guard 在 SUITE 循环**之后**),默认 ~4.3s、带任意 flag ~4.3–4.9s
⇒ **5 个子进程 ≈ 22s**。而自检 body 合计只 ~950ms。
⇒ 进程内调用的实测代价:**4327ms → 4917ms(+590ms)**,而不是 +22s。
## 三、接线时我踩的两个坑(都当场红,照实记)
1. **TDZ,同一个坑第二次**:我把接线块放在定义旁边(行 ~1448),而 `DIAG` 是 `const`、
定义在 ~1721 ⇒ `verdict`/`mutants-line` 两个自检抛
`ReferenceError: Cannot access 'DIAG' before initialization`。
**这个坑我在 `--mutants-line-selftest` 上已经踩过一次**。⇒ 移到所有 `const` 表与
自检函数**之后**,并在注释里写明"任何调用自检的代码都必须排在所有定义之后"。
⚠️ 顺带一个教训:**"接线成功"与"被接的东西真跑了"是两件事** ——
我第一版接线后红数也变了,但其中两条是**抛异常**贡献的,不是自检的结论。
我是看到"(它没打出 RED 行,真因见上)"这句**自己写的话**才回去查的。
2. **吞 stdout 不能吞 stderr**:只吞 `console.log`,`console.error` 不动 ——
否则"自检抛了异常"会被我自己盖住(本仓"错误信息被吞掉"那条老教训)。
## 四★ 变异时发现的**更大一条**:判官坏了,红被静默吞掉(M39)
把 `verdictOf` 改成**恒返回 `'green'`** ⇒ 默认跑
`red=10 … verdict=green`、**exit=0**、而且**红清单一行都不打**(green 分支提前 return)。
⇒ 自检红**确实进了 `reds`**,但"决定要不要报红"的那一格**正是被变异的那一个**:
**10 条红被静默吞掉**。这条老形状这次坏的是**判官自己**。
⇒ 加一条**独立**的安全网(不靠 `verdictOf` 自证):
`reds`/`brokens`/`selfCheckFailed`/`dueFailed` 有任何一个非空而 `verdictOf` 却说绿
⇒ **判官与账本不一致**,直接 `exit(1)` 并说明。
**判据与判官互为对照,谁单独说了都不算数。**
## 五、结构防线:**"写了自检但没接线"必须红**(pi 建议 ②)
pi 那条观察很准且我照抄进注释:本文件**自检 2** 的存在理由就是"防**写好了没接线**",
而它只扫 `*.test.mjs` ⇒ 这五个自检**因为不是 `*.test.mjs` 正好扫不到**。
**同一句话,换了个对象就漏了。**
⇒ 扫自己的源码:`function *SelfTest()` 必须在 `SELFTESTS` 里被调用(`prose`/`stripComments`),
另加"登记了不存在的函数"的反向检查。
## 六、变异验证(都已还原)
| 变异 | 结果 |
|---|---|
| **破坏 `UPSTREAM_RC`**(pi 的那一条) | 默认跑 **red 9→11**,两条自检红并指名不符的码 |
| **M36** 加第 6 个自检但不登记 | **红**:`写了但没接线:sixthSelfTest` |
| **M37** `probeIsDue` → `v === true`(闸锈死) | **红**:`--probe-selftest 红了:unknown…(probeIsDue("unknown") = false)` |
| **M39** `verdictOf` 恒 green(判官坏了) | **红**:`判官与账本不一致…`(修复前 **exit=0、红清单不打印**) |
## 七、验证与状态
· 五个自检**单独跑仍全部 rc=0**(CLI 路径行为不变)。
· 默认跑:`checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓` —— 与 pi 读数一致,
并新增一行 `自检段:5 个自检已在**默认路径**内跑过…源码里 5 个 *SelfTest 函数全部在册`。
· 耗时 **4327 → 4917 ms**(进程内;子进程方案会是 ~22s)。
· 顺手改掉 pi 指出与实现相反的那句注释("每次跑都钉住"⇒ 当时是假的,现在才成立)。
· 残留全清;`git status` 仅本文件;`HEAD=62297e3`。
|
|||
| 62297e3d83 |
修复: baseline-absent 同时是"关掉残留检测"的开关 —— 一个 rm 就能让残留仍在而那条红整条消失
pi 2026-09-18 报的。**成立,我端到端复现了**,而且是这条线上同一形状的**最后一跳**。
## 一、缺陷:结论不是"到不了读者",是**被关掉了**
`baseline-absent` 原来是 `{note: null, blocksGreen: false}` —— **既不说话、也不阻断**,
而它同时是"**关掉残留检测**"的开关。同一处残留,**只切 `baseline.sha` 在不在**:
| | summary.py | 套件 | 红清单里有 residue 吗 |
|---|---|---|---|
| `baseline.sha` 在 | `diag=baseline-residue` | `red=10 … verdict=red` | **有** |
| **删掉** | `diag=baseline-absent` | `red=9 … verdict=red` | **没有**(整条消失、`rc=0`、静默) |
`git status` 里 ` D baseline.sha` 与 ` M <被改文件>` **同时都在** ⇒ **残留还在**,
而唯一能报告它的机制被一个 `rm` 关掉,**不需要特权、不会红、也不说话**。
前几轮分别是"结论到不了读者"、"结论到不了 `verdict`";**这次是"结论被关掉了"**。
## 二、修法:把"缺席"分成两态,并给非阻断码再分两类
1. **`baseline-deleted`(新码,退 1)**:`baseline.sha` **受版本控制却被删**
⇒ 那不是"可接受的配置",是**缺文件**(该恢复它)。判别用既有的东西、不新造:
`git ls-files --error-unmatch` —— 三态:**0=受跟踪 / 1=没跟踪 / 128=git 答不了**,
`128` 时**不许猜**,退回缺席语义但**必须说话**。
2. **`baseline-absent` 补 `note`**(原来 `null` ⇒ 连读者都看不到):
说清"**这一格没检查**:没有底本 ⇒ 残留检测是关着的",并区分"本来就没跟踪它"与"判不出"。
—— 这是 pi 的"最小版",我认为它**不足以**独当(沉默正是这个洞的成因),所以两条都做。
3. ★ **`DIAG` 每个码新增 `checked` 字段**(pi 那条建议的落地):
· `checked: true` = "**检查了、结论无害**"(`baseline-stale`:可以静默);
· `checked: false` = "**根本没检查**"(`baseline-absent`/`counts-unusable`/
`baseline-unrunnable`/`baseline-unknown`:**必须说话**)。
⇒ 把 pi 的"应当分两类"从**文案要求**变成**结构字段**。
## 三★ 我加第一版时漏了一条判据,自己变异出来(M34)
`checked` 只是一个字段,**它的内容当时没有任何判据守着**:我把 `baseline-absent` 的
`note` 改回 `null`(= pi 说的"最小版没做"),`--verdict-selftest` **全绿** ——
因为此前的判据只管 `blocksGreen` 存不存在、两侧口径对不对。
⇒ 补第 ⑦ 条:**`checked: false` 的码必须有 `note`**,且**每个码都必须声明 `checked`**。
又一条"判据在,但走不到":字段加上了,没人查它的内容。
## 四、变异验证(都已还原)
| 变异 | 结果 |
|---|---|
| **M32** `baseline-deleted` 退回"当配置缺席"(**即 pi 报的那个洞本身**) | **红**:`UPSTREAM_RC[baseline-deleted]=1 与真跑出来的 rc=0 不符` |
| **M33** 关掉 `bl_tracked` 判别(一律当缺席) | **红** 2 条:端到端恢复 pi 那个洞(`diag=baseline-absent`)+ 反向覆盖报"该码没案例" |
| **M34** `baseline-absent` 的 note 改回 `null` | **红**:`这些码根本没检查、却一句话都不说:baseline-absent`(**上一版这里是 rc=0**) |
| **M35** 删掉某个码的 `checked` 字段 | **红**:`这些码没声明 checked:baseline-stale` |
## 五、端到端锚点(新增一条 `--exitcode-selftest` 案例)
`runIn` 加 `'deleted'` 模式:**先提交 `baseline.sha` 纳入跟踪、再把目标文件改脏、然后 unlink**
⇒ 判 `rc=1 且 diag=baseline-deleted`。这条**就是 pi 那个实验的自动化版本**:
残留是真的存在的,而底本被删了。反向覆盖随之报 `9 个码都有案例真跑验过`。
**真树端到端复核**(真残留 + 真删 `baseline.sha`):`red=10`(修复前 9)、`verdict=red`、
红清单里出现 `(summary.py)baseline-deleted —— 底本 baseline.sha **受版本控制却被删** ⇒ **残留检测已失效**`。
## 六、验证与状态
· 五个自检全 exit 0(`--verdict-selftest` 新增第 ⑦ 条;`--exitcode-selftest` 11→12 条案例、9 个码全覆盖)。
· 正常态与 pi 读数一致:`checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`。
· `baseline.sha` 的 7 条都是鸿蒙源码文件,**不含** `summary.py`/`run-all.mjs`
⇒ 本次改动**不需要重算底本**(`baseline=7/7✓` 亦为证)。
· 残留全清;`git status` 仅这两个文件;`HEAD=2870d47`。
|