Commit Graph

12 Commits

Author SHA1 Message Date
2fe023735e 跨端: 3 处判据基建修正(清册正则 / 变异锚点 / 计数棘轮)+ baseline 第 10 次重算
起因:`node run-all.mjs` 报 `verdict=red` 但四个计数全是 0 ——
红来自**到期闸**(`dueFailed`)与**计数棘轮**,不是断言失败。
逐个查清,三处都做成了真问题并留证。

## ① 跨文件手写色清册的正则从 `{6}` 放宽到 6 或 8 位

`glassCard` / `glassCardWall` 是 **8 位**(`#EBFFFFFF` / `#C7FFFFFF`,
半透明白是 WebUI `.glass-card` 的本质),而那条判据的正则只认 6 位 ⇒
它看不见这两个令牌,却报出「清册里的 glassCard 已不存在(清册过期)」——
**病因报错了**:不是清册过期,是正则比被判的东西窄。

改成 `{6}(?:[0-9A-Fa-f]{2})?`。★ 不能写 `{6,8}`:那会把 7 位这种非法长度也放进来。

同一形状在另两条判据上各出现一次(`B|旧机制不得回来` 与
`遮罩:交给系统的遮罩语义色`),它们原来**全仓**扫 `#……{8}` ⇒ 把这两个
**不是遮罩**的令牌一起撞红。都改成**同名枚举白名单**(不是放宽:
遮罩那个真实约束原样保留 —— `overlay` 写成 8 位单色照样红)。

## ② 变异锚点过期(守具有齿但没挂上)

`jobs/jobs-all.json` 里「写死色值 + 去卡片圆角」那条的锚点,
被 `6ca0113` 插进去的 `.attributeModifier(PressEffectModifier.of())` 隔断 ⇒
`hits=0`、`skipped=1`,而**没有任何东西变红**。

已把锚点补成当前代码形状,`ran` 从 51 回到 52、`skipped=0`。
★ 这是本仓那个老形状的又一次实例:**"清单没跟上代码"不会自己报警**,
得靠 `hits=0` 那条判据;它本次确实报出来了(`diag=mutant-anchor-stale`)。

## ③ 计数棘轮 6 → 7

给 `cross-client-logic` 新增了 `AddressSuggest` 一组(地址补全纯逻辑),
而套件只判**下界** ⇒ 不同步这个数字,将来**删掉**那一组不会有任何东西变红。
已显式改成 7,并写清"为什么必须手改"。

## ④ baseline 第 10 次重算(先证不是残留才重算)

`AdminUsersPage.ets` / `SettingsPage.ets` 哈希对不上,逐个取证:
- `sha256sum -c` → 这 2 个 FAILED(另 5 个 OK)
- `git diff --quiet HEAD` → **空**(与 HEAD 逐字节相同)⇒ 底本**过期**,不是残留
- `git log --oneline -3` → 最后一次动它们是 `f83c233`(我上一批的有意编辑)

★ 值得记一笔:漂移的是**上一轮**的文件,我这一轮完全没碰它们。
若只按"`git diff HEAD` 空就放行",这条**永远发现不了自己漏了一次重算** ——
这次是靠 `summary.py` 的 `baseline-stale` 主动报出来的。

## 结果

`files=34 ran=34 checks=543 pass=542 fail=0 skip=1 red=0 broken=0`
`mutants=52 ran=52 skipped=0 diag=none baseline=7/7✓`
(`verdict=red` 仅剩到期闸:5 条"只能静态"的判据,其到期前提"能装能点设备"
  现已成立,需要各自升级成行为判据 —— 已登记,不由这条提交关闭。)
2026-09-21 14:32:38 +08:00
5e4a1b616c 跨端: B 的交付物(跨端纯逻辑一致性判据)+ 照 skill 回扫修掉两处隐形债
用户:「你为什么不加载鸿蒙开发相关skill?」—— 说得对。那份
`arkts-grammar-standards` 写着 "REQUIRED before writing the first .ets file of a
session",而我这轮一直在写 `.ets`。补加载后照它的规则表**逐条回扫**,
当场抓出两处此前没人管的违规。

══ ① 用户要做的 B:`cross-client-logic.test.mjs`(新,6 条判据)

背景:两套纯逻辑各写一份且已分叉(replyTarget 214/170 行、mailGroups 178/459、
appearance 187/324、calendar 208/446)。当天已**踩到**两处分叉
(`participantAddress` 的 `||`、`ThreadPage` 字段全错)。

做法:**同一张用例表喂给两边,逐条比结果**(`--experimental-strip-types`
直接在 node 里跑两侧源码 —— 两边的 model 层都是纯逻辑、无 SDK 依赖)。
不选"生成一份共享源码":harmony 不能 import 工程外文件,
且两边类型系统不同(ArkTS 禁解构/any/对象字面量要具名类型),
生成器要维护"两边都能过"的子集,是另一个大工程。

