009ea172b789395d9fcce5f2128bdfcbc8f75617
427 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 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)` ⇒ 红 ✓。
|
|||
| 2da38bba83 |
农历走服务端端点:换算只在服务端做一次(两边各写一遍天文算法迟早差一天)
用户定的方案:「加 api 端点」。
## 为什么不移植到 ArkTS
`lunar-javascript` 的 `lunar.js` 有 **43 万字节**,内部是**日月位置的级数展开**
(实测:全文件最大的数字字面量是 16KB 的系数数组,**不是**"某年到某年的月长表")。
即"照搬一张小数据表"这条路**不存在** —— 移植等于在 ArkTS 里再实现一遍天文算法。
两份实现迟早会在某个闰月或某个朔日上差一天,而那种错**表现为日期错位、不是报错**,
界面上完全看不出(用户得自己去查日历才知道)。
所以:`GET /api/v1/calendar/lunar?from=&to=`(服务端 `internal/lunar`,同一作者的 lunar-go)。
## 形状是「按日期键索引的映射」,不是数组
客户端拿到 `map[iso] -> 标签` 直接按格子键查,不用自己遍历比对。
`text` 字段是**格子里直接显示的那个串**(初一=月名、其余=日名)——
由服务端定,两端同源。客户端各拼一份的话,同一天在两边日历上可能长得不一样
(例如闰月到底写不写「闰」)。
几个刻意的取舍:
- **不设默认 from/to**:默认范围会让「我要 3 月」与「服务端以为我要这个月」悄悄不一致;
- 入参只收 `YYYY-MM-DD`(**日期键**,不是 RFC3339):农历是"这一天是农历几号"的
纯日期语义,混用时间戳会被时区挪一天;
- 换不出来的日子**不进 map**(客户端查不到 ⇒ 那格不显示农历),而不是塞空对象 ——
空对象会让客户端以为"有农历、只是没内容";
- 区间上限 400 天(不是安全边界,是防客户端传十年前到十年后)。
## 判据(`server/internal/handler/lunar_test.go`)
参照物是服务端的 `internal/lunar`(权威实现),**不抄一份答案表** —— 库升级时判据跟着走。
钉的点各自对着一个会静默出错的错法:
- `Full` 与权威实现逐字一致(5 个日期,含春节、跨世纪、29 天月的边界年份);
- 日名表覆盖 1..30 且**五种前缀形态都在**(WebUI 那版漏过「二十」);
- ★ 初一显示**月名**、其余显示**日名**(与 WebUI `cellLunarLabel()` 同一口径);
- 闰月必须带「闰」字(不带的话闰六月与六月在格子里一样);
- 极端值(公元 1 年 / 1900 / 2100 / 9999)**不许 panic**,且换出来时文字里不许含
「无效/NaN」这类失败标记。
★ 最后一条我第一版**写错了**:断言「`time.Time{}` 应当换不出来」,实测库**换得出来**
(0001-01-01 → 「〇年冬月十八」)—— 我断言的是自己的想象而不是实际行为。
改成断言真正的契约(不 panic / 失败就不给 / 给了就得是真结果)后才对。
## 客户端
`LunarLabels` / `LunarRangeResponse` 两个模型 + `CalendarApi.listLunar(fromIso, toIso)`;
`CalendarPage` 在 `loadEvents()` 之后**不 await** 地拉农历(附加信息不该拖慢事件列表),
失败**不算 `this.error`**(否则"农历服务抖一下"会变成"整个日历打不开"),
只写 hilog 留痕。区间按**网格**取(不只本月 —— 月视图首尾显示上/下月格子)。
|
|||
| 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** 并打出"工具坏了,不是被测代码坏了"。它的目录我一行未动。 |
|||
| cc8beb79db |
fix(pi-bridge): --self-check 此前**一次都没在真实路径上跑过** —— 自检改进程内、默认路径先跑、量纲分三档
dsh 2026-09-18 在本插件里实测报的第二例(同一形状:判据存在但不在路径上)。
## 洞(实测,不是读代码)
· `package.json` 的 `npm test` = `env-preflight && run-suite`,**不带 `--self-check`**;
`deploy/install.sh` 也是 `npm test`,同样不带;全仓 `--self-check` 零引用,
本插件下**没有任何 .md** 提到它 ⇒ 它从没在真实路径上跑过。
· 实测:把 `findDuplicates` 改成恒返回 `[]` ⇒ **`npm test` 照样 `exit=0`**(带 flag 才红)。
· 后果不是小事:它是**跨文件重名**判据的引擎,而它**唯一**的分辨力判据就是这个自检
⇒ 引擎哪天退化成恒空,重名判据**安静地永远放行**,而整套测试全绿。
· ⚠️ 判"在不在路径上"**不能 `grep` 数命中**:`grep -c '判据自检'` 在默认跑里得 **16 次**,
全是若干 `.test.mjs` 的**用例名**恰好含这四个字,而 `selfCheck()` 自己的输出
(`^ (通过|失败) `、`干净样本`)**一次都没有**。**数命中 = 数到的是词,不是调用。**
## 修
1. `selfCheck()` 去掉 `process.exit`,改为**返回失败条数**(否则没法进程内调);
`--self-check` 那条 CLI 行为不变。
2. **默认路径先跑自检**:坏了就 `exit 3` 并明说"工具坏了、本次结论不可信"。
顺序排在跑套件**之前** —— 引擎坏了时套件那份结论本来就不可信,
而且省一整轮 509 条测试进程。
3. **量纲分三档**(本文件已约定的那套 + 新增一档):
`0`=全绿且无重名;`1`=断言失败/发现重名(**代码问题**);
`2`=环境;**`3`=判据引擎自己坏了(工具问题)** —— 不复用 `1`,
否则 `install.sh` 会把"引擎坏了"读成"代码有问题"。`install.sh` 本次不动
(它只判"插件装好没有",非零即中止,3 与 1 对它的行为一致;改它的退出码语义是另一件事)。
## 验证(都跑过)
· 干净树:`npm test` rc=**0**,默认路径出现 `判据自检:全部通过。`(此前 0 次)。
· **M**:`findDuplicates → []` ⇒ `npm test` rc=**3** + `判据自检 2 条失败 —— findDuplicates 的分辨力坏了…`。
· **反向对照**:真造一个跨文件重名 ⇒ rc=**1** + 点名 `2× 平台名字正常时派生别名并原样带标题`
⇒ 两个量纲**确实分开**,没把"代码问题"和"工具问题"混成一个码。
· `node --check` 通过;`--self-check` 单独跑 rc=0(行为不变)。
(补 dsh 交回的实测读数:`--self-check` 在干净树 4/4 通过、变异后 2/4 失败、默认跑 0 次。)
|
|||
| 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`。
|
|||
| 2870d479f8 |
更正: 我自己在两处注释里把"判据没答"写成了"判据答错了" —— 与 pi 更正的是同一个错,两人都犯在这条上
pi 2026-09-18 更正了它上一封的一处**证据标注**:M31(假绿方向)**不能**证明"⑤ 失真", 它只证明"⑤ 对白名单外的码**不表态**"。结论(⑤ 与注释不符、没读 `summary.py`、白名单硬编码)不变。 ## 一、pi 的更正成立,而且**我在同一件事上犯了同一个错** 我实测复现(把 `d07494e` 的旧⑤ 装回真目录里跑,依赖齐、能真启动): | 旧⑤ 遇到 | rc | 提到该码 | 行为 | |---|---|---|---| | 新码 `blocksGreen: false` | 1 | **1 次** | 报`上游退非零,下游却不红` ⇒ **编造**了一个上游事实 | | 新码 `blocksGreen: true` | 0 | **0 次** | **一片沉默** —— 它什么都没说 | ⇒ pi 说得对:**假绿方向是"判据没答",不是"判据答错了"**。 编造会让人去找不存在的问题;**沉默会让人以为它答过了**。这是两件事。 ★ **而我在 `8614903` 的注释与提交信息里写的是** 「假绿:…**同一句"上游退非零"也是编的**,⑤ 没验也验不了 ⇒ 漂移从这个方向完全看不见」 —— **那是把沉默说成了编造,我夸大了证据**。pi 更正的是它自己,我顺带发现**我得改的是两处代码注释 + 一条已发出的提交信息**。 (这条错的形状值得记:**"它没报" 与 "它报错了" 在输出上都是"没有正确的红"**, 不逐条数"它到底说了这个码没有",就会把沉默读成答错。我这次是用 `grep -c hypothetical-blocking` 得到 **0** 才看清的。) ## 二、改准(两处注释,同一段论证的两个副本) `DIAG`/`UPSTREAM_RC` 之前那段注释、以及 ⑤ 之内那段注释,都改成: · 假红 = **判据答错了**(编造上游事实); · 假绿 = **判据没答**(沉默,实测 0 次提及); · 并写明**我原先的写法是夸大证据**。 ## 三、pi §二 那个"洞":**我的实现已经补上,并实测过它挡得住** pi 自查出它建议里的洞:抽 `(diag, rc)` 唯一权威之前,先要有覆盖全 8 个码的真跑表, 否则新⑤ 会对 `undefined` 判"没登记上游"⇒ **天天假红**(M30 那个病换地方复发)。 pi 说这是它上一封**没说**的。 我核对(读 git 历史确认):**修复前**案例数组里只有 **5 个码**声明了 `diag=` (`baseline-unrunnable`/`residue`/`stale`/`unknown`/`absent`);`none`/`counts-unusable`/ `manifest-mismatch` 确实**没有案例**(它们当时是 `--mutants-line-selftest` 用**合成 status** 测的)。 ⇒ pi §二 的描述**准确**。 ★ 但我在 `8614903` 里多加的那一跳**正好就是防这个的**: `--exitcode-selftest` 的反向覆盖断言"`UPSTREAM_RC` 里每个码都得有案例真跑过"。 **实测两个方向:** · 删掉 `none`/`counts-unusable`/`manifest-mismatch` 三条(模拟"只抄有案例的那 5 个") ⇒ ⑤ 报 `DIAG 里这些码没在 UPSTREAM_RC 登记上游退出码:none、counts-unusable、manifest-mismatch`,rc=1 ⇒ **不会静默假绿**;同时那三条案例也各自报"没登记上游"。 · 反向覆盖报 `UPSTREAM_RC 的 5 个码都有案例真跑验过`(覆盖集随之收缩,看得出缺口)。 ## 四★ 补一个 pi 没问、但我认为必须量的:表到底**绑住真脚本**没有 pi §二 的担忧本质是"这张表可能只是抄的"。⇒ 我**改真 `summary.py`**(不动表)来验这一跳: · `manifest-mismatch` 的 `return 1` → `return 0` ⇒ 红:`UPSTREAM_RC[manifest-mismatch]=1 与真跑出来的 rc=0 不符`(两条案例各报一次) · `baseline-residue` 的 `return 1` → `return 0` ⇒ 红:`UPSTREAM_RC[baseline-residue]=1 与真跑出来的 rc=0 不符` ⇒ **改真脚本能被抓住** ⇒ `UPSTREAM_RC` 确实绑在真脚本上,不是抄的。 (我第一次跑这两条时脚本自身有 bug —— python 索引错位 + heredoc 引号 —— **变异没应用**, 所以那次"全绿"是**无效读数**;改正后才是上面这个结果。照实记。) ## 五、验证与状态 · 五个自检全 exit 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✓`。 · `git status` 仅本文件;`HEAD=8614903`;`/tmp` 无残留。 |
|||
| 8614903bc9 |
修复: --verdict-selftest 第⑤条"口径一致性"**没读上游** —— 注释说"不写死",实现恰恰写死了一份码名单;两个方向都失真
pi 2026-09-18 报的。**成立**,我按它给的两个构造各复现一次,两个方向都坏。 ## 一、缺陷本体:注释与实现相反 注释(原话): > 这条把 `summary.py` 的退出码分类与 `run-all` 的表**绑在一起**验(**不在一边写死另一边的值**)。 实现:遍历 `DIAG` 表 + 跳过三个**硬编码**码名 (`if (code === 'none' || code === 'baseline-absent' || code === 'baseline-stale') continue;`) —— **一个字节都没读 `summary.py`**。 ⇒ 它实际断言的只是"**除了我手列的这三个,其它码都必须 `blocksGreen: true`**"。 **注释说"不写死",实现恰恰就是写死了一份名单。** 它是"按字面量裁射程"的又一例, 只不过这次长在**自检里的一条断言**上,而报文写着"两侧口径漂移", 读的人不会怀疑它其实没查上游。 ## 二、两个方向都实测失真(都已复现) · **假红**:加一个**上游退 0 的提示码**(与 `baseline-stale` 同类、完全合法) ⇒ 报 `上游退非零,下游却不红 ⇒ 两侧口径漂移`、rc=1。 **那个码的上游根本不存在**(`summary.py` 永远不会打出它)—— ⑤ 在这里**编造了一个上游事实**并据此判红。 · **假绿**:加一个 `blocksGreen: true` 的新码 ⇒ **rc=0,一条 RED 都没有**。 同一句"上游退非零"也是编的 ⇒ 漂移从这个方向**完全看不见**。 ## 三、修法:把 `(diag, rc)` 抽成唯一权威,链条每一跳都有判据 新增 `UPSTREAM_RC`(码 → 退出码),然后: 1. **`--exitcode-selftest` 把它逐个真跑验一遍**:每条案例用**真脚本真跑出来的 rc** 去核对那张表,并**反向覆盖**——表里每个码都必须有案例真跑过,否则报 "表里的值是没人验过的"。(这一跳是地基:若 `UPSTREAM_RC` 只是手抄的希望值, ⑤ 拿它比 `blocksGreen` 仍可能**两边一起错而且自恰地绿**。) 2. **⑤ 改为双向比对** `UPSTREAM_RC ↔ blocksGreen`: 上游非零却不红 ⇒ 漂移;上游 0 却标阻断 ⇒ 也漂移(**上一版查不到的方向**); 码没登记上游 ⇒ 红("加码要同时登记它退几",与 `blocksGreen` 那条"缺字段即红"是同一句纪律的另一半)。 **白名单随之消失**(数据说话)。 ★ 链条现在是 **真脚本 → `UPSTREAM_RC` → `DIAG.blocksGreen`**,两个方向真绑上了。 ## 四、我写第一版时又犯的两处(都当场红) 1. **按"声明的"码去找**:我第一版用 `wantOut` 取码 ⇒ `wantOut=null` 的四条 (一致 / 说明条目 / 未列入清单 / 清单有磁盘无)**一个码都没验到**, 反向覆盖当场报出 `none`、`counts-unusable`、`manifest-mismatch` 三个码"没人验过"。 而这三个码真跑出来是 0 / 2 / 1(我单独量过,**表里是对的**)⇒ **是验法漏了,不是值错了**。 ⇒ 改成从**脚本真打出来的 stdout** 里取 `diag=` —— "验的是它真说的",而不是"我以为它会说的"。 2. **报文自相矛盾**:⑤ 无条件打 `ok` ⇒ 变异 (a) 输出长成 `RED DIAG 里这些码没在 UPSTREAM_RC 登记…` 紧跟 `ok 口径一致(双向):9 个码…`。 **与结构锚点那次是同一个错,我又犯了一遍** ⇒ 改成"这一格没红才打 ok"。 ## 五、变异验证(都已还原) | 变异 | 期望 | 结果 | |---|---|---| | **(a)** 加一个上游退 0 的提示码 | 红 | **红**,报文说"**没登记上游退出码**"(不再编造"上游退非零") | | **(b)** `baseline-stale` 标成 `blocksGreen: true` | 红 | **红**:`两侧口径漂移:baseline-stale(上游 rc=0,却 blocksGreen=true)` —— **上一版完全看不见的方向** | | **(c)** `UPSTREAM_RC['baseline-residue']` 改成 0(与真脚本不符) | 红 | **红**:`UPSTREAM_RC=0 与真跑出来的 rc=1 不符` | | **(d)** `UPSTREAM_RC` 里塞一个 `DIAG` 没有的码 | 红 | **红**:`两张表漂移` | ## 六、验证与状态 · 五个自检全 exit 0。`--exitcode-selftest` 现在逐条报"真打出 diag=…,UPSTREAM_RC=… ✓", 并断言 `UPSTREAM_RC 的 8 个码都有案例真跑验过`;`--verdict-selftest` 报 "口径一致(双向):8 个码逐条相符,且无未登记/多余码"。 · 全套件 `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 读数一致。 · 残留全清;`git status` 仅本文件;`HEAD=d07494e`。 |
|||
| d07494e8fd |
修复: "该红的红"没到达 verdict —— summary.py 的 status 只喂了文案,没喂决定颜色的那一格
pi 2026-09-18 本封。它读代码 + 端到端对照得出的结论**成立**,我独立复核了:
`verdict` 的四条输入(`reds`/`brokens`/`dueFailed`/`selfCheckFailed`)**没有一条**被
`sp.status` 喂过 —— 全部消费者只有两处、都是文本(`mutantsLine` 一行、`whyLines` 几行打印)。
⇒ 退 1 或 2 **只改措辞、不改颜色**;把别的红清掉之后就会出现
`verdict=green` 与 `diag=baseline-residue`("优先按变异残留查")**并排**。
(★ 比我上一轮修的那几处**好一档**:读者**看得见**了 —— 前几例是连读者都看不到。
但"变异可能没还原"坐在 green 旁边,仍是不能不管的形状。)
## 一、修法:诊断码表同时决定"红不红",接线**只此一处**
1. `DIAG` 表改成 `{ note, blocksGreen }` —— **一个码一行,两个字段一次定完**。
分开写就是"同一事实两份实现"(本仓已反复消:`blurStyleFor`、`stripComments`、
`want.why`、`.tmp` 台账路径)。放一起的结构性好处:**加一个新码就必须同时决定它红不红**。
该红:`counts-unusable` / `manifest-mismatch` / `baseline-residue` /
`baseline-unrunnable` / `baseline-unknown`(= **退非零的全部**);
不红:`baseline-stale` / `baseline-absent` / `none`(stale 不是缺陷,退非零会让正常提交天天假红)。
2. `summarizeMutants` 多返回一个 `red`(人类可读的一句)。**不认识的码也红** ——
上一版这里只改了**措辞**(打"请同步 DIAG")却仍然可以绿,**同一条缝的又一跳**。
3. `reds.push(r.red)` 走 pi 建议的那条路径(运行器自己推的红);
顺手补上**连 `summary.py` 都起不来**那一支(原来只改文案 ⇒ 整个读数器没跑起来也照样能绿)。
## 二★★ `verdict` 判定抽成纯函数 ⇒ **pi 说"代价不值"的那个实验变得可构造**
pi 明说它没做"清掉全部红后 `verdict` 真变绿"的完整实验,因为要动 SUITE 登记数、会撞接线自检。
⇒ 把判定抽成 `verdictOf({reds,brokens,dueFailed,selfCheckFailed})`,那个实验**只要传空数组**:
```
情形 verdict reds 该不该绿
正常 green 0 绿 ✓
残留 red 1 非绿 ✓ ← 修复前这里是 **green**(缺陷本体)
过期 green 0 绿 ✓
读数器没跑起来 red 1 非绿 ✓
```
★ **修复前的反面对照**(把接线退回"只改措辞"):残留 = `green`、读数器没跑起来 = `green`
⇒ 正是 pi 描述的那个"并存",**现在能跑出来了**。
## 三★★ 加自检时我自己的两处错(都当场红,照实记)
1. **自检验的是副本**:第一版自检自己写 `reds = r.red ? [r.red] : []`。
⇒ 我把生产那行 `if (r.red) reds.push(r.red);` **整行删掉**,自检**照样全绿**(实测 M24)——
因为它验的是**自己复刻的接线**。本仓老形状(`parseSkips` 抄份、`want.why` 现算一遍)第 N 次。
**修法不是再加断言,而是让接线住进被测函数**:抽出 `applyMutants(sp, reds)`,
生产与自检调**同一个它** ⇒ M24 重做后**红 7 条**。
2. **结构锚点假红 + 报文自相矛盾**:
· `indexOf('let mutantsLine')` 先命中**注释里提到这串字**的地方 ⇒ 扫的区间多包一个自检块
⇒ 报"1 处直接调"假红。改用 `lastIndexOf`(与自检 4 当年同一个坑,结论照抄)。
· 结构锚点**无条件**打 `ok` ⇒ M27 的输出长成 `RED … RED … ok 结构锚点:…`,
一个 `ok` 跟在自己的红后面。改成"没红才打 ok"。
## 四、变异验证(都已还原)
| 变异 | 期望 | 结果 |
|---|---|---|
| **M24** 删掉 `applyMutants` 里的 push(上一轮逃掉的那个) | 红 | **红 7 条**(修法①之后) |
| **M25** `baseline-residue` 的 `blocksGreen` 改 false | 红 | **红**,`残留` 报 green |
| **M26** `verdictOf` 恒返回 green | 红 | **红 7 条** |
| **M27** 生产绕过 `applyMutants`(直接调 `summarizeMutants`) | 红 | **红 2 条**(结构锚点抓住) |
| **M28** 端到端:让 `summary.py` 不存在 / `PATH` 无 `python3` | 红 | **红**(`red=10`、`verdict=red`) |
★ **M28 顺带查清一件事并写进注释**:`catch` 那一支**本机覆盖不到** ——
`summary.py` 不存在 ⇒ `spawnSync` 退 **2**(python 自己报错);`PATH` 里没有 `python3`
⇒ **`spawnSync` 不抛**,返回 `status=null, error.code='ENOENT'` —— 两条都走 `!m` 那一支。
`spawnSync` 只在参数非法时才同步抛。⇒ 照 pi 那句"前提构造不出来就写明":
**catch 那条红是兜底、不是实测通路**,注释里说清哪条是实测的(`!m`,两条都端到端验过)。
## 五、口径固定(回 pi §五)
`stale ⇒ 提示(rc=0,不进 verdict)` / `residue ⇒ 红(rc=1,进 verdict)` ——
pi 说这条口径要等 §四 修完才能真正固定,**现在固定了**:`--verdict-selftest` 里
`baseline-stale` 判"**可以**绿"、`baseline-residue` 判"**不许**绿",两条同时绿才算数。
## 六、验证与状态
· **五个自检全 exit 0**(新增 `--verdict-selftest` **12 例**:9 个情形 + 码表字段完整性 + 口径一致性 + 结构锚点)。
· 全套件 `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 读数一致。
· 端到端对照:残留态 `red=9 → red=10`(多出的那条点名 `(summary.py)baseline-residue`);正常态仍 `red=9`。
· 残留全清;`git status` 仅本文件;`HEAD=93dabb4`。
|
|||
| 93dabb435d |
修复: baseline= 那一格到不了套件(捕获组是前缀 + 过滤器不含它);baseline-unrunnable 退回 rc=0 的自相矛盾;并把「脏/干净/git 答不了」从两态改回三态
pi 2026-09-18 报的两条我都端到端复现了,都成立。修的过程中又发现**第三条**(我自己引入的),以及**自检自己的一处"判据在但走不到"**。 ## 一、① `baseline=` 那一格从来不进套件输出(pi 报的第 ① 条) 两处都实测确认: · 捕获组是**前缀**:`m[1]` = `RESULT mutants=48 ran=47 skipped=1 on_new_criteria=35`,`'baseline' in m[1]` = **False**; · `whyLines` 的过滤器不含 `baseline=`。 ⇒ 两条路都不通。**真·变异残留**时 `baseline=6/7✗**1 个文件既不在底本、也与 HEAD 不同 ⇒ 优先按"变异残留"查**` 在套件输出里 grep = **0 次**,而 `rc=0`、`mutants=48 ran=47 skipped=1` **看着完全正常**(我按你给的形状复现)。 **修法**:不扩正则、也不往过滤器里加关键词(那是"按字面量裁射程",本仓已栽过两次: `criteria-hygiene` 的 import 硬匹配、`prose()` 判"用没用")—— 改为 `RESULT` 行末尾加**结构化诊断码** `diag=<码>`,由 `run-all` 的 `DIAG_NOTE` 表逐码给一句准确的话: | `diag` | rc | 含义 | |---|---|---| | `none` | 0 | 正常 | | `manifest-mismatch` | 1 | 清单与磁盘不一致 | | `baseline-residue` | **1** | **真·变异残留**(与底本不同**且**与 HEAD 不同) | | `baseline-stale` | **0** | 底本过期(与 HEAD 逐字节相同,**不是缺陷**) | | `baseline-unrunnable` | **2** | 跑不了 `sha256sum`(没读数) | | `baseline-unknown` | **2** | git 答不了 ⇒ 判不出残留还是过期(没读数) | | `counts-unusable` | 2 | 读不到 `jobs/` | 码表是**闭合**的:**不认识的码会报警**,所以"上游加了新状态而下游不知道"会立刻显形, 不会静静变成"没问题"。同时把 `baseline=` 的**读数本身**(`6/7✗`)也摘进那一格 —— 端到端实测:残留时 `grep baseline=` 从 **0 → 1**,rc 从 0 → **1**。 ## 二、② `baseline-unrunnable` 原来 rc=0,与我自己的原则相反(pi 报的第 ② 条) 确认。我在 `blind/unreadable` 上定的是"**没读数不是成功 ⇒ 2**",而"跑不了 `sha256sum`"**是同一件事**, 却退 0 ⇒ 走正常路径 ⇒ 那句"是没读数"同样到不了读者。**我把"跑不了"和"没过"分成了两种状态(对), 但第三种状态(跑不了)没给它传播通道** —— 你这句说得准。 ⇒ 现在 `baseline-unrunnable` 与 `baseline-unknown` 都退 **2**, 且措辞由 `diag` 决定而**不是**由 `status` 决定:`status === 2` 那句通用话会说"上面的 mutants 数字**不可信**", 而这里是**假话**(数字照常有效,**只有 baseline 那一格**没读数)。所以措辞一律走 `DIAG_NOTE`。 ## 三★★ 我自己引入的第三条:`git` 三态被读成两态(假警报指向最危险结论) 修 ① 时我要构造 residue,才发现:`git diff --quiet` 退 **1** 有**两种**含义 —— 实测 · 仓库内干净 = **0** · 仓库内已改 = **1** · **非仓库/HEAD 取不到 = 也是 1**(stderr `error: Could not access 'HEAD'`)。 而原代码是 `.returncode != 0` ⇒ **"git 根本没答"被读成"有差异" ⇒ residue**, 也就是这一格**最危险**的读数。我自己在临时目录里跑时就撞到了:`REPO` 解析成 `/` ⇒ 一律报 residue。 ⇒ 加 `can_ask`(`git rev-parse --is-inside-work-tree HEAD`,非仓库退 **128**,可区分): 答不了就报 `baseline-unknown`(**没读数**),**不许**声称 residue。这与上面 §一 那段注释警告的方向一致: **假警报指向最危险的结论**。 ## 四★★ 自检自己的一处"判据在,但走不到" 补 `baseline` 四态锚点时发现:旧 `runIn` 的临时目录是 `/tmp/xxx/`, 而 `summary.py` 用 `REPO = HERE/../../../..` ⇒ **`REPO` 解析成 `/`**(实测打印过)。 于是 `baseline=` 那一格**在原来的四例里从未被真正走成"对"**:`sha256sum -c` 在 `/` 下全部 FAILED、 `git` 也答不了。它们期望 rc=0/1 却看着"通过",是因为**两个错误互相抵消** ("判不了"被读成"有残留" **且** residue 当时退 0)。 ⇒ 重建 `runIn`:按脚本真实位置**嵌套**建目录 + 造一个**迷你真仓库**(`git init` + 一次 commit + 一个可提交可改脏的目标文件),让 `REPO` 落在仓库根上。现在 `baseline=` 的**四态** (对 / 过期 / 残留 / 判不了)**每一条前提都构造得出来**。 ## 五、变异验证(都已还原) | 变异 | 期望 | 结果 | |---|---|---| | **M21** residue 退回 rc=0(pi 报的原始行为) | `真·变异残留⇒1` 红 | **红**,`stale` 照旧绿(两条独立) | | **M22** `unrunnable`/`unknown` 退回 rc=0(pi 本封 ② 的原始行为) | 两条都红 | **都红** | | **M23** `can_ask` 恒真(三态退回两态) | `git 答不了` 红(会把"判不了"说成 residue) | **红**,rc 报 1(正是那个假警报) | ## 六、验证与状态 · **11 条锚点全绿**:一致⇒0 / unlisted⇒1 / ghosts⇒1 / 说明条目⇒0 / 盲读⇒2 / 单文件⇒2 / **无 sha256sum⇒2** / **残留⇒1** / **过期⇒0** / **git 答不了⇒2** / 没有 baseline⇒0。 · `--mutants-line-selftest` **9 例**(含新增 `baseline-stale` 与"不认识的码⇒报警");四个自检全 exit 0。 · 全套件 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0`、`mutants=48 ran=47 skipped=1 diag=none`(与你读数一致)。 · 端到端:残留时 `grep baseline=` **0→1**、rc **0→1**;正常态 `diag=none`、rc=0。 · 残留全清(`/tmp` 无 `exitcode-*`/`barepath-*`);`git status` 仅这两个文件;`HEAD=e99a657`。 ## 七、你 §三 那句补充 > "能构造"是**上游锚点**的条件,不是**传播**的保证。 认同,而且这封正好是它的**正面用法**:`baseline` 那一格的前提能构造(我造了迷你真仓库), 但**锚点原本不在"到达"那一端** ⇒ 构造得出来 ≠ 到得了读者。 §四 我那句"崩点在打出 RESULT 之前…同一条缝"你说"修完之后它换了形态:不再崩、但结论仍然到不了"—— **同意,而且换到了第三个位置**(前两个:`whyLines` 的 `status !== 0` 关、`unlisted/ghosts` 退 0; 这一个:**正则捕获组**)。现在这条通道两半都钉住了:**结论**走 `diag`、**读数**走 `baseline=`。 |
|||
| e99a657143 |
修复: rc=2 那半个缝(降权构造 blind/unreadable 的上游锚点);顺带修掉它暴露出的**真 bug**:sha256sum 不在 PATH 时 UnboundLocalError 崩在打出 RESULT 之前
pi 2026-09-18 报的 M18。我复现了它,补锚点时又**意外撞出一个提交态就有的真 bug**(不是 pi 报的、也不是我预想的)。
## 一、M18 复现:rc=2 只有下游锚点、没有上游锚点
`--exitcode-selftest` 原来四例期望只覆盖 **0/1**(实测读出 `, 0,` `, 1,` `, 1,` `, 0,`)。
把 `summary.py` 的 `if blind or unreadable: return 2` 改成 `return 0`:
| 自检 | rc | 该不该响 |
|---|---|---|
| `--exitcode-selftest`(上游) | **0(绿)** | **该红** |
| `--mutants-line-selftest`(下游) | **0(绿)** | 不该(它喂合成 status) |
真跑 blind 确认后果:`nobody + chmod 000 jobs/` ⇒ 变异版 **rc=0**(应 2)
⇒ "环境:没读数"整条链又断回"看着正常",**两个自检都不响**。与 pi 逐字一致。
★ 根因是**结构性障碍**,不是疏忽:`summary.py` 用 `os.access(JOBS_DIR, R_OK|X_OK)` 判 blind,
而 **`os.access` 对 uid=0 恒为 True**(实测:`chmod 000` 后 root `access()==True`、nobody 才 False)
—— 与我早先在 `check-file-modes.sh` 踩的 `[ -x ]` 是**同一个 root 陷阱**,只是换成了 `os.access`。
所以自检以 root 跑时 blind 分支**物理上不可达**。
## 二、补法:降权构造前提(三格都有锚点:0 / 1 / **2**)
`runIn()` 加降权模式:临时目录整棵放开到 nobody 可达 → 按场景收紧
(`blind`:`chmod 000 jobs/`;`unreadable`:目录 755 + 单文件 000)→ 用
`runuser -u nobody`(无则退 `setpriv --reuid=65534`)跑**真脚本**。两条路我都实测得 rc=2。
新增两条案例:`盲读⇒2`、`单文件读不到⇒2`。
★ **前提构造不出来时**(拿不到降权工具)报 **RED 并明说"本机覆盖不到、这不是通过是没有锚点"** ——
照 pi 那句"哪条前提构造不出来就写明,别让它默认看起来是被守着的"。不静默跳过。
**变异验证**(都已还原):
· **M18**(`blind ⇒ return 0`)⇒ 两条降权案例**都红**、exit 1(改前两个自检全绿)。
· **M19**(只把 `unreadable` 那一半改成 0)⇒ **只有**"单文件读不到"那条红 ——
证明两条锚点是**独立**的,不是互相冗余。
## 三★★ 补锚点时撞出的**真 bug**(提交态就有,不是本笔引入)
构造"前提 B(单文件不可读)"时跑出 `UnboundLocalError: cannot access local variable 'r'`:
```python
try:
r = subprocess.run(['sha256sum', '-c', bl], ...) # ← 在 try 内绑定
...
except Exception:
baseline_ok = (False, -1, -1) # ← 设了非 None
...
else: # baseline_ok 非 None ⇒ 走这里
detail = [... for ln in (r.stdout or '').splitlines() ...] # ← r 从未绑定 ⇒ 崩
```
崩点在**打出 `RESULT` 行之前** ⇒ 上游只看到"没打出 RESULT",而真因(一个外部命令缺失)
埋在 traceback 里。**触发条件很实在**:`sha256sum` 不在 PATH(精简镜像 / PATH 被裁的 CI)。
实测:正常 rc=0;`PATH` 里去掉 `sha256sum` ⇒ `UnboundLocalError`、rc=1。
⇒ 两件事一起修:
① `r` 不再依赖 try 内绑定;
② 把"**跑不了这项检查**"与"**检查了、没过**"**分成两种状态**(合并正是这个 bug 的成因)——
现在那一格打 `baseline=(**跑不了 sha256sum 校验**:FileNotFoundError: ...)—— 这一格**不是**"底本对"也**不是**"有残留",是**没读数**`。
并补第 7 条锚点:**故意让 PATH 里没有 `sha256sum`**,要求 ① 不崩 ② 仍打出 `RESULT` ③ 那格说"跑不了"。
**M20**(把 `r` 绑定退回 try 内)⇒ 该案例红、exit 1,报的正是那个 `UnboundLocalError`。
## 四、自检自身的两个问题(照实记)
1. **`--exitcode-selftest` 清理漏了**:清理原来是顺序执行的最后两步,`runIn` 中途抛错就跳过 ——
实测 `/tmp` 里留下 `exitcode-*`。改成 `try/finally`(失败正是最常被重跑的路径),
并加了"抛错时也清理"的验证(模拟中途 throw ⇒ 残留 0)。
2. **PATH 一裁连解释器都找不到**:`no-sha256sum` 那条如果不把 `python3` 解析成绝对路径,
会变成"脚本没跑起来"(退 1),而 **1 又是别的案例的期望值** ⇒ 又一次"没跑起来长得像通过"。
已先解析绝对路径。
## 五、验证与状态
· 7 条锚点全绿:一致⇒0 / unlisted⇒1 / ghosts⇒1 / 说明条目⇒0 / **盲读⇒2** / **单文件⇒2** / **无 sha256sum⇒不崩**。
· 四个自检全 exit 0:`--mutants-line-selftest` 8 例、`--exitcode-selftest` **7 例**、`--skip-selftest` 5 例、`--probe-selftest` 3 例。
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0`(与改动前**同样 9 条**)、`mutants=48 ran=47 skipped=1 on_new_criteria=35`。
· 变异:M18 / M19 / M20 全部按预期红并已还原;`/tmp` 无 `exitcode-*`/`barepath-*` 残留。
|
|||
| 6ee9902194 |
修复: 第三例落在两层之间 —— unlisted/ghosts 退 **0** ⇒ 已算出的警告被下一层丢掉;并把**上游退出码**也钉进自检(我变异时发现下游自检守不住它)
pi 2026-09-18 报的第三例。我复现了它,修完后又**自己变异出一个更值得记的问题**:修完之后 `--mutants-line-selftest` 仍守不住上游。 ## 一、第三例:复现(pi 报的那一例,端到端) 加一个未列入清单的 job 文件,改前实测: ``` 磁盘上 job 文件数 13 summary.py 自己说"未列入清单" 1 次 summary.py 退出码 0 ← 就是这里 套件报的 mutants mutants=48 ran=47 skipped=1 on_new_criteria=35 套件输出里 grep "未列入清单" 0 次 ← 一个字都没到读者眼前 ``` 根因两层:①`summary.py` 里只有 `blind or unreadable` 退 2,`unlisted`/`ghosts` **只打印、然后 `return 0`**; ②`run-all` 的 `whyLines: status !== 0 ? whyLines : []` 把**已经算出来**的警告又丢掉。 ⇒ 数字按清单算是**对的**,错的是**读的人不知道它不是全集**。 ★ 这**正是** `590a72a` 标题那句承诺("清单外即报,与 SUITE 同形状")和那边注释 ("与 run-all.mjs 自检 2 同形状:**清单外即红**")说的东西:自检 2 是**真的红**, 而这边只打印、退 0、打印被下一层丢掉 —— **"同形状"当时只同了前一半**。 ## 二、修法:退出码按**修法不同**分两类(照 pi 的提醒) | 情形 | 退出码 | 含义 | 读者该做什么 | |---|---|---|---| | `blind` / `unreadable` | **2** | 环境 | 去修权限 | | **`unlisted` / `ghosts`** | **1** | 清单/数据 | 去改 `jobs.manifest.json` | 按 `env-defaults.sh:25` 那条"别让环境问题冒充代码缺陷"的**反方向**:**也别让"清单没跟上"冒充环境**。 两类都退非零 ⇒ `run-all` 那边**既有的** `status !== 0` 路径自动把 `whyLines` 转印出来, `run-all` 只需把 status=1 那类的**措辞**说准(数字照播 —— 它没错 —— 但挂上"不代表磁盘上现在有多少个变异体")。 **端到端复验(跑出来的)**: | 场景 | summary.py rc | 套件输出 | |---|---|---| | 一致 | 0 | `mutants=48 …`(原样) | | 未列入清单 | **1** | `…(**注意:清单与磁盘不一致** —— 上面的数字**不代表磁盘上现在有多少个变异体**)` + 警告行转印 | | 清单有、磁盘无(ghosts) | **1** | 同上,`磁盘上没有:jobs-GHOST-probe.json` 转印 | ## 三★ 我修完后自己变异,发现**下游自检守不住上游** 把 `summary.py` 里 `if unlisted or ghosts: return 1` 整段删掉(=**退回第三例**), `--mutants-line-selftest` **照样全绿** —— 因为下游收到的是我**喂给它的** `status`, 上游到底退几,它管不着。**同一个缝换了个位置**:结论到达套件的那条通道,上游没有判据守着。 ⇒ 补 `--exitcode-selftest`:把**仓库里那份 `summary.py`** 逐字节复制进临时目录、 配上构造的 `jobs/` 与清单,**跑真脚本**验退出码契约(4 例:一致⇒0 / unlisted⇒1 / ghosts⇒1 / `_` 说明条目不算 ghosts⇒0)。重做 M17(删掉那段)⇒ **自检红、exit 1**,缝在两层都封住。 ★ 这个自检我第一版**只拷了一半依赖**(`summary.py` + `jobs/`,漏了 `test-keys.json` 与 `baseline.sha`)⇒ 每次都 `FileNotFoundError` 退 1。危险之处在于**四个案例里有两个期望 本来就是 1**,于是"没跑起来"**长得像**那两个通过。只有期望 0 的两条把它揭出来。 ⇒ 现在先判 stderr 里有没有 `Traceback`,有就单独报"**脚本没跑起来**,别把它当成退出码不对"。 ## 四、自检自身的两处错(照实记) 1. **声明值与现算值两份实现**:我既在案例里写 `want.why`,又用一条正则从 stdout **现算**一遍 期望条数 ⇒ `want.why` **从没被读**,且现算那条一旦与 `whyLines` 的过滤器不同步, 自检会**自证自恰**地绿。改成只读声明值 —— 立刻暴露出我两个声明值都写错了 (`partial` 真值 2 我写 1、`unlisted` 真值 3 我写 2)。**这正是本仓反复消的"同一事实多份实现"**。 2. **分支顺序错**:status=1 那条我第一版放在 `if (m)` **之前** ⇒ "没打出 RESULT 且 rc=1" (脚本没起来)会被它抢答成"清单与磁盘不一致"。自检⑤当场红,已收进 `if (m)` 内。 ## 五、验证与状态 · 四个自检全绿:`--mutants-line-selftest` **8/8**、`--exitcode-selftest` **4/4**、 `--skip-selftest` 5/5、`--probe-selftest` 3/3,都 exit 0。 · 全套件 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0`(与改动前**同样 9 条**)、 `mutants=48 ran=47 skipped=1 on_new_criteria=35`。 · 变异:M17(删上游 `return 1`)⇒ 退出码自检红、exit 1;已还原(`sha256` 比对)。 · 实验残留全还原:`jobs/` 12 个、`jobs.manifest.json` 17 条且无 GHOST、 `/tmp` 隔离副本已删、`git status` 只剩本笔两个文件。 |
|||
| 4b841e019a |
修复: "没读数"到不了读者 —— summary.py 的 rc=2 与三行 ✗✗ **一个字都没进套件输出**(pi 实测的口子 + 我实测的第二例:部分可读时那行**看着完全正常**)
pi 2026-09-18 报的是**我上一轮新引入**的口子。我复现了它,并实测出**第二种**它没提、
且**只靠它的修法①治不了**的情形。
## 一、pi 那一例:盲读时 `mutants=0` 与"真的没有变异体"长得一样
`run-all.mjs` 原来只做:
```js
const m = /(RESULT mutants=\d+ ran=\d+ skipped=\d+ on_new_criteria=\d+)/.exec(sp.stdout);
mutantsLine = m ? ` ${m[1]}` : ` mutants=(… status=${sp.status} …)`;
```
⇒ **只在正则不匹配时才看 `status`**。而盲读时 `summary.py`(我上一轮刚改的)
**照样打 `RESULT mutants=0 ran=0 skipped=0 on_new_criteria=0`** ⇒ 正则匹配、走真分支、
`status=2` **从没被读**;而 `sp.stdout` **全文件只有这一处引用** ⇒ 它那三行
`✗✗ 读不到 jobs/ 目录 …` / `✗✗ 清单里有 12 个,却读到 0 条条目` **一个字都到不了读者眼前**。
**复现(跑出来的,不是读代码)**:`nobody` 下跑真 `summary.py`,rc=2、三行 ✗✗ 都在,
再拿 run-all 里**那一行**的正则去匹配它:
```
m 为真? true → 套件会播报: RESULT mutants=0 ran=0 skipped=0 on_new_criteria=0
```
⇒ 与"这棵树真的一个变异体都没有"**长得一模一样**,而真相是"**没读数**"。
★ 我上一轮自己在 `summary.py` 注释里写过"那边也会看到 `mutants=0`"并当成**可接受** ——
理由是"这一行自己带 ✗✗ 说明"。**错了**:那几行不在套件的输出里,所以没有"自己带说明"。
这正是本仓反复消的形状("看不到 ⇒ 绿"),只是最后一跳长在**播报端**:
stdout 说对了、退出码也说对了,**但没有通道把它们送到读者眼前**。
## 二★ 我实测出的第二例:**部分可读**(pi 那封没提,且它的修法①兜不住)
目录**能**进入、但**单个 job 文件**读不到(`chmod 000 jobs-one.json`)时:
```
$ runuser -u nobody -- python3 summary.py
RESULT mutants=47 ran=0 skipped=47 on_new_criteria=0(…原始条目 73,其中 retired 13) baseline=0/7✗
★ 清单与磁盘不一致 —— 上面的数字**不代表"磁盘上现在有多少个变异体"**:
清单里有、**在但读不到**:jobs-one.json(权限问题,不是缺失 —— 改权限,别删条目)
rc=2
```
那行 `mutants=47 …` **看着完全正常**,而且**它匹配正则**。⇒ pi 的修法①
(盲读时改打 `mutants=NO-READING`)**对它无效**,因为这里 `blind` 为假、数字是真算出来的。
**只有"先判 status"能兜住它** —— 所以两层修法不是叠保险,是各治一例。
## 三、修法:先判退出码,再把原因行**转印**出来
1. `summary.py` 盲读时改打 `RESULT mutants=NO-READING …`(不匹配该正则,让读数器
**自己说"我没读数"** —— 与我在 `summary.py` 里做的是同一件事,只是补上到套件这一段)。
2. `run-all.mjs` **先判 `status === 2`**,并把它 stdout 里那些 `✗✗`/`★ 清单与磁盘不一致`/
`在但读不到` 行**转印到套件输出** —— "为什么没读数"只有 stdout 知道,而读者只看得到套件输出。
非 0/2 的异常退出码也留痕(`(注意:summary.py status=N)`),不许静默。
**真端到端验证**(不是读代码):把工作树复制到 `/tmp`,`runuser -u nobody` 跑**整个套件**:
| 场景 | 套件播报的 mutants 那格 |
|---|---|
| 正常(root) | `mutants=48 ran=47 skipped=1 on_new_criteria=35` |
| 盲读(nobody) | `mutants=(**环境:没读数**,summary.py status=2 —— 上面的 mutants 数字**不可信**)` + 两行 ✗✗ 转印 |
| 部分可读(nobody + 单文件 000) | 同上 + `在但读不到:jobs-one.json(权限问题,不是缺失)` 转印 |
## 四、抽出纯函数 + 自检:因为这个分支**在正常路径上走不到**
盲读只在**非 root + `jobs/` 不可读**时发生,而套件以 root 跑 ⇒
那段决策**只在我手跑 `runuser` 时才经过**。"只在我手跑时才经过"的分支等于
**没有判据守着**:哪天顺序被调回"先看正则"、或 `status === 2` 被写成字符串比较,
没有任何东西会响,而它是"读数器说自己没读数"的**唯一通道**。
⇒ 抽成纯函数 `summarizeMutants(stdout, status, stderr)` + `--mutants-line-selftest`(6 例),
每次跑都钉住:正常 / 盲读 / **部分可读(正则匹配)** / 非 0-2 退出码 / 没打出 RESULT /
**反面对照**(直接验证"先看正则"的实现确实会把部分可读播报成 `mutants=47`)。
**变异验证**(都已还原):
· M15 删掉 `status === 2` 前置分支(回到有洞的顺序)⇒ 自检②③ **红**,且③的失败输出
正是那个 bug 本身:`RESULT mutants=47 ran=0 skipped=47 …` 会被当成权威数字播报。
· M16 `whyLines` 置空(不转印原因)⇒ 自检②③ **红**(转印 0 行,期望 1/2 行)。
## 五、验证与状态
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0` —— 与改动前**同样 9 条**
(并发会话的"自报>清单" + 4 条 exit 1),`mutants=48 ran=47 skipped=1 on_new_criteria=35` 不变。
· `--mutants-line-selftest` 6/6 绿、`--skip-selftest` 5/5 绿、`--probe-selftest` 3/3 绿。
· 实验残留全部还原:仓库内 `test/mutants/jobs/` 权限 **755**(与实验前一致)、
`/tmp` 隔离副本已 `rm -rf`、`git status` 干净。
|
|||
| 0822993e76 |
修复: 我上一笔升级留下的**更糟的灰区** —— 跳过要**具名**、"移出 STATIC_ONLY"要**要求行为层真跑过**(pi 的变异证明我复现了)
pi 2026-09-18 报的是**我上一笔(b84880f)自己的缺陷**,我按两处都实测复现,没有一条靠信。
## 一、缺陷:升级后有一条真实回归**零痕迹**
形状是**两件事同时发生**:
1. `harmony-nav` 移出 `STATIC_ONLY`(7→6)⇒ 到期闸**不再点名它**;
2. 行为条在设备不在时 `t.skip` ⇒ **不跑,也不算红**。
⇒ 这条判据有了一个"既不红、也不算没升级"的状态。升级前它在 `STATIC_ONLY` 里,每轮都被
点名(红但**看得见、可行动**);升级后**不被点名**,而行为条可以**永远不跑**。
**我的复现**(不是转述):注入 `assert.ok(false,'MUTANT: 真实回归')`,同一棵树两种设备态:
| | RESULT | 红清单里有 harmony-nav |
|---|---|---|
| 设备在 | `checks=459 pass=454 fail=5 skip=0 red=10` | **有** |
| 设备不在 | `checks=459 pass=454 fail=4 skip=1 red=9` | **没有** —— 与未变异基线**逐条一致** |
文件级同样:设备在 `fail 1`;设备不在 `fail 0 / skipped 1`(变异**够不着**)。
★ 还有一层 pi 点出的:两次 `verdict` 都是 `red`,是**被别的红兜住的** ——
等那批红清掉,这条回归就能让套件在"绿"的状态下藏着。
## 二、闸 (i):跳过必须**具名**(与红清单同级)
原来 `skip=N` 只在余额里**数得出来**,但**看不出是谁** —— 于是和"设备恰好不在、
什么都没坏"不可区分。node:test 其实**已经**把原因打出来了(`ok 16 - … # SKIP 设备不在 —— …`),
是解析只取了计数、把名字和原因丢了。⇒ 取回来,并在汇总里按**与红清单并列**的格式打印
(文件 + 判据名 + 原因)。实测(设备不在):
```
跳过的判据(1 条,分布在 1/29 个文件)—— **不是通过**,也不等于没问题:
- test/harmony-nav.test.mjs(跳过 1 条)
「★ 行为(设备):底栏真渲染了可点的导航项(dumpLayout 实测,live ⊆ source)」:设备不在 —— 行为部分本次不跑
```
## 三、闸 (ii):移出 `STATIC_ONLY` 必须**出示行为层绿跑记录**
"7→6"原来是纯**记账动作**:从"到期闸点名"挪到"行为条管辖",而**没有任何东西要求
行为条真的执行过**。⇒ 行为条**断言全过之后**留一条本机记录(`noteBehavioralRan`),
`run-all` 拿它当结算前提;没有记录就**点名**(回到被看见的状态)。
★ 方向要紧:宁可**多报**(说你还没验过),不许**漏报**(把没验过的当成验过了)。
★ 我自己在这里补了一层 pi 没提的收口:**只在到期前提成立时才追这条账**。否则没设备
的机器上行为层**不可能**留记录,那条红就是**天天假红且无法行动**(没人能在那台机器上
把它做绿)—— 正是探针三值设计要避免的。前提不成立时,可见性交给闸 (i)(具名 + 连续
轮数 + 超 K 自红)。四态实测:
| 场景 | 闸(ii) | 闸(i) |
|---|---|---|
| 设备在 + 有记录 | 静默 | 静默 |
| 设备在 + 记录被删(真跑不了) | **红** | **点名** |
| 探针说不(无设备机器) | 静默(避免清不掉的假红) | **点名** |
| 设备在但"忙"(前台是别人的) | **红** | **点名** |
## 四、我在实现过程中自己写错的(照实记)
1. **自检抄了一份副本**:我把 `# SKIP` 正则**复制**进 `--skip-selftest`
(当时的想法是"自检不该依赖被测对象")—— 那样自检验的是**副本**,生产那份改坏了
自检照样绿。是变异时 `AssertionError: 命中 2 处` 把它暴露出来的。
⇒ 抽成唯一实现 `parseSkips`,自检**直接调它**。再变异**生产那份**(去掉破折号剥离)
⇒ 自检红(`why: "- 设备忙"` vs `"设备忙"`)、exit 1,**这次验的是真身**。
这正是本仓反复消的"同一个事实多份实现"(`blurStyleFor`、`stripStrings` 兄弟副本同款)。
2. **自检案例我写错了**:第一条拿的是"不带 ` # SKIP` 标记的行"却期望解析出 1 条 ——
自检当场红。真形状一定带标记;不带标记的行正是另一条要钉的"不许被当成跳过"。
(顺带:自检连**我写测试时的错**都抓到了,方向对。)
3. **账本路径两处各拼一次**:`run-all` 与 `lib/harmony-device.mjs` 各按自己的位置算
`.tmp/` 路径。已注释说明"写的那边是唯一权威、这边只读,路径若漂移会**多报**不会漏报"。
## 验证
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9` —— 与改动前**同样 9 条**
(并发会话的"自报>清单" + 4 条 exit 1),两条新闸在正常路径上**都静默**。
· 变异:M7 自检②、M8 自检③、M12 自检正则、M14 **生产**解析器,全部按预期红并已还原。
· `--skip-selftest` 5/5 绿;它与既有 `--probe-selftest` **同形状**(都先跑套件再判定),
不是我引入的新形状。
|
|||
| 4f386199cf |
修复: 集合指纹**不是集合的函数**(顺序敏感 + 只含 name:size);"读不到"曾静默报成"全 0 且指纹正常";"设备忙"的跳过**加界**;criteria-hygiene 两处按字面量裁射程
pi 2026-09-18 报的两个洞我**都当场复现了**(不是我信了,是跑出来了),修完都验了阳性对照。
## 1. 集合指纹必须是集合的函数(pi 洞 1,两个都复现)
原来 `'\n'.join(f'{f}:{getsize(f)}' for f in listed)`。实测:
· **顺序敏感**:只把 `jobs.manifest.json` 反序(集合/内容/计数全不变)
⇒ `sha=02502771` → `3439e049`。于是"集合变了 ⇒ 一眼看得出"失效,
**每次清单整理都假变**。
· **只含 name:size**:字节级等长改写(`jobs-one.json` 的 `name: 'image'` → `'imoge'`,
265 字节不变)⇒ **指纹仍是 02502771**。更尖锐的是 file 路径改成等长的
`ApiClienX.ets`(指向不存在的文件)时 `ran/skipped/on_new_criteria` 全变而
**指纹不变** —— **"集合没变而数变了"恰恰是它声称要抓的情况,它抓不到。**
⇒ `sorted()` + 内容哈希。实测:原序/反序/排序三种**同一哈希**(`33ff3bea`);
等长改内容 ⇒ 变(`89b2aa41`)。
## 2. "读不到" ≠ "不存在"(pi 洞 2,复现 + 比 pi 说的更糟)
隔离副本 + `runuser -u nobody` + `chmod 644 jobs/` 实测:
```
nobody: mutants=0 ran=0 skipped=0 … 原始条目 0 sha=e3b0c442 rc=0
```
`e3b0c442` = **空字符串的 sha256**;而且退出码 **0**。根因:`exists()` 对
**不可进入目录里的文件**返回 **False**(实测),12 个全被跳过;而 `glob` 那一半
照样列得出 12 个 ⇒ `unlisted`/`ghosts` 全空 ⇒ **"清单与磁盘不一致"那条警告一声不响**。
**同一份权限,两个半边给出互相矛盾的结论。**
⚠️ 比 pi 说的更糟的一点:那行**同时**印着 `清单 12 个 job 文件` 和 `sha=e3b0c442`
—— 一句话里说"12 个"却一个都没读到。
⇒ ① 判目录可进入性(`access(R_OK|X_OK)`),读不到就 `✗✗` 明说数字**全部无效**、
并点明"这不是空集合";② `open` 接住 `OSError`,把读不到的文件**攒起来一次报全**
(原来会死在第一个文件上抛 `PermissionError`,让人以为"就这一个");
③ 退出码 **2**(本仓约定:2=环境)—— 读不到就是没读数,而**没读数不是成功**。
④ `unreadable` 与 `ghosts` **分开报**:修法完全不同(修权限 vs 删条目)。
★ 这里我先写了个**不可达的分支**:`unreadable` 先探后读,而真读时 `PermissionError`
会提前抛出 ⇒ 那段报告永远走不到("判据在,但走不到",这次长在报告分支上)。
自己查出来并改成"读的时候接住",才让它成为可达的真分支。
## 3. "不抢前台"的跳过**加界**(pi 2026-09-18 §3,我接受)
只跳过不设界,"设备忙"会变成到期判据的**永久灰区**:不算红不算绿 ⇒ 永远不必被升级
—— 到期机制要防的正是这个,只是入口换成了"设备忙"。⇒ 连续 K(默认 3)轮没跑成,
**跳过自己变红**并给出接管路径(K 轮内是礼貌,K 轮外是闹钟)。
边界:**只对"设备在、前台不是我们的"计数**;**设备不在不计数也不变红** ——
那是 `PROBES.device` 的既有裁定(没装 SDK 的机器不该天天假红),超出本模块职责。
账本 `.tmp/harmony-busy-skips.json`(已 gitignore):判"**这台机器上**连续多少轮没验成",
换机器不继承。实测 1→2→3 轮 skip、第 4 轮起 FAIL;跑成一次即清零、再从 1 重新数。
自检(不连设备)把 ①K 轮内礼貌 ②超了必红 ③跑成清零 ④上限可覆盖 都钉住,
变异验证 M7(over 恒 false)⇒自检②红、M8(noteRan 不清零)⇒自检③红。
## 4. 顺带修掉 criteria-hygiene 自己两处"按字面量裁射程"
我按纪律把账本读取从裸 `readFileSync` 改成 `prose()`(`criteria-hygiene` 立刻红,
**那条判据是对的、我错了**),接着暴露出该判据自身两个洞:
· **import 按写法硬匹配**:只认 `'./lib/read.mjs'`/`'../lib/read.mjs'`,
而 `harmony-device.mjs` **就在 `lib/` 里**、按惯例写 `'./read.mjs'` ⇒
假红"根本没 import"(其实 import 了、运行时完全正常)。改成**解析说明符后与
`SELF` 比**(本仓已用"解析后比较"解决过同一族:`abspath`、`join(HERE,…)`)。
· **用 `prose()`(原文)判"用没用"** ⇒ **注释里**写 `prose(…)` 就算用了
(实测该文件命中 3 处、只有 1 处是真调用)。改成 `code()`。这与它上面那条
"不许裸用 readFileSync"踩过的是同一个坑,我在那条上写了理由、**这条漏了**。
两处修法都做了承重验证:删真 import ⇒ 红;import 换成 `code as prose2`(别名)⇒ 红;
只在注释里写 `prose(` ⇒ **绿**(对照旧实现:同一份样本 ⇒ **红**)。
## 验证
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9` —— 红的仍是同样 9 条(都是并发
会话的"自报 > 清单"与 4 条 exit 1),我中途引入的两条(裸 readFileSync、import 假红)
已消。`mutants=48` 不变。
· 所有 chmod/清单反序/等长改写**都已还原**,`git status` 只剩本次 5 个文件。
|
|||
| b84880f14b |
test(判据): 到期闸第一条按 (a) 升级 —— harmony-nav 加**行为层**(真 dumpLayout),并把"跳过"变成可数余额
pi 2026-09-18 报"静态判据到期闸开了"是真阳性:探针的前提"本工作区能装、能点 设备"现在成立(我实测:签名 HAP 装上 install bundle successfully、aa start start ability successfully、uitest uiInput click 返回 No Error、uitest dumpLayout 出真 UI 树)。闸门要求 (a) 改行为判据 或 (b) 改换更准的前提 —— (b) 救不了: 它建议拆的"目标存在 ↔ 能装能点"两层我两层都实测为真,拆开照样红。所以走 (a)。 ## 1. 新增行为层(harmony-nav,17 条) `lib/harmony-device.mjs`:设备侧 harness(findHdc / hasTarget / foregroundBundle / dumpLayout / walk / findByText / boundsAt)。**只读不抢** —— 模拟器是共享的, 应用不在前台就跳过,不启动、不点。 harmony-nav 新增两条: · 「★ 行为(设备):底栏真渲染了可点的导航项(live ⊆ source)」—— 真 dumpLayout,断"屏幕下 1/4 里真画出了可点的项、每项至少亮一个 源码 NAV_ITEMS 定义过的标签"。**版本无关**:已安装构建可能比 HEAD 旧 (实测前台那份是 3 项,HEAD 源码是 4 项),所以断"live ⊆ source"而不是 "相等"——"四项齐不齐"仍由静态层把。 · 「★ 判据自检:底栏取值逻辑」——纯函数 `navItemsOf` 上的合成树断言。 它**不替**静态那批(点击配对/挂载映射/命中区 ≥44vp/让位派生)——那些读源码更准。 它补的是源码读不到的那半:真渲染出来了吗、真可点吗、标签对吗。 ## 2. 把"跳过"变成可数余额(run-all.mjs) ⚠️ 这是本轮**我先写错、再查出来**的地方,记在这里:升级之前套件里**没有任何 in-file skip**(全仓 grep 零命中)。加了第一条之后,`# tests N` 把跳过的**算进总数**, 而 `pass = checks - fail` 又把它读成**通过** —— 实测设备在 / 设备不在两次运行的 `RESULT files=…` **一字不差**(都 `checks=457 pass=453 fail=4`,而文件自己报 `# tests 16 / # pass 15 / # skipped 1`)。**"看不到 ⇒ 绿"长在总数行上。** 修法照 `fail` 那一格的先例(pi 2026-09-15 指出缺 `red` 时的同一形状): · 取 `# skipped K` 成一格;`records` 带 `skip`;投影加 `totalSkip`(第 5 个); · `pass` 改成 `checks - fail - skip`;汇总行补 `skip=N`(并在单位说明里写清它是 "本次没跑",既不是通过也不是失败); · 自检 5 补 `checks ≥ skip`(对**每个**按文件累加的计数器都成立的上界 —— 照 pi 那条"只给其中两个判上界,第三个就永远没人管")。 实测:设备在 `… pass=454 fail=4 skip=0`,设备不在 `… pass=453 fail=4 skip=1`, `pass+fail+skip == checks` 在两个方向都成立。 ## 3. 到期债务结算一笔:7 → 6 STATIC_ONLY 去掉 harmony-nav;`docs/DEBTS.json` 的 static-criteria 7→6(含 where 清单同步);SUITE 登记数 11→17(原登记 11 早已与文件里的 15 条不符 —— 并发会话 加了日历/我的那批测试没改登记,这条红一并消掉)。 ## 变异验证(都做了阳性对照,还原后 sha256 一致) · M1 `sourceLabels` 取空集 → 行为条 not ok,报出真实文案 `✉️、通信`; · M2 底栏阈值 0.75→2.0 → 报"实际 0,一个都没有"; · M3 `AGENTMAIL_HARMONY_DEVICE=none` → `# skipped 1`(且**改 run-all 之前** 汇总读不出来 —— 这正是上面第 2 条要修的实证); · M4 删掉 `if (a.type === 'Text') return false;` → **第一次没红**(自检④拿的是叶子 FAB,它的 textsUnder 是空、被下一条内容条件滤掉,没测到那行)⇒ 换成"带子文本的 可点 Text"后**才**红。自检本身也会空跑,这是同一族病的又一个实例; · M5 阈值 0.75→0.10 → 自检②红;M6 `clickable` 判定取反 → 自检①红。 ## 说明与遗留 · 已在运行的构建比 HEAD 旧(前台那份 3 项),行为条按"live ⊆ source"设计, 所以它现在**绿**且**没有**把"我的缺一项"误报成红 —— 那是 build-stamp 的活。 · 模拟器是**别人会话的**:我为验证装/启动/点过(pi 明确没动它,我动了,如实记); harness 因此按"不抢前台"写:不在前台就 skip 并计数。 · 余下 6 条到期判据(appearance/logic/cross-client-theme/defaults/admin/imageprep) 未动,仍留在 STATIC_ONLY 里红着 —— 到期机制该干的事。harness 已就位,可按 可观测性逐条升级。 |
|||
| 590a72a715 |
修复: job 集合从 glob 改成**清单**(+ 清单外即报,与 SUITE 同形状)+ RESULT 行带集合指纹;baseline 播报区分"底本过期"与"变异残留";权限政策补上**目录可进入性**(原来只管文件)
pi 2026-09-18 报的三件,逐条实测后处置。
## 一、job 集合是 glob ⇒ "权威"是**树的函数**(同一段代码两个数)
pi 独立复算对上了(48/48/0、A=36/B=41 与我一致),但发现 48 与 52 **都不是错的** ——
它们读的是**不同的集合**,而**没有任何东西说明读的是哪个**:
| 快照 | job 文件 | 输出 |
|---|---|---|
| 提交态 | 9 个(全跟踪) | `mutants=48 …` |
| 工作树 | 11 个(2 个未跟踪) | `mutants=52 …` |
真因:`glob(JOBS_DIR + '/jobs*.json')` ⇒ **未跟踪的 job 文件静默进入统计**。
这与"写了判据忘了接线"同族,区别是 `run-all.mjs` 有**自检 2** 挡着(清单外的 `*.test.mjs` 直接红),
这边没有对等物。**本仓用"清单 + 清单外即红"解决过同一个问题两次,这是第三次。**
**处置(pi 建议的前者)**:新增 `jobs.manifest.json` 显式列 job 集合,`summary.py` 只读清单;
清单与磁盘不一致时**明说**(两个方向都报:未列入清单 / 清单里有但磁盘没有),
并声明"上面的数字**不代表磁盘上现在有多少个变异体**"。
**另加集合指纹**(pi 建议的后者,两条都做了):`sha=…` —— 让"48 还是 52"变成可判的:
集合没变而数变了 ⇒ 真算错;集合变了 ⇒ 一眼看出是换了快照,不必再互相复算一遍。
**变异验证**(两个方向都试):
```
加一个未列入清单的 job 文件 ⇒ 报「未列入清单:jobs-UNLISTED-probe.json」
且 mutants=**48**(未被静默计入)✓
清单里加个不存在的文件 ⇒ 报「清单里有、磁盘上没有:jobs-GHOST.json」✓
```
(`_` 开头的条目是说明,读时滤掉 —— 否则会被当成文件名,`ghosts` 假报一堆。)
## 二、`baseline=4/7✗**有文件没还原**` 是**假警报指向最危险的结论**
实测三个不匹配的文件 **全部与 HEAD 逐字节相同**(`git diff --quiet HEAD` 为空):
`AdminUsersPage.ets`/`SettingsPage.ets` 是提交 `6861934`(09-17 21:15)改的、
`ApiClient.ets` 是 `9c6e9c6` 改的 ⇒ **底本过期,不是变异残留**。
★ 而原来一律打"✗**有文件没还原**" —— 那会让人去翻变异,而真因只是底本没跟上提交。
**两种成因在 `sha256sum -c` 眼里一模一样**,所以播报必须分开,并各给判别方法:
```
baseline=4/7⚠**底本过期**(3 个文件与 HEAD 逐字节相同 ⇒ 是正常提交改过、不是变异残留)
```
**底本已重算**,并按要求在文件里记一行"为什么"(含"重算前必须先证是提交态"这个前提)。
**重算后仍在校验**(变异验证):给 `AppearanceApi.ets` 追加一行 ⇒ 立刻 `FAILED`;还原 ⇒ `7/7 OK`。
## 三、`summary.py` 对非 root 是坏的:`jobs/` 缺 `x` 位
```
drw-r--r-- client/electron/test/mutants/jobs ← 缺 x
$ runuser -u nobody -- python3 …/summary.py
PermissionError: [Errno 13] Permission denied: '…/jobs/jobs-all.json'
```
⇒ 那份"口径的唯一权威"**只有 root 跑得起来**。已 `chmod 755`,非 root 复跑输出一致。
★ pi 的深层判断成立且我核实了:**这条落在任何判据的射程之外** ——
`deploy/check-file-modes.sh` 的政策是"**源文件**不得比 0644 更严",
而它遍历的是 `git ls-files`,**只看文件**;"目录缺 x"比"文件 0644 更严"更严重
(连 `stat` 都进不去)。
⇒ **给该政策补上"目录可进入性"**(只判 `u+x`、只判"仓库内容所在的目录",
不去管 node_modules/dist —— 那会把这判据淹掉;也不判 group/other —— 那取决于本机 umask)。
**★ 我第一版这里又写错了,而且错得正好是被测的那个病**:
用 `[ -x "$d" ]` 判可进入 ⇒ **恒为真**,因为 **root 无视权限位**(`test -x` 对 uid=0 永远返回 0),
于是变异验证"撤掉 x"**根本不触发**,而**输出看起来完全正常**。
⇒ 改成**直接读权限位**(与同文件里文件那段的位运算同一做法)。
改后变异验证:撤 x ⇒ `[FAIL] 目录不可进入:…(drw-r--r--,缺属主 x 位)`、退出码 1;还原 ⇒ 0。
## 四、顺带修好三处既有权限违规(`check-file-modes.sh` 原来一直红着)
```
.githooks/pre-push 711 ⇒ 755(保留执行位,**不是**去掉)
test/harmony-arkts.test.mjs 600 ⇒ 644
model/DeviceProbe.ts 600 ⇒ 644
```
三者都是**提交态**就这样(不是并发会话弄的)。按脚本自己的"药方按类型分岔"修的。
现在该政策 **退出码 0**。★ git **不存** 600/644 的区别(只记 exec 位)⇒ 这三处是**本地状态**修复,
不随提交走,新克隆不受影响 —— 与 pi 对目录权限的说明同一条。
## 五、pi 报的另两件:**在 HEAD 上已经不存在了**(是过时读数)
- **ArkTS 两处阻断**:`MainPage.ets` 的 import 现在在 71-74 行、最后一个 `const` 在 88 行
⇒ **位置正确**;`AdminUsersPage.ets` 的 `Chip` 签名已是 `ResourceColor`(`:410`)
⇒ 两处**都已在 HEAD 修好并提交**(工作树 `client/harmony/` 干净)。`harmony-arkts` 判据 3/3 绿。
- **套件现状**:现在 **29** 个判据文件(不是 23),`unreported=0 broken=0`。
## 六、测量
| 相位 | RESULT |
|---|---|
| build | `files=29 ran=29 checks=456 pass=453 fail=3 red=9 broken=0 unreported=0` |
| install | `files=29 ran=27 checks=444 pass=442 fail=2 red=8 broken=0 unreported=0` |
三种 cwd 仍逐字节一致(上一轮的修复保持)。
|
|||
| 5621cf97fa |
fix(webui): 深色模式「只有通信页正常」—— 根因不是 alpha,是底没暗下来
用户原话:「webui 只有通信页面的深色模式正常了,剩下的三个页面深色模式 可读性都极差」。99a2d7a 只修了 `.glass-card` 那块(通信页走的就是它), 其余三页的面板走 `html[data-bg='on'] .bg-white` → `--bg-glass`,没跟进。 我第一版把这三档 alpha 从 0.9/0.84/0.55 降到 0.08/0.05/0.04,**仍然不够**。 ## 正确判据:逐个叶子文本节点,采样它**真实渲染的底色** 之前的探测全在数 DOM 祖先链上的 backgroundColor,而 `.app-backdrop` 是 `position:fixed` 的**兄弟节点**(不是祖先)⇒ 永远采不到壁纸与遮罩, 只能退回"壁纸均值 213"→ 得出"底是亮的"但与屏幕不符。 改成截图后用 pngread.mjs 逐像素采样:对每个叶子文本节点取其 bbox 内 出现最多的颜色当作它的实际底色,再算 WCAG 对比度。得到决定性的数字: 通信 最差 1.87:1 / 日历 最差 **1.19:1** / 联系 1.64:1 / 我的 1.42:1 (日历页「廿五」fg=rgb(138,146,161) bg=rgb(134,132,132) —— 字和底几乎同色) ## 根因:`--bg-dim` 是**比例**,比例压不住一张**浅**壁纸 用户 jianf 的壁纸均值 RGB 213(浅照片)、dim=56 ⇒ 213×0.44 ≈ 94,仍是中灰。 近白正文对 94 只有约 3.6:1;次要文字 gray-400 对 94~134 只有 1.2–1.4:1。 **没有任何文字颜色能救** —— 这与 background.test 的契约「玻璃是白色材料」 是同一件事的两面:深色下敢用近白基材,前提就是「背后是深底」, 而这个前提此前没人保证。 ## 改动 1. 新增 `--bg-dim-min`(`:root` 0% / `.dark` 92%),遮罩取 `max(var(--bg-dim), var(--bg-dim-min))` —— **取 max 而非覆盖**, 用户调得比下限高时仍以用户的为准,不下调他的选择。 2. 玻璃 alpha 收到 0.04/0.03/0.02(第三层从 `transparent` 改为 `--bg-glass-nested3` 的小值:深色下"透明"= 浅壁纸直接透上来)。 3. `.dark` 的 `--glass-card-wall-a` 0.1 → 0.04 与其它档对齐。 4. 浅色分支完全不变(dimMin=0% ⇒ max() 等价于原值,实测 glass 仍是 0.88、 遮罩仍是 `rgba(255,255,255,0.56)`)。 ## 验收(1280×800 真渲染采样,逐个叶子文本节点) 修复前:通信 1.87 / 日历 1.19 / 联系 1.64 / 我的 1.42(<4.5 的节点 17/88/22/26) 修复后:通信 6.14 / 日历 4.90 / 联系 4.96 / 我的 5.38(<4.5 的节点 **0/0/0/0**) 代价(明写在案):深色下浅壁纸被压得很淡(92%)。这是可读性优先的取舍, 壁纸仍在(8% + 玻璃质感 + 模糊),只是不再是主体。 ## 判据(theme.test 30 → 38,已同步 run-all 的棘轮) 新增 4 条,针对"归因错"这件事本身: · 深色下有压暗下限且 ≥90%(浅色下不干预) · 下限真的作用在遮罩上(**不是只定义变量**) · 用最坏输入(纯白 255 壁纸)实算 gray-400 对合成底 ≥ 4.5:1 · 判据自检:拿掉下限必须判红 变异自检跑过:下限改回 56% ⇒ 2 条红;下限定义了但没用上 ⇒ 1 条红。 `--revert-mutation` 之外的基本面:theme 38/0、background 44/0、 cross-client-theme 15/0、appearance-defaults 4/0。套件 broken 由 4 降到 3 (build stamp 因重构建而转绿),red 23 不变,无新增失败。 |
|||
| 99a2d7ad7e |
fix(webui): 深色模式真的落地了 —— 之前 .dark 是「有意留空」的
用户报「深色模式可读性差」。实测(1280×800,读页面计算值)拿到两个数字:
· .glass-card 合成成 rgb(237,237,237) 白卡,而其上 --c-gray-900 文字是
rgb(243,245,248) ⇒ **1.07:1 的白底白字**
· .nav-rail 合成成 rgb(188,189,190) 浅灰条,未选中文字只有 4.03:1
根因写在 index.css 自己的注释里:`.dark { /* 有意留空 */ }`。当年留空的理由
是「组件没有 dark: 变体,只换令牌会半深不浅」——**方向反了**:组件写的是语义
色阶(bg-white / text-gray-900),灰阶反转后本来就会自适应;真正没适配的是
**手写 CSS 里那几处硬编码白色**(.glass-card 的 0.92、--nav-bg 令牌),
它们不在 --c-* 色板里,所以「色板变量已全覆盖」的判据一直是绿的。
改动:
1. .glass-card / .nav-* 全部改走令牌,主题之间只差 alpha;
深色给 --glass-card-a: 0.06 + --nav-bg: 30 35 44/0.72。
遵守既有契约(background.test「玻璃是白色材料」):**基材恒为白**,
只降 alpha,绝不换成深色层。
2. CalendarView 的非本月农历小字 text-gray-300 → gray-400/500。
gray-300 在这套调色板里是**分隔线档**(全仓 68 处 border-gray-300、
当文字只有 5 处),深色下它是 61,68,81,落在深卡上 1.49:1;
而 gray-400 在深色 4.66:1、浅色白卡 2.54:1,两端都更好。
3. 判据:theme.test 补 3 条(手写 CSS 不许有绕过主题的硬编码白色表面 /
深色卡片 alpha 必须降低 / **按实际 alpha 合成后**正文须 ≥4.5:1,
即把 1.07:1 那个事故写成可计算的断言)。background.test 那条
「.dark 里不许出现 --nav-*」**方向反转**为「必须有且底暗字亮」——
它原来守的是「还没有深色主题」这个前提,其注释本就写明
「真做深色主题时这条要一起改」。
验证(不是"改了就算"):
· 自建探针遍历 **9 个页面**(三个通信子页签 / 邮件详情 / 日历 / 联系人 /
我的 / 写信 / 地址补全弹层):修复前通信 14 处、日历 74 处、
联系人 13 处低于 WCAG AA;修复后 **9/9 页面 0 处**。
· 三条新判据逐条**变异自检**(还原修复即变红,且失败信息自带药方);
浅色三个取色值与改动前**逐字节一致**(导航 0.72 白玻璃、卡片 0.92 白)⇒ 零回归。
· theme.test 34 通过 / background.test 44 通过 / tsc 干净。
· 已按 deploy/redeploy-gateway.sh 部署到 systemd 实例,后置清单自动项全绿,
并在生产实例上复量 9 个页面(同为 0 处)+ 端到端发信(状态 read,非仅入库)。
#深色模式 #可读性 #WCAG
|
|||
| 2166f0ed81 |
fix(harmony): 补齐窗格切换动画(transition 挂在会换的那棵子树上)
用户(2026-09-17):「一方面一点动画都没有」。
★ 第一版写错了,这里记下来 —— 它的错法很典型,下次还会踩:
只在 onClick 里把 `currentIndex = ...` 包了一层 `getUIContext().animateTo(...)`,
并把 `.transition(...)` 挂在**内容容器(稳定父节点)**上。
编译过、判据(当时只钉"有 animateTo")全绿,**但动画是静默失效的**:
`animateTo` 只负责开一个动画窗口,被换掉的子树自己不声明 transition 就什么都不会动;
而挂 transition 的那个 Column 在新旧两种状态下**都是同一个节点**,永远不会触发。
实测取证:把时长临时改成 20s,6 秒后截图仍是硬切(收件箱整版清晰、没有叠影)。
修法(对齐 WebUI `.pane-rise` / `.rise-in`,`index.css:1208`):
· `Theme.paneRiseIn()` —— 4vp 上浮 + 淡入;`riseInOffset = 4` 与
`@keyframes rise-in { from { opacity:0; transform: translateY(4px) } }` 逐字一致。
· 挂到**if/else 各自的子树根**上(CommPage / ContactsTab / SettingsPane,
以及常驻+visibility 的日历那一支)—— 这才是会被插入/移除的节点。
· `TransitionEffect.asymmetric`:入场 180ms(durBase)、出场 120ms(durFast)。
出场更快是刻意的:同长会让新旧两层半透明地叠着,看起来像"闪一下",
而用户对"闪"敏感(09-14 否掉过整屏淡入)。
· 续用令牌 `durFast=120` / `durBase=180` / `easeOutSoft=cubic-bezier(0.22,1,0.36,1)`。
判据(219 passed / 0 failed):
· 新增「窗格切换有真的过场动画」6 条:令牌数值逐个钉死(120 / 180 /
cubic-bezier(0.22,1,0.36,1));`transition` 必须挂在 if/else 分支根(≥2 处,
含日历那一支);必须 asymmetric。
· **变异自检**:删掉那 4 处 `.transition(...)` ⇒ 该条立刻变红;还原 ⇒ 全绿。
留给以后的坑(写进 Theme 注释):`snapshot_display` 单次往返约 1s,
180ms 的过渡**抓不到**(连拍三帧像素级一致,会得出"动画没做"的错误结论)。
要取证就得把时长临时调到 20s 再取样,验完还原。本文件注释里留了这条与那次实测值。
未验:真机手感(模拟器已确认过渡链路生效);出场动画与详情 push 的叠加观感。
|
|||
| 686193458a |
fix(harmony): 底栏黑带 / 联系人点不开 / 我的页不守导航 / 顶栏硬截断
用户(2026-09-17)连报五条:「一点动画都没有」「底部那个黑条是啥意思」
「日历页面、我的页面、联系人页面哪个跟 webui 对齐了」「联系人页面连点都点不开」
「你的顶栏为什么还是硬截断而不是渐变」。逐条查证后修:
── ① 底部黑条 = 系统导航栏区域,不是我们画的 ──
根因:窗口默认给系统导航条留位,那块在深色下是黑的;界面于是看起来底下多一条黑带。
修法:`setWindowSystemBarEnable(['status'])` 隐藏系统导航条(底部那条本就是我们自绘的
悬浮玻璃条),内容铺满全高。
★ 刻意**不用** `setWindowLayoutFullScreen(true)`:实测试过,它也能消掉黑带,但会连
状态栏区域一起吃进布局,页签栏被时钟/电量盖住(截图硬证「07:43」与「收件箱」重叠)。
── ② 联系人点不开(用户原话「连点都点不开」)──
根因:`ContactItem` / `WorkCard` **根本没有 onClick** —— 卡片画出来了,
但没有任何点击路径。当时判据只钉了"字段与 WebUI 一致",没钉"点了会发生什么"。
修法:点卡片打开那条会话的邮件列表,就地在**本窗格内**展示(新增 `SessionMailsView`
+ `GET /sessions/{id}/mails`),底部导航保留;每封可再点进详情。
── ③ 我的页不遵守导航规则 ──
根因:点底栏「我的」走 `pushUrl('pages/SettingsPage')` 推**独立 @Entry 页** ⇒
底部导航整条消失,要按返回才能再切窗格。WebUI 里 `account` 只是一个 `viewMode`,
与收件箱同级、导航常驻。
修法:`SettingsPage` → `SettingsPane`(去掉 @Entry 与返回键,从 main_pages.json 摘除),
作为**第 4 个内容窗格**挂到 `currentIndex === 3`;`NAV_CONTENT_COUNT` 3 → 4,
`NAV_ITEMS` 第 4 项去掉 `route`;拨动动画包 `getUIContext().animateTo`。
实测:`我的` 页底部导航可见,可滚到「退出登录」「管理」。
── ④ 顶栏硬截断 ──
根因:`Scroll` 与 `List` 都能挂 `fadingEdge`(同在 `ScrollableCommonMethod` 上),
但**只有 MainPage 的 List 挂了** —— MailDetailPage / CalendarPage / SettingsPage /
AdminUsersPage / SessionsPage / InboxPage 的滚动容器全漏。同一次滚动里
列表是渐隐、正文是硬切,看起来就是"顶栏没做渐变"。
修法:13 个容器全部补齐(LoginPage 例外:它的 Scroll 是整页根、不是列表,
写了理由)。实测:视口上沿被切断的文字最暗 170–196,而下方不透明正文是 **19**。
── ⑤ 一点动画都没有 ──
实测确认改造前全仓 `animateTo` / `transition` / `animation` **一次都没用过**。
补:Theme 加令牌(durFast 120 / durBase 180 / easeOutSoft = cubic-bezier(0.22,1,0.36,1),
与 WebUI `--dur-*`/`--ease-out-soft` 同值同曲线);切窗格包 `animateTo`;
导航项选中变色加 `.animation`。
判据(218 passed / 0 failed):
· 新增「渐隐覆盖**所有**页面滚动容器」:按文件枚举 + 计数配平,
并自检扫到的容器总数(防正则写坏 ⇒ 全绿)。**这条一写就抓到 LoginPage**。
· ①/②/⑤ 与 headless 那几条按新事实改写:内容窗格 3→4、
`normalizeNavIndex(3)` 归一到自己、导航项里**禁止**再出现 `pushUrl`
(那正是导航消失的形状)。
· 修掉自己引入的两处判据缺陷:`itemCode` 未定义;以及一条**窗口式断言**
(从内容层 `}` 往后扫,靠数花括号定界,而注释里就有大量 `.padding({`,
切片长到 1700+ 字符一路扫进 `onAreaChange` ⇒ 假红),改为纯负向断言。
未验:真机观感;横屏 Auto Split 双栏;日历页深度对齐(本轮只补了渐隐)。
|
|||
| 7e5b11392f |
fix(push): session_id 非空时必须是合法 UUID —— 它此前静默接受任意字符串,真 bug 就是这么活下来的
dsh(f07daac1 之后的 5fab8734)报:客户端一度把这个字段填成**服务器地址** (`AccountInfo.server` = `https://…/api/v1`),而这里只 TrimSpace、不看格式 ⇒ 不 400、不影响收信、不进日志;而**投递路径也不看它**(通知里的 session_id 来自 邮件自己,见 notify/mail.go;dispatch 只用 token 的 Provider/Token)。 两条路都不看 ⇒ 存了假值没有任何机制会报警,可它存在的唯一目的就是 「点通知回到那条会话」。 改动只有一条校验(非空才校验,空串仍合法 —— 契约允许"客户端还没进任何会话"), 400 文案自带药方(写明"要么 UUID、要么留空")。 判据 `TestPushTokenSessionIDMustBeUUID` 四个用例:服务器地址顶替 / 随便一个词 / 空串 / 真 UUID。**变体验证两个方向**: · 去掉校验(连 import 一起去)⇒ 「非法」两条当场红(实际 200); · 把判断写反(`err == nil` 才报错)⇒ 合法的真 UUID 也红 ⇒ 证明它不是"恒 400"。 恢复后 `go test ./...` 全绿、`go vet` 干净。 取舍:老客户端带旧值上报会吃 400,按契约 400 归静默 ⇒ 不打扰用户、只是那次不上报。 `push_tokens` 现为 0 行,**没有历史脏值要清**。 |
|||
| 0bced9fcff |
跨端: fix(推送客户端) 更正我上一笔的注释:session_id 不是"只写不读",GET 会回给客户端
上一笔 `9404f98` 的代码是对的,但注释里我写了一句**过度概括**:
"服务端对这个字段不做格式校验、而且当前**只写不读**"
后半句错了。`GET /api/v1/me/devices/push-token` 把这个字段**原样回给客户端**:
server/internal/handler/push.go:148 "session_id": t.SessionID,
更正为准确的三条(各自都能复核):
1. 服务端不做格式校验(只 `TrimSpace`、允许为空)⇒ 所以不 400、不影响收信、不进日志;
2. 但 GET 会回给客户端 ⇒ 假值**是可见的**,且 `isRegistered` 的比对口径
(只按 `provider + token_tail`,见 PushContract.ts 那段说明)恰好不看它 ——
两件事合起来意味着:**没有任何机制会因为这个字段错了而报警**;
3. 投递暂不受影响:发通知用的是**邮件自己的** `session_id`
(`notify/mail.go:266` 构造 `push.NewMail{SessionID: m.SessionID}`),
`dispatch` 只用 token 的 `Provider`/`Token`(`push.go:155`)。
⇒ 结论不变但理由更准:**这个字段存在的唯一目的就是"点通知回到那条会话",
而它存的值是错的**;"不会立刻炸"正是它该先修的原因。
写这条注释时我把"投递不读它"顺手写成了"没人读它"——**"不参与这条路径"与"没有读取方"
不是同一件事**,与这两天反复出现的形状同族(把"我没看到"读成"不存在")。
|
|||
| 9404f98bde |
跨端: fix(推送客户端) session_id 传的是**服务器地址** —— 一个"不报错"的假数据
这条不是别人报的 bug,是我自己在做上一封收尾时写下的,今天读自己的代码才发现。
## 错在哪
`reportToken` 里:
const sessionId: string = this.account.getActiveAccount() === null
? ''
: this.account.getActiveAccount()!.server; ← AccountInfo.server = **服务器地址**
`AccountInfo.server` 的值长这样:`https://mail.jianfgit.xyz/api/v1`。
而服务端对这个字段的语义是「客户端**当前所在的会话**:点通知要回到那条会话里的那封信」
(`server/internal/handler/push.go`),类型上是会话 UUID(`push_tokens.session_id`)。
## ★ 为什么它能一路活到今天:因为它**不报错**
两件事叠在一起,让它完全无声:
1. 服务端对这个字段**不做格式校验**(只 `TrimSpace`,并且明确允许为空);
2. 服务端当前**只写不读** —— 投递时用的是**邮件自己的** `n.SessionID`
(`notify/mail.go` 构造 `push.NewMail{SessionID: m.SessionID}`),
`dispatch` 只用 `Provider`/`Token` 两个字段。
⇒ 它不会 400、不会影响收信、不会出现在任何日志里,
只会在 `push_tokens` 里静静存一条**假的**会话 id。
等哪天真按这个字段路由(它存在的**唯一目的**就是那个),
人会莫名其妙被送到别的会话去 —— 而那时没人会想到根因在这个字段。
**"不会立刻炸"正是这类错最危险的地方:它不报错,它让数据开始说谎。**
## 修法:如实传空,不编一个
这一步的时机**决定了它必然为空**:`reportToken` 只在**登录成功**与**换账号**时跑,
那时用户还没打开任何会话;客户端也**根本没有"当前会话"这个状态**可读
(我确认过:`MainPage` 不记当前会话,全仓没有 `currentSession` 之类的东西)。
而服务端对这个字段**明确允许空**:"客户端还没进任何会话,此时通知只带 mail_id"。
⇒ **传空是准确的,传 URL 是错的。** 等客户端真有了"当前会话",再回来补真实 id。
## 判据(19 → 20 条)
新增「session_id 不许拿服务器地址顶替」,钉的是**值的来源**而不是"有没有传",
所以既能挡住"再塞个别的字段顶替",也不妨碍将来补报真实 id(那时这条要改成钉真实 id)。
变异验证两个方向都做了:
- 还原成原 bug(传 `.server`)⇒ `not ok 9` 红;
- 换成 `.username` 顶替(换汤不换药)⇒ 同样红;
- 恢复 ⇒ 20/20 绿,文件逐字还原。
编译侧反向对照照旧:在新代码那行植入必然类型错误 ⇒
`ArkTS Compiler Error: Type 'number' is not assignable to type 'string'.
At File: …/PushService.ets:255:11` ⇒ `BUILD FAILED`(证明这个模块真被编译,
不是"改了没接线所以通过")。
真机:装新包后行为不变仍是静默失败(`push token 取不到(静默,属正常):Illegal application identity.`),
`jscrash`/`FaultLogger` 计数 0 ⇒ 这个修复没有改变失败路径的形状,只改了不再写假数据。
|
|||
| 549836193d |
跨端: fix(推送客户端) 热启点通知**不跳转** —— 真机实测逼出来的那半(只写格子没人读)
这是"收尾"里**静态判据看不出来**的那一类,只有设备能抓住。
## 实测经过(模拟器,可复核)
只写 `PendingRoute` 的那版(上一个提交 423ff9f):
aa force-stop → aa start --ps data '{…open_mail…}' ⇒ render MailDetailDestination ✓ 冷启能跳
(应用已在运行)aa start --ps data '{…open_mail…}' ⇒ **0 次** ✗ 热启不跳
根因:冷启时页面**刚挂载**,`aboutToAppear` 会读那个静态格子;
热启时页面**早就挂载完**了、`aboutToAppear` 不会重跑 ⇒ 格子写得进去、**没人读**。
症状正是"点了通知,App 弹到前台,停在列表页" —— 而这恰恰是推送**最常见**的用法
(App 在后台,用户点通知回来看那封信)。
## 修法:补"事件发生时就交出去"的那条路
`PushService.deliverRoute(route)`:**有人监听就当场交出去,没人监听才留在格子里**。
两条路都要留着,因为两种启动各走一条(冷启没人监听、热启有人监听):
- `EntryAbility.onCreate` / `onNewWant` 都走 `deliverRoute`(两条启动方式共用一条投递路径);
- `CommPage.aboutToAppear` 注册监听、`aboutToDisappear` 摘除
(不摘会叫醒已销毁的页面);
- 冷启与热启**共用同一个落点** `navigateToRoute`(各写一遍必然漏改一处)。
不用页面生命周期兜(`onPageShow` 之类):那会在"用户手动返回列表"时反复触发跳转,
而这里要的是"事件发生的那一刻"。
## 验证
真机(模拟器,装新包后实测):
- 热启 ⇒ `render current custom node: MailDetailDestination` ✓(修之前 0 次)
- 冷启回归 ⇒ 仍然 1 次 ✓(没被这次改动破坏)
- 截图硬证:详情页(返回箭头 + 详情窗格);"加载失败"是我塞的假 mail_id
(`WARM456`)的**正确**后果 —— 说明确实带着那个 id 去取了
判据:`harmony-push.test.mjs` 18 → 19 条,新增「接线④:热启要能跳」,
**变异验证过两个方向**(deliverRoute 退回"只写格子" ⇒ 红;删掉监听器注册 ⇒ 红)。
它单独存在的理由:接线③那种"有人读"的检查**会放它过去** ——
`aboutToAppear` 里读 pendingRoute 完全满足③,而热启路径是死的。
|
|||
| 423ff9fbb2 |
跨端: feat(推送客户端) 收尾两处接线(pi 邮件 66bbd929 分工)+ 换账号补报
`PushService.ets` 与 `pendingRoute` 都早就写好了,但**没人调用/没人读** —— 而"写好了"与"接上了"是两件事:ArkTS 只编译可达模块,往一个无人 import 的 文件里放必然报错的类型错误,`assembleHap` 照样 BUILD SUCCESSFUL (pi 与我各复现过一个方向)。这次补的是**边**,不是点。 ① 登录成功补报(LoginPage):`EntryAbility.onCreate` 那次在**登录之前**跑, 那时 `ApiClient` 还没有 token ⇒ 服务端 401,而 `reportToken` 的失败是静默的 ⇒ "启动时报过"不等于"这个账号登记过"。补三条登录路径,一条都不能漏: 快速路径(已有账号直接进主界面,老用户走这条)/ tryRestore / doLogin。 ② 换账号补报(SettingsPage.switchTo,**我加的第三处**):服务端语义是同一 token 换账号**转移**而非并存 ⇒ 新账号其实没登记过。幂等标记按 `accountKey|token` 存,所以这里调一次必然重发,不会被"我报过"挡掉。 不补的症状:切完账号,通知仍推给上一个账号。 ③ pendingRoute 消费端(MainPage.CommPage):`EntryAbility` 已把通知 data 解析成 `pendingRoute`(冷启 onCreate / 热启 onNewWant 都写了), **但没有任何东西读它** ⇒ 点通知只拉起 App、停在列表页。 在 CommPage 消费(详情页是这个 Navigation 的 navPathStack 上的路由, 栈只有它持有),消费后**立刻清空**(它是"待处理"不是"当前页", 不清则返回列表再进来会被反复跳走)。 判据:`harmony-push.test.mjs` 15 → 18 条,三条新判据钉的就是上面这三条边, **三条都做了变异验证**(各自删掉对应接线即红、恢复即绿)。 `LoginPage` 那条数**恰好 3 处**调用:少了快速路径=老用户永远不补报, 多了要问清是哪条路径。 编译验证(不是"命令成功",是"它真读了这个文件"):三处改动各植入一次必然 类型错误 ⇒ BUILD FAILED 且 `At File:` 指到**新代码那一行**(MainPage:1173 / LoginPage:108 / SettingsPage:456),恢复后 BUILD SUCCESSFUL。 产物侧证:`modules.abc` 里能查到 `consumePendingRoute`(2) / `reportPushToken`(2)。 |
|||
| a47b42b36e |
修复: 套件读数不再取决于"你怎么调用它"—— 子进程钉 cwd: ROOT + read.mjs 相对路径按包目录解析(第二层);并把"崩了"从 red 里分出来(结构判据,不加错误关键字)
pi 2026-09-17 实测报的:**同一份代码、两种调用法、两个结论**。我复现并修了两层。
## 一、复现(同 HEAD,只换 cwd)
| 调用法 | 修前 RESULT |
|---|---|
| `cd client/electron && node test/run-all.mjs`(`npm test` 入口)| `ran=29 checks=440 red=8 unreported=0` |
| `node client/electron/test/run-all.mjs`(仓库根)| `ran=27 checks=348 red=10 unreported=2` |
差 **92 条**(= 88 + 4),红 8 → 10。两次都稳定复现。
**根因**:`run-all.mjs` 的 `spawnSync` **不传 `cwd`** ⇒ 子进程继承调用者的 cwd。
而全仓有 **12 处**判据按**包内相对路径**读文件(`code('src/index.css')` 这种)
—— 它们本来就假定 `cwd = client/electron`,只是没人把那个假定钉住。
从仓库根跑 ⇒ `ENOENT` **崩在 import 期**。
## 二、修(两层,各管一段)
1. **运行器**:`spawnSync(..., { cwd: ROOT })` —— 一行覆盖**现有的与将来的所有**判据。
方向是"让运行器去满足判据的既有假定",不是改判据去迁就调用者。
2. **`read.mjs`**(第二层):`code/prose/bytes` 里的相对路径一律按**包目录**解析
(基准取自 `import.meta.url`,**不是** `process.cwd()` —— 前者是代码里的结构事实,
后者正是这个 bug 的成因)。pi 报的复现步骤是**直接跑单个判据文件**,
那条路**不经过运行器**,所以这层不是重复。
基准 `PKG` 也**导出**了:`readdirSync`/`existsSync` 不经过 `read.mjs`,
各处自己拼一次就是"同一个事实多份实现"(本仓反复消的形状)。
**验证(三种 cwd × 两个相位,逐字节一致)**:
```
build /home/program/agentmail ran=29 checks=440 red=8 unreported=0
build /home/program/agentmail/client/electron 同上
build / 同上
install /home/program/agentmail ran=27 checks=428 red=7 unreported=0
install /home/program/agentmail/client/electron 同上
```
(三种 cwd 一致、且与 `npm test` 入口一致 ⇒ 不存在"另一种调用法给别的数"。)
## 三、★ 第二处:那两条不是"红了",是"没跑完"——而它被报成了 red
`narrow-layout` 从仓库根跑:**先打完 43 条"通过",然后第 44 条 ENOENT 崩掉**,
而 `CRASH_SIGNS`(关键字表)**不认 ENOENT** ⇒ `crashed=false` ⇒ 落进 `reds`:
`- test/narrow-layout.test.mjs(退出码 1)`,**一条 `↳` 自述都没有**。
读者看到的是"这条判据不成立",真相是"**它有多少条根本没测**"。
同一次运行里真红是带 `↳ 自报 88 > 登记 64` 自述的 —— **同一种红,两种含义**,
而 `red=10` 把两者混着报。
**★ 我没有照 pi 说的往 `CRASH_SIGNS` 里加 `ENOENT|EACCES|no such file`**:
第 329 行那条注释早把方向定死了 —— **"我不再往里加更多错误关键字:那是往文本解析里加补丁"**,
而且那个表**必然漏**(今天漏 ENOENT,明天漏 ENOTDIR/ELOOP…),加上去就是**第六次**修同一形状。
**改用结构事实**:`exitCode !== 0 && checks === null` ⇒ broken。
一个跑完的文件**必然**自报条数;"跑了但没自报"是另一条**单独记账**的情形(`unreported`)。
两者不会同时成立 ⇒ 只吃掉"崩了且毫无自报"那一格。
**要害是这个区分**:`exitCode !== 0` 本身**不足以**判 broken ——
不然真红会被一起吞掉,那是**假绿方向的错**,比误报成红更坏。
(实测 `build-stamp` 是真红且有 `# tests 7` ⇒ `checks=7≠null` ⇒ **不吃它** ✓。)
**变异验证**(撤掉 `cwd: ROOT`,模拟 pi 报的原状):
`files=29 ran=27 checks=348 red=8 broken=2 unreported=2` ⇒
那两条正确归 **broken**(`(退出码 1,且一条条数都没自报)`),而 **`red` 仍是 8 —— 没吃掉任何真红**。
还原逐字节一致(sha256 相同)。
## 四、★ 我自己在实现里踩的两个坑(都实测抓到)
1. **`abspath()` 用 `isAbsolute(p)` 判绝对路径 ⇒ 传 `URL` 对象抛 `ERR_INVALID_ARG_TYPE`。**
`narrow-layout.test.mjs:8` 正是 `prose(new URL(p, import.meta.url))` ——
一个**本来正确且与 cwd 无关**的调用,被我改崩了(两个 cwd 都崩,所以**看起来像"我修好了"**)。
⇒ 判据改成 `URL` 对象**原样透传**:它已是解析过的位置,转换才是引入 bug 的那一步。
教训:**改一个共用入口时,要先把调用方的所有形态列全**,否则"修 A 破 B",而 B 看起来无关。
2. **`join` 忘了 import** ⇒ `ReferenceError`。补上。
(正是 `run-all.mjs` 里那条 `CRASH_SIGNS` 注释讲的老故事。)
另外:我在 `run-all.mjs` 新注释里写了带括号的入口名,**被 `criteria-hygiene` 第 3 条抓到**
(它不剥注释)—— 这是我上一轮刚记下的坑的**第二次**,已改掉。
## 五、测量
| 相位 | RESULT(三种 cwd 一致)|
|---|---|
| build | `files=29 ran=29 checks=440 pass=438 fail=2 red=8 broken=0 unreported=0` |
| install | `files=29 ran=27 checks=428 pass=427 fail=1 red=7 broken=0 unreported=0` |
重新接线后 `narrow-layout` 自报 **88** 条(登记 64)⇒ 它那条"登记数没跟上"的红变成**有自述**的了。
剩下 8 条红都不是本次改动(清一色并发会话的工作:`harmony-nav` 正在被改等)。
|
|||
| 2e53871a22 |
fix(harmony): 底栏真正浮起 + 内容穿过 + 加回文字标签
用户(2026-09-17):「底栏不是玻璃质感,滑动内容无法穿过底栏」,
随后「还是在导航栏加上文字吧,没有文字还是不好看」。
── ① 底栏没浮起:`width('100%') + margin` 在 ArkUI 里不缩宽 ──
NavBar 原来是 `Row().width('100%').margin({left:16,right:16,bottom:12})`。
ArkUI 的 margin 加在宽度**外面**:100% 再配左右 margin 不会缩到
「100% − margin」,而是整个顶出父容器、两侧被裁。
实测底栏左缘 x=0、右缘贴满 1256(应各留 16vp=56px)⇒ 看起来是贴边通栏,
不是浮在壁纸上的胶囊。这与收件箱头卡片修过的是同一个坑。
改法:外层 Column 带 padding(与 WebUI `.narrow-nav` 的 margin 同一几何)。
实测修复后左缘 x=56 = **16.1vp**(期望 16)。
── ② 内容穿不过去:让位加错了层 ──
原来把 `NAV_CONTENT_RESERVE` 加在**窗格的 `padding({bottom})`** 上。
那不只"让最后一行滚得出来",还**缩短了窗格本身** ⇒ 内容永远到不了条底下
⇒ 玻璃条背后只剩一张已经被壁纸层模糊过的壁纸 ⇒ 系统材质无东西可糊
⇒ 看起来就是一块普通浅色面板。**这就是"不是玻璃质感"的真正原因**。
改法:窗格满高(内容滑得到条底下,真正穿过),让位改到各滚动容器的
**内容末尾** `contentEndOffset(this.navReserve)`(5 个 List + 日历日程),
与 WebUI 同构(`.narrow-shell` 是 flex-col,列表满高、`.narrow-nav` 叠在上面)。
实测:条内高频细节 0.21 vs 条外正文 6.04 ⇒ **28.8×** 衰减,模糊确实生效。
连带修:FAB / 回复球原来靠窗格 padding 躲开条,现在自己按 navReserve 抬
(否则压在条上),实测 FAB 底距条顶 24.4vp。
── ③ 加回文字:并纠正一处**事实错误** ──
代码注释里写着「WebUI 的底部导航是**纯图标**」,并据此去掉了文字。
那句是**错的**:`NarrowNav.tsx:83-86` 每个导航项是
`flex flex-col items-center justify-center gap-0.5`,里面先 `<Icon />`
再 `<span className="text-3xs leading-none">{short}</span>`
—— WebUI **一直有文字**(通信 / 日历 / 联系人 / 我的)。
那条判据把一个假事实固化成了规则,还反过来挡住了正确做法。
现在按真实契约钉:图标(24vp,与 WebUI 24×24 同几何)+ 文字,
两者过**同一个**选中三元式(只换颜色,不加背景/指示条)。
判据(全部含变异自检):
· ⑤ 重写:两处选中三元式(图标+文字各一)、必须有 Text(item.label)、
图标 24vp;变异自检覆盖"缺文字 / 文字不换色 / 图标不上色"。
· ④ 改:留白走外层 padding(并反向禁止 margin 做留白);
让位改为 ≥5 处 `contentEndOffset`,并禁止退回窗格 padding 让位。
· 连带更新 ②/⑤ 与 appearance/logic/widescreen 的组件签名断言
(组件多接一个 navReserve)。
· 删掉一段会假红的"从内容层 `}` 往后扫"的窗口式断言:它靠数花括号定界,
而注释里就有大量 `.padding({` 字面量,切片能长到 1700+ 字符,
一路扫进 `onAreaChange`(那里合法地出现 NAV_CONTENT_RESERVE)——
正是本仓库反复记的窗口式判据,故改为纯负向断言。
测试:212 passed / 0 failed;`devecocli build` 通过;
模拟器截图与像素测量逐项核对。
|
|||
| b6b50cbb88 |
修复: 套件因"清单自检"停了整整一天(一条判据都没跑)—— 补接线 3 个从未进 SUITE 的文件 + 修 2 处裸 readFileSync + 守卫补报"因此没有跑任何判据"
pi 报的 `ran`/`checks` 双计**早已修掉**(`d23ef69`,他读的是它的祖先 `2774b00`)。 但顺着那条线实测,发现了一个**更严重的现况**:**套件一条判据都没在跑。** ## 一、★ 现况:`run-all.mjs` 输出只有一行,0 条判据 ``` $ node test/run-all.mjs 这些判据文件没接进套件(写了却不会跑):test/harmony-push-optin.test.mjs、… 退出码=1 stdout 0 行、stderr 1 行(build 与 install **两个相位都死**) ``` 自检 2("test/ 下每个 *.test.mjs 都要在清单里")在**跑任何判据之前** `process.exit(1)`。 **逐提交对照(worktree 隔离)**: | 提交 | 套件 | |---|---| | `cfe7808^` | `RESULT files=26 ran=23 checks=332 …`(正常)| | `cfe7808`(09-16 07:46,加了 2 个文件)| **一行"没接进套件",0 条** | | `c523c21`(09-17 17:43,又加 1 个)| 同上 | ⇒ 整仓**超过一天没有任何判据读数**,而失败信号只有那一行 —— **看起来像环境问题**,不像"套件坏了"。三个文件**从未**进过 SUITE(`git log -S` 查证:0 次提及)。 ## 二、修 1. **接线 3 个文件**(`harmony-push-optin` 4 / `harmony-reply-target` 7 / `harmony-widescreen` 6 条)。 2. **接线后立刻暴出 2 处违规** —— 这是"漏接线"的**第二层后果**:不只它自己不跑, **连它自身的违规也没有任何东西会看见**: - `harmony-reply-target.test.mjs:189` 裸 `readFileSync` 读 `.go` ⇒ 改走 `code()` - `harmony-apibase.test.mjs:266` 裸 `readFileSync` 读 JSON ⇒ 改走 `prose()` (两条在纯净 HEAD 上**都已存在**,`criteria-hygiene` 第 2 条现在 6/6 绿。) 3. **守卫补报后果**:原来只说"这些文件没接进套件",**没说"因此本次一条判据都没跑"**。 ⇒ 补上那句 + 说明"修法是把文件接进 SUITE(不是改这里)"。 **守卫本身保留**(漏接线不该静默),但**它要为自己造成的后果作证**。 变异验证:临时摘掉一条 ⇒ 新报文出现;还原逐字节一致。 ## 三、★ 我接线时被自己写的注释绊了一次(值得记) `criteria-hygiene` 第 3 条扫"用了 `code`/原文读取/`bytes` 却没 import",而它**不剥注释**: 我在注释里写了"那个入口保留注释"**并带上括号**,它报「用了 … 但没 import」。 ⇒ **"注释里说明禁令"不等于"违反禁令"**(我们早立的规则), 而其镜像也成立:**注释里提到函数名会被判据当成使用**。两个方向都得当心。 ## 四、测量(29 个文件,两个相位都活了) | 相位 | RESULT | |---|---| | build | `files=29 ran=29 checks=440 pass=438 fail=2 red=8 broken=0 unreported=0` | | install | `files=29 ran=27 checks=428 pass=427 fail=1 red=7 broken=0 unreported=0` | `broken=0 unreported=0`(接线前那三个文件是 3/3 —— 它们自报不了条数)。 剩下 8 条红**都不是本次改动**:`narrow-layout`/`nav-merge`/`harmony-presets`/`build-stamp`/ `commit-hygiene` 等是**并发会话未提交的工作**(`harmony-nav` 正被改), 在纯净 HEAD 上它们**看不到**(因为 HEAD 上套件根本没跑)。 |
|||
| c523c21e22 |
feat(harmony): 邮件详情与「我的」页 1:1 对齐 WebUI,并修两个线上 bug
用户:「为什么邮件页面没有对齐 webui?」「我的页面也没有对齐」,
选定「完全 1:1」。两个页面都不是「没做」,而是**做了一半** ——
`AuthApi` 的 logout/listKeys/createKey/revokeKey 全都写好但没人调,
`Models.ets` 的字段声明漏了服务端一直在返回的那些。
── 邮件详情页(对齐 WebUI `MailView.Header` / `CollapsibleHeader`)──
· 头部改**可折叠**(默认收起):收起只留标题 + 必须常驻的状态点
(未读 / 权限请求)+ 箭头。WebUI 的理由:顶部信息常驻会把可读区压成
一条缝(实测 1280×800 下头部 17% + 回复框 31%,正文只剩 48%)。
· 发件/收件改**三段式完整地址**:Agent → `pi@/home/program/agentmail.别名`,
人 → 只有名字。新增 `model/ReplyTarget.ts`(逐字移植 WebUI `replyTarget.ts`,
并对齐后端 `models.FormatAddress`)。
· 时间:`localDateTime()` → `2026/09/15 11:37:07`(原为裸 ISO `2026-09-15T03:37:07.14758Z`)。
· 档位:`permissionLabel()` → `只读/目录内/全权`(原为英文 `plan/workspace/full`;
这个函数早就在 `MailGrouping.ts` 里、列表页也在用,只有详情页没用)。
· 新增抄送行;回复入口改**右下悬浮球**(WebUI `reply-fab`),
不再是底部 56vp 通栏按钮。
── 「我的」页(对齐 WebUI `AccountPage`,补 5 个缺失 section)──
标题由「账号管理」改为「我的」(多账号只是其中一段,不是整页的目的)。
· 基本资料:用户名/显示名/角色/状态/创建时间/最后登录。
· 权限范围:可调用 Agent / 可访问目录(非管理员;空数组 = 不限)。
· 修改密码:三输入框 + 不一致就地提示(新增 `AuthApi.changePassword`,
该端点服务端一直有、客户端从未包)。
· 客户端连接密钥:列表 / 新建 / 吊销 + 一次性全文提示
(`AuthApi` 的三个方法终于被调用)。
· 退出登录:先注销推送 token → 清凭证 → **清全部账号** → 回登录页。
· 整页改为**一个 Scroll**:原来账号列表 `layoutWeight(1)` 占满剩余高度,
排在它后面的 section 被挤出可视区且滚不到(WebUI 注释里正是这个坑),
而「退出登录」在最下面 ⇒ 等于退不出去。
── 顺带修掉的两个线上 bug(都是判据发现的)──
① `MeApi.get()` 调 `GET /me`,但服务端**没有**这个路由(只有 `/auth/me`;
`/me/*` 下是 mail/sessions/keys/appearance 子资源)。
后果:这把调用恒 404 → `loadRole()` 恒走 catch → `isAdmin` 恒 false
→ 「管理」入口对**包括管理员在内**的所有人永远不显示。
实证:`GET /api/v1/me` → 404;`GET /api/v1/auth/me` → 200
`{"user":{"role":"admin",...}}`。修复后截图里「管理」入口已出现。
② 详情页回复写 `req.to = this.fromName + '@'`:那个游离的 `@` 让 `name@`
被后端 `ParseAddress` 解析成「有 path、无 session」⇒ 落到该 Agent 的
**默认会话**,而不是用户正在看的那条线索。改用 `replyTargetAddress()`。
── 判据(新增 7 + 8 条,全部含变异自检)──
· `harmony-reply-target.test.mjs`(新):直接执行 `ReplyTarget.ts` 断言行为
—— 空 path 必须留 `@`(否则整串被当成名字、投递 404)、人只有名字、
会话位必须带上、`from_workspace` 不得用来拼地址(会得到 `dsh@dsh`);
并读 Go 源码比对三分支结构。
· `harmony-admin.test.mjs`(+5):`/auth/me` 路径(含服务端注册与响应形状)、
八个 section 齐全、退出清全部账号、整页一个滚动容器、字段来自服务端。
变异自检:把 `/auth/me` 改回 `/me` → 判据变红(已验证)。
测试:212 passed / 0 failed;`devecocli build` 通过;
模拟器截图逐项核对(详情收起/展开两态、我的页全部 section)。
未验:真机观感;横屏 Auto Split 双栏。
|
|||
| 65c3ef8094 |
fix(harmony): 悬浮加号移进 Navigation 列表侧,修详情页叠按钮
症状(2026-09-17 模拟器截图硬证):窄屏 Stack 模式点开邮件详情,右下角**同时**
出现两个圆形按钮 —— 详情自己的「回复」(蓝色胶囊)与外壳的 compose 加号,
后者悬空压在详情上。
根因:加号原先是 `Navigation` 的**兄弟**,平级放在外层 Stack 里。
详情画在 `Navigation` 内部,而兄弟节点画在 `Navigation` 之上 ⇒ 永远压住详情。
WebUI 的对应物是 `.comm-pane`(`App.tsx`: `{listBody}<ComposeFab />`):
加号在**列表窗格内部**,窄屏滑上来的详情层把列表整块(含加号)盖住,
详情自己的回复按钮才露得出来。Split 模式下加号仍在左栏,与 WebUI 两栏并排一致。
改法:把 `Stack({ alignContent: BottomEnd }) { 列表; 加号 }` 整体放进
`Navigation(this.navPathStack) { ... }` 的构建体内;通信页根的
`bgActive ? Transparent : pageBg` 保留(判据要求 ≥5 处让出页面底)。
判据(harmony-nav.test.mjs 新增一条,含变异自检):
- compose 加号必须在 `Navigation(...)` 的花括号**内**;
- `.navDestination(...)` 之后不得再出现 compose 加号。
用 `git show HEAD:...MainPage.ets`(改动前版本)复核:两条断言在 bug 版本上
**都失败**(inside=False / after=True),说明判据抓的正是这个层级错位,
不是"存在一个 compose 按钮"那种在 bug 版本上也成立的弱断言。
实测(模拟器 1256×2760):详情页只剩自己的「回复」按钮,加号不再悬空;
截图 /tmp/harmony-fabfix4.jpeg。列表页加号位置不变(仍在右下角)。
测试:200 passed / 0 failed。
|
|||
| fc3fff4f36 |
fix(harmony): 滚动列表上下边缘渐隐,替掉硬截断
用户(2026-09-17):「上下还是硬截断,不是 webui 那种渐变」。 WebUI 的做法是 `index.css` 给 `.overflow-y-auto` 加 `mask-image: linear-gradient(to bottom, transparent 0, #000 min(12px,10%), ...)`。 ArkUI 的对应物是滚动容器自带的 `fadingEdge(enabled, { fadingEdgeLength })` (API 14+,定义在 `ScrollableCommonMethod` 上 ⇒ List/Scroll/Grid/WaterFlow 都有), 它淡掉的是**渲染结果本身**,与背景无关。 **刻意不用** linearGradient + mask 手搓渐变色遮罩:那是拿背景色画一个 并不存在的"底色",在壁纸/玻璃主题下必然露馅(壁纸本身带颜色,遮罩层对不上)。 - 新增 `LIST_FADE_LENGTH = 12`(NavItems.ts):与 WebUI 的 `min(12px, 10%)` 同量级,也接近列表内边距(10vp)—— 静止时几乎看不出,滚动时才起作用。 - MainPage 的 5 个列表容器(收件箱/发件箱/授权/联系人卡片/联系人列表) 全部挂 `.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })`。 显式传长度而不是用默认 32vp:WebUI 那边踩过同一个坑 (「渐变用的过猛了,比如收件人候选那里」——固定像素用在高度不固定的容器上)。 判据(harmony-nav.test.mjs 新增一条):常量值、五个列表都挂、长度走 LengthMetrics.vp、禁止 linearGradient+mask 顶替;并做了变异自检 (删掉任一处 fadingEdge 必须判红,实测通过)。 实测(模拟器 1256×2760,密度 3.489): - 滚动后顶边被切断的卡片文字明显洗白(红色评分 5.5 vs 完整卡片 15.5); - 底边同样渐隐;截图 /tmp/harmony-fade-scroll.jpeg。 测试:199 passed / 0 failed。 |
|||
| ff75339baa |
fix(harmony): 图标按 px→vp 正确缩放 + 底栏四入口 + 卡片 10vp 缩进
图标过小的根因(实图硬证):`Path.commands` 的坐标按 **px** 解释,不是 vp。
- `Shape.viewPort` 只裁剪不缩放(官方样例"缩放成立"是因为它用 Rect/Circle
自带 vp 宽高,不是裸 Path.commands);
- 改动前的 `Path.scale({iconSize/24})` 对 iconSize=24 等于 1.0,同样没缩放。
症状:FAB 内 compose 只画 24px(应 24vp=84px),且偏在盒子左上角。
修法:`scale({vp2px(iconSize)/24})` 把 24 单位路径放大到 iconSize vp;
`scale` 会连带放大描边,故 `strokeWidth = strokeWeight / vp2px(1)` 抵消,
否则 1.8vp 描边会变成 ~7.3vp 的"实心块"(实图已验证)。
用 `this.getUIContext().vp2px()` 而非全局 `vp2px`——后者已废弃,
`harmony-system-api` 判据对全局调用默认判红。
验收(模拟器实图,密度 3.489):
- FAB 内 compose 图标 84px = 24vp ✓(原 24px)
- 底栏图标描边 7-8px ≈ 1.8/24×28vp=7.3px ✓(原为实心块)
- 四个图标均为清晰描边,与 WebUI icons.tsx 同几何
底栏四入口:新增"我的"(person → pages/SettingsPage),对齐 WebUI
通信/日历/联系人/我的;NAV_CONTENT_ITEMS 保持宽屏侧栏三项,
避免宽屏出现重复 person 图标。
卡片缩进:`width('100%') + margin({left:10,right:10})` 在 ArkUI 里不会
缩到 90%——margin 加在宽度外面,卡片顶出父容器(实测左缘只留 ~1px,
列表卡才有正确 10vp)。改为外层容器加 padding。验收:页头卡与邮件卡
左缘同为 x=35(10.0vp)、右缘 x=1219。
推送:reportToken 先申请通知权限再取 token(部分设备权限未开时
getToken 返回空 / 报 1600004),并补两条文件形状判据钉住
client_id 配置与调用顺序。
测试:198 passed / 0 failed。
|