★ **首轮运行就报出两处真分叉,都不是我踩到才发现**:
  ① `formatAddress('dsh', undefined, undefined)`:electron 返回 `"dsh"`,
     harmony **抛** `Cannot read properties of undefined`。
     —— 又是 `omitempty` 那个坑(**第三次**),这次是判据先报的。
  ② `monthGrid`:electron **固定 6 行**(`grid-rows-6`),harmony **4~6 行**
     ⇒ 翻月时网格高度跳动。WebUI 的注释明写要避免这个("行数变化会让整个
     网格高度跳动,翻月时页面内容上下弹")。
  ③ 顺着 ② 又发现:WebUI 邻月格子**填真实日期并置灰、可点**
     (`CalendarView.tsx:545-556`),harmony 留**空白格**。

★ 判据自身的两次错,都留了档(判据的 bug 与代码的 bug 一样危险):
  · 第一版把 `args[0]` 当单个参数传,字符串被当可迭代对象展开 ⇒
    `formatAddress('d','s','h')` —— **判据自己造出假分叉**。
  · 第一版 `weekStart` 传 0(周日),而两端实际都是 1(周一)⇒ 又一处假分叉。
    差一点就去"修"一个不存在的问题。
  · `monthGrid` 的投影第一版按 `inMonth ? [y,m,day] : null`,
    把"邻月填不填真日期"这个**真分叉**抹平了 —— 投影只该换表示,不该替我看不看。

★ 三类"不同"要分清(写进文件头):**命名不同**(投影归一,不是分叉)、
  **签名不同**(ArkTS 没 Date 重载习惯;语义必须一样)、**行为不同**(是分叉,以 electron 为准)。

══ ② skill 回扫抓出的两处隐形债(编译器只告警、判据也不管)

· **正则字面量**(`arkts-no-regexp-literals`):`MailDetailPage.ets:615` 的
  `/^\d+$/`(从 2026-09-19 活到今天)。
· **废弃的全局 `router`**:`api/Logout.ets:76` 的 `router.replaceUrl(...)`。
  它是个独立函数(没有 `this`)⇒ 拿不到 `UIContext`,改成由调用方传
  (两个调用点都持有 `getUIContext()`,零成本)。

★ 这两条为什么能活这么久:**编译器对它们只告警、不挡构建**,
  全仓也**没有判据**管 ⇒ 规则事实上不存在。已补两条判据,都做了变异验证。

══ ③ 顺带修正一条**恒真的同义反复**断言

`harmony-calendar` 里 "today 不在本月:不许标在别的月" 那条:
它是在"邻月格子是空 `DayCell`(`iso` 为空串)"时写的 ⇒ `c.iso === today`
**永远不可能**匹配 ⇒ `count === 0` 恒真,**看起来守着一条规则,其实什么都没守**。
改成真不变量:**"被标为今天的那一格,iso 必须就是 today;至多一格"**,
并反向核对"2026-10-01 确实出现在 9 月网格里"(否则那段是空转)。
实测 WebUI `CalendarView.tsx:547` 是逐格 `isSameDay` ⇒ **它会标**,
所以原来那条"不许标"本身就窄了一半。

══ ④ 登记两处盘点发现(**没有**顺手改,因为需要人决定)

· `harmony-dead-pages`:`InboxPage.ets`(238 行) 不可达(不在页面表、无人导航),
  `SessionsPage.ets`(170 行) 唯一引用来自 InboxPage ⇒ 一起不可达。
  没删是因为 `HARMONY-ALIGN-PLAN.md:214` 把它当变异测试靶子用过 ——
  删掉会永久丢代码,是否只是"早期留存"我判断不了。
· `harmony-permission-history`:WebUI 授权栏显示**待决 + 已决策历史**两段
  (拿 inbox 自己分组,`PermissionList.tsx:27/174/182`);鸿蒙调专用端点
  `/permission/pending`(SQL `WHERE pr.result IS NULL`)⇒ **只拿得到待决的**。
  已在 `cross-client-logic` 的 gaps 里如实登记,判据会盯着"不要再少"。

══ 判据状态

`files=33 ran=33 checks=530 pass=530 fail=0 skip=0 red=0 broken=0 unreported=0`;
`baseline=7/7✓`(底本第 9 次重算,已按规矩先 `git diff --quiet HEAD` 取证 + 记录理由)。
`verdict=red` 残余仍是 5 条静态判据的**设备到期提示**(既有机制)。

══ 环境

模拟器昨天起卡死(hdc 能连、shell 超时、CPU 150%、跑了 34 小时),
导致设备判据各跑 836 秒后失败 —— 看起来像"套件卡死"。用户批准后杀掉重启
(`Emulator -start HATriple -noWindow`,`devecocli` 那套因 x11 起不来),
现在**75~150 秒**跑完整套。
2026-09-20 22:35:35 +08:00
6ca0113fd2 跨端: 统一顶栏组件 + 按压反馈(真跑通)+ 滑动判定从"时长门"改"速度门"
用户四条意见,逐条都是真问题,且**两条是我写完没生效**:
  ① 「所有的顶栏都应该应用玻璃圆框效果」② 「抽象一个统一的顶栏组件出来吧」
  ③ 「你不觉得发件箱太大了吗」④ 「各个组件带响应点击、滑动等的动画了吗」
  ⑤ 「还有转场动画呢?」⑥ 「还有其他行为都要一一对齐,例如邮件展示页面」

★ ① ② 统一顶栏 `AppHeader`
  改造前**六处各写各的**:`AdminUsersPage`(40×40 返回键+16 号标题)、
  `SettingsPage`(20 号标题+40×40「+」+**实心 Theme.surface**)、
  `MailDetailPage`(36×36 + 14 号标题,行高只有 14px 被裁过)、
  `MainPage.SentTab`/`PermissionTab`(通栏、无圆角)、`CommTabBar`。
  尺寸(36/40、14/16/20)、底色(实心白/透明/无)、返回键(有/无)三类都不一致;
  且四处用 `Text('‹')` 当返回键 —— **用字符当图标**(本仓明令禁止,字形随字体变、
  基线对不齐),而 `ICON_PATHS` 里**早就有** `chevronLeft`。
  ⇒ 抽成 `AppHeader`(返回键 + 标题 + `@BuilderParam` 右侧动作区),
    几何复用底部条常量,材质走 `Theme.navMaterial`。已接入 5 处。
  ★ `topInsetPx` 由调用方传:状态栏避让是**窗口级**事实(属页面),
    组件自己读会变成"每层各加一次"(平板侧栏 83+83 双计就是这么来的)。

★ ③ 发件箱"太大"—— 我的理由错了
  我第一版让 `HEADER_HEIGHT = NAV_BAR_HEIGHT`(56),理由是"顶栏与底栏同在一根
  竖轴上,高度不同会一眼看出来"。**那个理由不成立**:
  底栏是**两行**(图标+文字标签)⇒ 需要 56;顶栏只有**一行标题** ⇒ 56 里一半是空白。
  实测 `Row [277,315,1105,476]` = 161px = 56vp,标题那行只占 54px。
  "两根轴上的条要一样高"是把**对齐**理解成了**等高**。真正要对齐的是**左右留白与圆角**。
  ⇒ `HEADER_HEIGHT = 44`(= 可点区下限,返回键装得下)。

★ ④ 按压反馈 —— 两次失败才跑通,两个坑都记进注释
  · **坑一**:`.attributeModifier()` **一个组件只能挂一个**,链两个是**后者覆盖前者**。
    会话组头卡同时挂了玻璃与按压反馈 ⇒ 只生效一个,**编译器不报错、运行不提示**。
    WebUI 是 CSS,类名天然叠加;ArkUI 是单一插槽,"叠加"必须显式做
    ⇒ 新增 `CompositeModifier`。
  · **坑二**:`applyPressedAttribute` **只对自带按压状态机的组件**(`Button`)回调。
    实测:挂 `Button` 上 → hilog 有输出;挂只有 `.onClick` 的 `Row`/`Column` 上
    → **一次都不回调**(按下与常态逐像素相同 `239,244,255`)。而全仓 117 处
    `onClick` 的主体正是纯 `Row`/`Column` ⇒ 那条路对本仓没用。
    改用 `onTouch` + `animateTo`。
  · 底色用**系统点击效果色** `ohos_id_color_click_effect`(不是我自己挑的):
    第一版用 `Theme.surfaceMuted`,实测只差 `3/3/3`,肉眼看不出 ——
    因为"一块面的颜色"与"按下时叠的提示色"在系统色板里是**两个不同语义**。
  设备验证:`onTouch type=0`(Down) → `type=1`(Up) 成对到达,像素确有位移。

★ ④ 滑动判定:从「时长门」改成「速度门」(**设计错误,不是调参**)
  旧门 `SWIPE_MAX_DURATION_MS = 700`("整个手势超 700ms 就拒")。
  而**时长 = 距离 ÷ 速度** —— 它把两个量混成一个 ⇒ 同样的手速下
  **滑得越远越容易被拒**,屏幕越大越严重(平板同一手势像素更多)。
  设备实测(3184×2232,位移恒 542.6vp):
      600px/s→5909ms | 1200→3161ms | 1800→2006ms | 2500→1429ms | 5000→616ms | 15000→123ms
  ⇒ 旧门意味着**只有 ≥5000px/s 才过**,而那一档重复测试也只有 2/4 成功
    —— 用户报的"滑不动/时灵时不灵"就是这个。
  改 `SWIPE_MIN_SPEED = 0.25`(vp/ms,取自上表两档中间)后实测:
      1200px/s → 0/3(正确拒绝:那是拖动)    2500px/s → **0/4 → 3/3**
      5000px/s → **2/4 → 3/3**
  两条判据跟着改形态(**语义不变**:都是"够快才翻页"):
  · `cross-client-gesture` 断言"算的是 dx/ms"——只断言"有个门"会漏掉这次的错
    (旧的时长门**存在**、常量**有名字**、数值**是正数**,三条全过,而真机拒掉正常滑动);
  · `harmony-logic` 的行为判据加了两条**回归钉**:
    `542.6vp/1429ms`(实测的正常甩动)必须过;
    `1200vp/3000ms`(同速度、更远)也必须过 —— 旧门正是在这里误拒。

★ ⑤ 转场动画:查清了,"不做页面级"是**决定**而不是漏做
  WebUI `index.css:1230` 写明:页面级淡入会让**已在那儿的框架**也一起暗一下
  ("观感还是闪"),且用户 2026-09-14 **亲口否掉过**"整屏一起淡",
  所以那一档整体删掉,只保留**局部**动效。鸿蒙侧当前是:
  · `pushUrl` 推页 → **系统自带的滑动转场**(连拍 8 帧验证:第 3 帧能看到
    写信页正在覆盖发件箱,是真实的横向滑入,不是硬切);
  · 局部入场 → `paneRiseIn` / `menuIn` / `calendarSlide`(已接 12 处)。
  所以"加 pageTransition"反而会与用户当时的否决冲突 —— 这一条**不做**,
  理由记在这里,免得下次又当成遗漏。

判据:files=32 ran=32 checks=519 pass=519 fail=0 red=0;baseline 7/7✓。
2026-09-20 10:51:28 +08:00
eb4e413c67 跨端: 玻璃走基础组件(透明度/磨砂/投影)+ 判据去芜存菁(修 4 个判据自身缺陷)
用户两条:「玻璃不透明/不够磨砂炫酷,要更基础组件化」+「判据去掉无意义的、保留有用的」。

★ 玻璃的透明度与质感(逐层调出来的,不是拍的)
  · 透明度:`BlurStyle` 是固定档、没有 saturate 旋钮 ⇒ 观感偏灰。
    改用参数化 `backgroundEffect({ radius, saturation })`,字段与 CSS `backdrop-filter`
    一一对应,于是可以**照抄 WebUI 的数**:`blur(18px) saturate(1.5)`
    (WebUI `.narrow-nav`,那里最"玻璃"的一档)。**saturate 才是玻璃感的主要来源**。
  · 质感:只模糊+饱和仍然"像一块平的白方块" —— 玻璃板之所以像板靠的是**边缘**。
    WebUI 的完整配方是 `box-shadow: 0 8px 28px rgb(0 0 0 / 0.16)` + 发丝描边。
    只有模糊没有投影 ⇒ 一片雾;加上投影 ⇒ 一块板。**这是"不够炫酷"的主因**
    (我先前两个方向都调了模糊与饱和,方向就偏了)。
  · 投影用**系统预置档** `ShadowStyle.OUTER_FLOATING_SM`("浮起的小面板"),
    不是手写 `{radius, color, offsetY}` —— 手写色要么是 `#AARRGGBB`、
    要么是 `rgba()`,两者都被判据明令禁止,而那条禁令**是对的**:
    手写阴影色在深色下看不见(深底叠深影 = 无变化),WebUI 在 `.dark` 里
    要把同一处从 0.16 加重到 0.55 —— 那正是"替系统猜深浅"。

★ 基础组件(用户:「定义一个基础玻璃容器给各个组件引用」)
  · `GlassCardModifier` / `PaneModifier`(`AttributeModifier<CommonAttribute>`):
    **一处定义、处处引用**。页面里的玻璃从 15 处内联三连收敛成
    `.attributeModifier(...of(this.bgActive))`。
    ★ 为什么不是 `@Styles`:全局 `@Styles` 读不到 `this`(而玻璃要知道 `bgActive`);
      成员级 `@Styles` 能读 `this` 但**每个 struct 各一份**,MainPage 有 7 个 struct
      ⇒ 只是把"每处抄 3 行"换成"每 struct 抄 1 份",没解决问题。
  · `Motion.dur()`:接系统「减弱动态效果」(`isAnimationReduceEnabledSync()`,API 23)。
    WebUI 有 `prefers-reduced-motion` 硬约束,鸿蒙这边**改造前一次都没调过**。

★ 途中撞出并修掉的**真 bug**(都是我自己的)
  · 把 `@Styles glassCard()` 里也替换成 `attributeModifier` ⇒
    「我的」页 ProfileSection/AppearanceSection/PushSection **整块不渲染**
    (设备实测:Scroll 直接跳到「客户端连接密钥」)。删掉那个死 `@Styles` 即恢复。
  · `Theme.shadowColor/hairline` 用了 `rgba()` + `#AARRGGBB` ⇒ 违反本仓两条既有禁令
    ("鸿蒙侧不写 CSS 颜色函数"、"半透明属于系统材质/职责")。改用系统阴影档。

★ 判据去芜存菁:修了 4 个**判据自身的缺陷**(都是它声称在管、实际漏判/误判)
  · 材质来源只扫 `backgroundBlurStyle` —— 玻璃改成 `backgroundEffect` 后**整类漏判**。
    实测:往 Modifier 里塞 `backgroundEffect({radius:99,saturation:9.9})` 照样全绿。
    ⇒ 补上这一类,并加"参数必须引 Theme 令牌"(否则只是换个旋钮写死)。
  · 参数抽取用 `[^)]*` —— 对 `this.materialOf()` 与 `{...}` 对象字面量都提前截断,
    **误伤合法写法**(本次被它误红两次)。改成配对括号 + 去外层花括号。
  · 嵌套判定**没比文件**:拿 A 文件的字符偏移去比 B 文件 ⇒ 判出
    "MainPage 内含 SettingsPage 的玻璃"这种物理上不可能的红。
    假红比漏报更坏 —— 会让人去改本来对的代码(本次差一点)。
  · `harmony-appearance` 数的是**内联字面量**("三元出现 ≥5 次"),
    收敛成组件后写法变了、行为没变却变红 ⇒ 改成判不变式
    (让位只有一条路径 + 每个窗格都收到开关)。
  · 顺带:设备判据找「背景」时只在首屏可见节点里找(而 dumpLayout 只给可见节点),
    且我的第一版只朝一个方向滑(越滑越远)⇒ 改成"先回顶、再逐屏向下扫"。

判据:files=32 ran=32 checks=518 pass=518 fail=0 red=0;baseline 7/7✓(第 7 次,逐个取证)。
2026-09-20 08:16:10 +08:00
5103e0aee3 跨端: 抽出基础面/玻璃组件(Motion + Surface)+ 三处硬弹的弹层补过渡
用户两条要求,各自都指向"决策被复制到各处"这个病根:
  ① 「应该定义一个基础玻璃容器给各个组件引用」
  ② 「很多页面还是不够精致……封装为基础组件供所有页面使用。基础组件包含动画」

★ 玻璃不透明的真根因(前几轮都没找到,这次是像素证据定的)
  实测模拟器 3184×2232、壁纸 aurora + 压暗 37:
      Navigation [229,140,3156,2204]   ← 255,255,255(整块内容区)
      y=2210(Navigation 之外那条缝)    ← 226,225,235(壁纸清楚可见)
  那条缝是决定性的:**壁纸层本身是好的**,白是因为上面盖了不透明的壳。
  链路上一共三层,逐层修:
    · `Navigation` 外壳:不设背景 ⇒ 系统默认不透明白,把里面已透明的窗格整片盖住;
    · navBar 内容层(列表栏根容器):同样没有背景;
    · **卡片**:铺的是 `Theme.surface`(实体系统色)。
  修后:缝隙 203,213,228 / 卡片 191,204,220 —— 壁纸透得出来。

★ 新增基础组件(common/)
  · `Surface.ets`  —— `GlassPane`(正文面)/ `GlassCard`(嵌套面)/ `PageHeader` / `Pressable`
  · `Motion.ets`   —— `Motion.dur()`:接系统「减弱动态效果」
    (`accessibility.isAnimationReduceEnabledSync()`,API 23 正好够用)。
    WebUI 侧有 `prefers-reduced-motion` 硬约束(index.css:1266),
    鸿蒙这边**改造前一次都没调过** —— 系统里关了动画,我们照样动。这是无障碍义务。
  · `Theme.cardMaterial` —— 卡片材质令牌。不是拍脑袋选的:
    `COMPONENT_THIN` 实测卡片 252,254,254(吃掉 94% 壁纸,就是用户看到的"白卡");
    `BACKGROUND_THIN` → 185,190,201,壁纸透得出来且文字对比度仍够。

★ 判定形状按"玻璃从例外变成默认"重写(判据 C 条)
  原来是"允许出现玻璃的位置"白名单,逐处登记。那个形状在"玻璃是少数例外"时成立,
  但用户要的是**全部玻璃化** ⇒ 白名单退化成"把每处抄一遍",且挡不住新写的裸枚举。
  改成**规则**:材质档次只能来自 `Theme.navMaterial` / `Theme.cardMaterial`
  (或 `BlurStyle.NONE`),页面里出现裸 `BlurStyle.XXX` 即红;
  辅助方法(`this.materialOf()`)体内也查,否则等于开了后门。两条变异都验证咬得住。

★ 判据 C 条自身的两个 bug(都被本次触发)
  · 嵌套判定**没比文件**:`c.at`/`o.at` 是各文件自己的字符下标,直接比大小
    于是判出"MainPage 内含 SettingsPage 的玻璃"这种物理上不可能的红。
    假红比漏报更坏 —— 会让人去改本来对的代码(本次差一点)。
  · 参数抽取用 `[^)]*`:`this.materialOf()` 里含一层 `()`,在内层括号处截断,
    把合法写法读成 `this.materialOf(` 判红。改成配对括号抽取。

★ 出现/消失动画:用户点名的三处硬弹
  这些挂在 `if (cond)` 上,条件一变整块出现/消失,原来一帧过渡都没有 ——
  而代码上"看着像挂了动画"(外层页面有 transition),所以最容易漏。
  · `MainPage` 账号选择器下拉 → `menuIn()`(从上往下落的浮层档)
  · `AdminUsersPage` 新建用户表单 → `paneRiseIn()`(就地展开档)
    ★ 过渡必须挂在**调用点的 Column**:`@Builder` 返回 void,链不上修饰符。
      第一版写进了 `CreateForm()` 内部,是新判据抓出来的。
  · `SettingsPage` 新增账号弹层 → `paneRiseIn()`(与回复/转发弹层同一挂法)
  新增判据「出现/消失的那类元素真的挂了过渡」:**枚举所有条件挂载的浮层/展开块**
  (不是数数 —— 数数挡不住"新增第四处又漏了"),变异验证过。

★ 顺手修的两处真嵌套玻璃
  `SettingsPage` 的账号列表与密钥列表都是"卡里面的一段列表",两层都铺材质 =
  WebUI 用 `.bg-white .bg-white { backdrop-filter: none }` 明确禁止的嵌套
  (alpha 相乘 0.82×0.82=0.97 把壁纸吃光)。去掉内层材质,玻璃只留一层。

★ 判据放宽一处(不是放水)
  `harmony-contacts` 那句写死 `pushUrl`,判的是"tryRestore 有没有跳转",
  与用哪个原语无关。修 LoginPage 返回栈时改成 `replaceUrl` 后它误报了;
  改成 `(pushUrl|replaceUrl)`,原语该用哪个由 `harmony-nav` 那条专门判。

判据:files=32 ran=32 checks=518 pass=518 fail=0 red=0;
baseline 7/7✓(第 6 次重算,两个文件逐个 `git diff --quiet HEAD` 取证为有意编辑)。
2026-09-20 07:37:16 +08:00
1da4e15a7b 跨端: 补齐「我的」页深色可读性(4 页 219 段全 ≥3:1)+ 判据自己漏报的那一类
上一条把通信/日历/联系扫干净了,但**「我的」页没扫到**
(扫描脚本按文案点不到它 —— 那是侧栏**底部的头像按钮**,不是导航项)。
补上后立刻又抓出问题,并把判据自身的**漏报形状**一并修了。

## 一、「我的」页两处真 bug

1. **主题分段按钮 `SettingsPage.ets:834`**:
   `.fontColor(... ? Theme.surface : Theme.textPrimary)` —— `surface` 是
   **会翻转的面色**,压在 `Theme.accent` 蓝底上,深色下变成近黑。
   设备读数:`深色 2.93:1  ink rgb(32,34,36) bg rgb(34,96,228)`。
2. **权限档徽标 `MailDetailPage.ets:391`**:同一个错法(第 13 处)。
3. **账号名 `SettingsPage.ets:1179`**:三元里的裸 `Theme.accent`
   压在 `accentSoftFor()` 底上 ⇒ `jianf 2.83:1`。
   `AdminUsersPage.ets:386` 同形状(角色徽标)。

## 二、判据自己的漏报形状(比 bug 本身更值得记)

静态防线(`C|会翻转的 Resource 不得当前景色`)**在 bug 存在时是绿的**。
原因是它的提取正则:

    /\.fontColor\(Theme\.([A-Za-z0-9_]+)\)/      ← 要求令牌是**唯一实参**

于是**三元里的令牌全被漏掉**:

    .fontColor(this.appearanceTheme === t ? Theme.surface : Theme.textPrimary)

改成"在 `fontColor(` 之后的整段实参里找所有 `Theme.X`"之后,
它**立刻报出上面第 1、2 两处**(此前一直绿)。

★ **判据漏报的常见形状是它自己的正则太窄,而不是被测代码太隐蔽。**
  这条写成注释留在判据里了。

## 三、我自己犯的批量替换错误(已加判据钉住)

把裸 `Theme.accent` 换成 `accentFor()` 时,**误把 6 处 `backgroundColor` 也换了**。
`accentFor()` 深色给浅蓝 `#80AFF9`,而搭档前景是白色 `accentFg`
⇒ 白字压浅蓝 ≈ **1.4:1**,主按钮文字会彻底看不见。

- 6 处已逐处还原(复核:`grep -c "backgroundColor(Theme.accentFor())"` = 0)。
- 新增判据 **`C2`**:`accentFor / dangerFor / approveFor / warnFgFor / textSubtleFor`
  **只能用于前景**,`backgroundColor(Theme.XFor(...))` 直接判红。
  (`accentSoftFor` 是例外 —— 它本来就是"面"。)
  ★ 修法不是"下次小心点":批量替换一定会再犯,**一行判据把它变成不可能**。

## 四、设备复扫(修后)

```
[通信]      扫 42 段,低对比 0
[日历]      扫 85 段,低对比 0
[联系人]    扫 51 段,低对比 0
[我的]      扫 41 段,低对比 0      ← 新增
```
**219 段文字全部 ≥3:1**(本轮全部针对深色)。

## 五、底本

`baseline.sha` 第 5 次重算。重算前**专门复核**了"那 6 处误改有没有残留"
(不只看 `git diff` 非空就放行):`backgroundColor(accentFor)` 计数为 0、
`git diff HEAD~1` 里 backgroundColor 只有 calendar 那一处(有意改动)。

`run-all.mjs` → `checks=515 pass=515 fail=0 skip=0 red=0 broken=0 unreported=0`。

**未验**:浅色主题下的对比度没扫(本轮全部针对深色)。
2026-09-19 20:43:59 +08:00
4ecddfc2b6 跨端: 语义色/三级文字在深色下没提亮(设备扫出 12 处)+ 判据取样法修到第三版
## 一、判据基建先修对,否则是在听噪声

深色可读性扫描的**取样法迭代了三版**,每版都因为**假红**才改的
(记在这里,因为"判据自己错"比"界面错"更费时间):

1. **取中心一个像素** ⇒ 落在笔画之间、读到的是底色 ⇒ 一片 ratio=1.00 假红。
2. **取全局最暗/最亮** ⇒ 会采到**两个不同的东西**:细字的抗锯齿中间色。
   实例:segmented「日」报 ink `rgb(32,34,35)` / bg `rgb(98,100,101)` 2.69:1 ——
   而截图里它要么蓝底白字、要么深底浅字,两种都远高于 3:1。
   **反复核对才确认是判据的错,不是界面的错。**
3. **步长采样 + 众数** ⇒ 仍然假红:周表头「一」报 2.03:1,
   而逐像素的真实墨是 `rgb(166,167,167)`(**6.6:1**)——
   稀疏网格**整个跳过了笔画**。这不是调参能修好的(字越小越糟)。
4. **逐像素 + 众数=底 + 离底最远者=墨**(最终版)。
   理由:一个文字框里**面积最大的一定是底**,而"离底色最远"就是笔画最实那部分。
   ⚠️ 逐像素是**负担得起的**(整屏已解码在内存,一个框最多 9 万像素)——
   **不要再为了省开销退回抽样**,前两版都因此假红。

## 二、真 bug 三批(都是"深色下没提亮")

### ① 语义色前景(12 处 → 修完设备复扫 0)
WebUI 的语义色也是**双通道**,`.dark` 段把 `red/green/amber-700` 换成浅色
(`248/118/249 → 164/219/195` 那组)。鸿蒙只有一个深色值:
- 「归档」按钮 `Theme.danger` #B91C1C 压深面 ⇒ **2.47:1**(联系人页 7 处)
- 「今天」/「日」等 ⇒ **2.38–2.77:1**

加 `dangerDark/approveDark/warnFgDark`(逐字对齐 WebUI `.dark`)
+ `dangerFor()/approveFor()/warnFgFor()` 入口,22 处接入。

### ② 日历工具条整块配色抄错(不是"深色档选错")
对照 WebUI `CalendarView.tsx:313-331`:

| 元素 | WebUI | 鸿蒙(改前) |
|---|---|---|
| 「今天」 | `border-gray-300` + `text-gray-700`(**中性**) | `accentSoft` 底 + `accentStrong` 字 |
| 月/周/日 未选中 | `bg-white` + `text-gray-700` | `accentSoft` 底 |

鸿蒙把**次要的中性按钮**画成了**品牌色按钮** —— 视觉重量跟主操作一样,
三个档看起来"都像选中"。已按 WebUI 改成中性。

### ③ 三级文字在深色下 2.82:1(**71 处**,全局最大的一个)
WebUI 的 `.dark` 段注释逐字写着这个坑:

> 「`gray-400/500`(次要文字)→ **提亮**。深底上的浅色 gray-400
>  只有约 2:1 对比度,远低于 WCAG AA 的 4.5:1 —— **看得见但读不动**。」

鸿蒙用的系统三级色 `ohos_id_color_text_tertiary` 深色下是 `rgb(102,103,105)`
压深面 **2.82–3.05:1**,而它被用在 10–11px 的**小字**上
(时间戳、农历、空态、辅助说明)。

**不换掉系统令牌**("系统拥有的维度"纪律照旧),加 `textSubtleFor()`:
深色下改用二级色(实测 `rgb(166,167,167)` ≈ 7.15:1,
且与 WebUI 的 `gray-500` 同档位)。71 处接入。

**设备复扫(修后)**:通信/日历/联系三页 **176 段文字全部 ≥3:1**。

## 三、欠账与底本

- `baseline.sha` 第 4 次重算。仍**先逐个取证**:
  `git diff --quiet HEAD -- <f> && echo STALE || echo INTENTIONAL`
  ⇒ 三个全是 `INTENTIONAL`(我改的),不是变异残留。
  把这条取证命令也写进文件,下次照抄。
- `dangerDark/approveDark/warnFgDark` 已登记进 `SELF_OWNED_COLORS`
  + `Theme.ets` 登记表(各带理由与 WebUI 出处)。

`run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。

**未验**:①「我的」页(底部头像按钮,扫描脚本没找到入口)没扫;
② 浅色下的对比度没扫(本轮全部针对深色)。
2026-09-19 20:27:09 +08:00
13b557a742 跨端: 品牌蓝深色下没提亮(24 处字/图标看不见)+ 补深色可读性设备判据
## 一、真 bug:WebUI 深色下把品牌蓝**提亮**了,这边没有

WebUI 的强调色是**双通道**(`tailwind.config.js` 的 `backgroundColor`
/`textColor` 覆盖 + `index.css` 两段定义):

| 通道 | 用途 | 浅色 | 深色 |
|---|---|---|---|
| `--s-blue-600` | **实心按钮底** | `37 99 235` | `37 99 235`(**同值**)|
| `--c-blue-600` | 内容/交互的**蓝字与图标** | `37 99 235` | **`128 175 249`** |

`index.css:353` 写了理由:主按钮底跟着变「会让主按钮在深色页面上
失去『这是主操作』的视觉重量」;而蓝字必须提亮,否则深底上读不动。

鸿蒙只有一个 `Theme.accent = '#2563EB'` ⇒ 24 处字/图标在深色下
对比度 **2.61:1**(设备实测:管理页返回箭头 `‹`),低于 WCAG 图形下限 3:1。

**取证方式**:在跑着的 WebUI 上用 CDP 读**计算样式**(不是读 CSS 源)——
浅色 `37 99 235` / 深色 `128 175 249`,实测确认。

## 二、修法:加前景专用的深色档 + 唯一入口

- `Theme.accentDark = '#80AFF9'`(= WebUI `.dark --c-blue-600`)
- `Theme.accentFor(dark?)` 作为**前景**唯一入口
- **当背景的 20 处保持 `Theme.accent` 不动**(跟 WebUI 的 `--s-*` 一致)

24 处 `.fontColor/.iconColor(Theme.accent)` → `Theme.accentFor()`。

## 三、顺手修掉「转述一层就会漏」这个结构问题

`accentSoftFor(isDark)` 原本的约定是"页面算好深浅色传进来"。
给 `accentFor` 做准备时一数:**8 处**直接用了 `Theme.accentSoft`(没走入口)
—— 约定**已经漏了**,而漏掉的症状正是上一轮那个"深色下白底卡片刺眼"。

于是把两个 `For()` 的参数都改成**可选**:`AppStorage` 是 ArkTS 全局键值存储,
静态类可以直接读(原来"拿不到 Context"的理由不成立)。
⇒ `Theme.isDarkNow()` 成为唯一判断点,调用方不必再各自转述。

## 四、设备判据:深色可读性**扫一屏**

原来只有悬浮球那一条(单个点)。这个 bug 类一天撞到**两批**(12 处 + 24 处),
逐处写判据追不上 ⇒ 改成把当前页所有小段文字都量一遍对比度。

三个实现要点(第一版全踩了,都写进注释):
- **不能只取中心一个像素**:中心多半落在笔画之间 ⇒ 读到的是底色,
  报出一片 ratio=1.00 的假红。改成**框内网格扫描取极值**(最亮=底/最暗=墨)。
- **整屏解码一次**:每点 spawn 一次 ffmpeg 太慢 ⇒ 新增
  `readPixels()`(157ms 解整屏,比逐点快三个数量级)。
- **只判"有真实墨迹"的框**(`hi.L - lo.L >= 0.02`),否则跳过而不是判红。

实测:修前 1 处低对比(2.61:1),修后 **40 段文字全部 ≥3:1**。

## 五、判据自身的三个修正

- `accentSoftFor(dark:)` 的签名断言跟着放宽成 `dark?`,并**补上 `accentFor` 的**。
- **孤儿令牌判据从"一跳"改成"走整条链"**:`KEY_IS_DARK` ← `isDarkNow()`
  ← `accentFor()` ← 24 处页面。只查一跳时它假红 ——
  ★ **"有没有人用"是可达性问题,不是邻接问题**;加中间层(抽 `For()` 入口)
  恰恰是我们鼓励的写法,而旧判据会因此假红。
- 手写色登记表补 `accentDark` 一行理由。

## 六、`baseline.sha` 重算(先核过不是残留)

三个文件哈希对不上。逐个 `git diff --quiet HEAD -- <f>` 取证:
- `AdminUsersPage.ets` / `SettingsPage.ets` —— 本次**有意编辑**;
- `api/AppearanceApi.ets` —— **与 HEAD 逐字节相同** ⇒ 底本取完后被**合法改过**
  (提交 `f811c98`),属 `stale` 不是 `residue`。

按该文件自己那条纪律(「重算必须是一次有记录的动作」)在文件里记了理由。

`run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。
2026-09-19 20:05:33 +08:00
e8b260dd70 维护: 底本重算(4 个文件漂移 = **底本过期**,不是变异残留)—— 逐个核实后按本文件的协议记一行"为什么"
pi `df62ad3d` 那封的 §四 我复核后**已经在 `d07494e` 修好了**(父提交正是 pi 报信时的
HEAD `93dabb4`):`if (r.red) reds.push(r.red)` + `DIAG[*].blocksGreen` 让
`summary.py` 的 status 真的进了 `verdict`。同刻 A/B(只切 baseline 一个变量)实测:
残留态红清单 diff **恰好 +1 行**(`(summary.py)baseline-residue ——…`),`red` 10→11;
stale 态 diff **0 行**。⇒ "stale ⇒ 提示(0) / residue ⇒ 红(进 verdict)" 这条口径成立。

这轮顺手核"每个文件的登记/实际读数"时,撞上底本自己的状态:
`baseline=3/7(底本过期…)`。逐文件核过 —— 4 个漂移**全部**是
`git diff --quiet HEAD -- <f>` 为空的**提交态**(各自有明确的提交):
  · AdminUsersPage.ets  ← 0e5eec6 (09-18 11:47)
  · BackgroundPicker.ets、SettingsPage.ets ← 36ef15a (09-19 12:30)
  · ApiClient.ets      ← bcd7e7f (09-18 12:55)
⇒ 是**底本过期**,不是残留(两者在 `sha256sum -c` 眼里一模一样,
所以"重算"必须是有记录的动作,否则会永久掩盖真残留)。

重算后:`sha256sum -c` 7/7 OK、套件 `diag=none baseline=7/7✓`(原 3/7)。
**并验证重算没有把真残留一起盖掉**:改脏一个在底本里的文件 ⇒ 仍报
`diag=baseline-residue baseline=6/7✗` + 那条红进 verdict(red 10→11)✓。

★ 记一条观察:这次漂移属 `baseline-stale`(rc=0、只提示)那一档 ——
它**不假红**(正常提交不会天天红),但也**不会被自动发现**:
我是靠主动逐文件核查撞上的,不是它自己报的。
"不假红"与"会被发现"在这个闸上仍有取舍,这条记为已知残留。
2026-09-19 13:01:39 +08:00
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 仍逐字节一致(上一轮的修复保持)。
2026-09-18 04:07:26 +08:00
c12744e8c3 跨端: 导航条材质选 (a) 固定档(推翻我的 (b))—— 并修掉"注释说 (a)、代码是 (b)"的自相矛盾
pi 2026-09-15 裁定:**推翻 (b),选 (a)**。我原先给 (b) 的理由不成立,他逐条驳了:

1. **WebUI 的导航条根本不读 `--bg-blur`**:`index.css:1114` 的 `.narrow-nav` 是硬编码
   `backdrop-filter: blur(18px) saturate(1.5)`。我引这条支持"两个量不同",
   而同一条也说明**它不由用户偏好驱动**。
2. **WebUI 那个滑杆的语义是"背景"**:`BackgroundPicker.tsx:183` —— `label="模糊"`、
   `hint="虚化细节,避免背景与正文抢注意力"`、`min=0 max=24`,只作用在
   `.app-backdrop{filter:blur(var(--bg-blur))}` 上。
3. WebUI 自己留了**分开的**令牌 `--bg-blur-panel`(`index.css:267`,注释写明
   "与壁纸自身的 `--bg-blur` 分开")—— 它的词汇表本身就拒绝把两者等同。
4. **★ 我给 (b) 的理由不成立**:我写"(a) 会让那个滑杆在导航条上变成死控件",
   可那个滑杆**已经**被壁纸消费了(`MainPage.ets` 壁纸层的 `.blur(bgPlan.blurPx)`)——
   它从来**不是**导航条的控件。(a) 之下它照样是活的。
5. §7.12 的「材质(玻璃)」行原本写的就是固定档 ⇒ (a) 是**回到**已登记契约。

产品向还有一条:**导航条是 chrome,材质应当稳定**,不该因为用户换张壁纸而变厚变薄。

## 最该记的是:我的注释和代码**互相矛盾**

`MainPage.ets` 里那段注释论证的是 (a)、并明确写着"跟随是错的,pi 抓出来了",
而它下面那一行代码是 (b)。**下一个读者会照注释把代码改回去,并引我那句话当权威。**
这是这一路反复在消的形状(说的与做的不一致、而判据看不见),这次落在**注释**上 ——
而注释正是"理由要写清"那条纪律的证据源。已整段重写为真实的 (a) 版本,并把
"(a) 会让滑杆变死控件"这个**错的理由**连同它为什么错一起留在注释里。

## 做掉的东西

- `MainPage.ets`:导航条回到 `.backgroundBlurStyle(Theme.navMaterial)`;
  删掉 `NAV_MATERIAL_OF` 表与 `navMaterialFor` 的 import((a) 之下都是孤儿)。
- `model/Appearance.ts`:删 `navMaterialFor`(它存在的唯一理由就是方案 (b))。
  `blurStyleFor` 现在**没有任何调用点** —— 如实登记在它的文档注释里
  ("有测试"不等于"有人用",上一轮我刚因同形状被抓过),不假装它活着。
- **五处"整条字面表达式"断言改成语义断言**(pi §四):`cross-client-theme`、`harmony-appearance`、
  `harmony-nav`(3 处)此前都在钉
  `/\.backgroundBlurStyle\(NAV_MATERIAL_OF\[navMaterialFor\(…\)\] \?\? Theme\.navMaterial\)/`
  —— 字面换字面,正是 `CRITERIA.md` 不许的"对源码形状的匹配"。
  现在判:① 那一处的材质**来自 `Theme.navMaterial` 这个系统令牌**;
  ② **不许跟随** `bg_blur`(NavBar 真代码里不许出现 `blurPx`/`blurStyleFor`);
  ③ 令牌是 `BlurStyle` 枚举值、不是 `NONE`、且**成员名真实存在于 SDK 枚举**。
- "可达性"那条判据**随契约作废**(它守的是方案 (b)):换成判 (a) 的契约。
  **判据随契约走,不随实现走。**
- `harmony-appearance`:原先判"页面里那张表的键必须是 SDK 成员"。表删了,
  改成**枚举 `blurStyleFor` 的整个值域**(0..40 + 界外 + NaN),逐个核 SDK 成员 ——
  比原来只核表里那三行**更严**。

## 判据自己先错了一次,记下来

新判据第一版**没剥注释**就断言"NavBar 里不许出现 `blurPx`",当场红了 ——
而红的原因不是代码错,是 `NavBar` 的**文档注释**里正好写着
"我一度把档位接过用户偏好(`navMaterialFor(this.bgPlan.blurPx)`)"这句历史说明。
**注释说明禁令 ≠ 违反禁令**;不剥注释,这条判据就会变成"逼人删掉解释",
恰好与本仓库"理由要写清"的纪律相反。改成 `stripComments(bar)` 后再判,并加了一条
"注释里确实留着那处说明"的自检前提。

## 变异体:45 个全部被抓(含 3 个新判据的专属变异)

方案 (b) 落地时配的 13 个变异体**整体作废**(它们锚的代码被删了),标注 `retired`
并写清理由 —— 不是"没跑成"。另有 5 个锚点漂移(我在注释里逐字引用了被锚的那句代码,
污染了通用正则)的**重锚**到真代码;注释里那句逐字引用也一并去掉了
(**注释里逐字抄代码**正是让"按字面锚定"的变异体反复失效的根因)。
新增 3 个针对 (a) 契约的变异体(绕过令牌 / 又跟随 `bg_blur` / 令牌变 `NONE`),全部被抓。

## 未验

- **真机观感仍未验**:三档材质在真机上能不能看出差别、滑杆手感、管理页布局,
  只有真机能答。本机模拟器已起(`hdc list targets` 有目标),但
  `run-all.mjs` 的**设备闸已经到期**(8 个静态判据的前提成立)⇒ 套件现在会挡在
  那道闸上、不打 `RESULT`。**这不是本笔引入的**(前提是环境变了),已单独报给 pi。
- Go 侧 `debt_registry_test.go` 仍未在本机跑(无 Go 模块缓存);pi 已在别处跑过,绿。
2026-09-15 11:58:19 +08:00
bcd4f97e37 跨端: 变异体计数收进仓库 —— 前面报过 40/48/58 四个数,根因是"job 集合"从没定义
pi 用 `/tmp/mut/` 复算后指出:48 也不对。他是对的,而且**不是记性问题、是口径问题**——
我把 9 个 `jobs*.json` 的条目**直接相加**,没做归一:同一个变异体在跨批重锚时被键了多次
(13 组重复、15 条冗余),最典型的是「计划不搬运模糊值」同时挂在 `jobs-b3`/`jobs-blur`/`jobs-blur2`
三个文件、三个不同 `test` 键上 —— 于是"按判据文件分组求和"必然把它算三次,
而"跑在新增判据上的是多少"在交叉归类下**没有唯一答案**(41 或 35)。

更根本的是 pi 指出的第二层:**那个统计脚本根本不在 `/tmp/mut/` 里**(他为了复算是现写的),
而且 `/tmp` 会被清、不在仓库里 ⇒ 变异体数字**只活在信里**。
这一路已经立过同形状的规则(余额打在 `RESULT` 行、权威源在文件里),这条当时漏了。

## 做了什么

- `client/electron/test/mutants/`:把 `mut.py`、`run.sh`、`jobs*.json`、`baseline.sha`
  从 `/tmp` 挪进仓库(`/tmp` 会清、复核方够不着)。
- `mutants/summary.py`:**口径的唯一权威**,定义写死在代码里:
  · 不同变异体 = 按 (file, pat, repl) 去重(`retired` 不计);
  · 跑起来 = 锚点在该文件里**恰好命中 1 次**(与 mut.py 同一条件);
  · `on_new_criteria` = 该变异体的**每一个** test 键都指向本批新增的两个判据文件
    (口径 A —— 不因交叉归类虚高;另报口径 B 作参考,它只增不减,不拿来报数)。
- `run-all.mjs` 的 `RESULT` 行播报它,并**顺带自证基线**:跑不起 `summary.py` 时
  **不静默**(打印 status 与 stderr 末行)——我第一版路径写错,只看到"计数未知",
  真因(`can't open file …/test/test/mutants/summary.py`)被吞掉了。
- `mutants/test-keys.json`:`test` 键 → 判据文件的**唯一来源**(`mut.py` 与 `summary.py`
  共用)。此前两处各写一份,分叉过一次:键名从旧名换成 API 名后 `summary.py` 那份没跟上,
  于是所有锚点被判 `hits=-1`、报出"51 个变异体全部 skipped"。
- 无歧义口径下的**当前真值**:`mutants=48 ran=48 skipped=0 on_new_criteria=36 baseline=7/7✓`
  (口径 B = 41;原始条目 66,其中 `retired` 5)。
- 清掉 pi 指出的三类脏数据:
  · **过期条目**(锚点是修复前的旧写法,`hits=0`)标 `retired` 5 条 ——
    它们**不是"没跑成的变异体"**,重锚后都跑过、都红了;留着只会把 skipped 一直抬高;
  · **重复计数**(multipart 那条在两个文件里各一次)去重;
  · **真 skip** 的 multipart 锚点切片成 `name: 'file',\n contentType: mimeType` ⇒ 真的跑起来了
    (此前命中 2 次,因为 `ApiClient.ets` 有两个 multipart 构造器)。
- 修两处并发/竞争:`mut.py` 的 `tempfile.mktemp()`(Py3 起 deprecated,**TOCTOU**)→ `mkstemp`;
  `run.sh` 的固定 `/tmp/mut/bak` → 按 `$$-$RANDOM` 唯一(并行跑会互相覆盖备份)。

## 未做(如实说)

- **`AdminUsersPage.ets` 有一处不是我做的改动留在工作树里**(11:20:48,我 11:21 的提交之后):
  `Chip(text, bg, fg: string)` → `ResourceColor`。核实过是**正确的 ArkTS 修法**
  (`Theme.surfaceMuted`/`textSubtle` 是 `Resource`、`chipNeutralBg` 是 `string`,
  第 368 行的三目因此是 `Resource | string` ⇒ 旧签名**编译不过**)。
  我**没有提交也没有回退**它 —— 工作树是共享的,不该替别人提交别人的活。
  基线因此重算了(`baseline.sha` 顶部记了原因与哈希来源,重算本身是**有意动作**:
  随手重算会把"某次变异没还原"永久掩盖掉)。
- Go 侧 `debt_registry_test.go` 仍未跑(沙箱无 Go 模块缓存),只做了 `gofmt`。
2026-09-15 11:23:41 +08:00