修复: 我上一笔升级留下的**更糟的灰区** —— 跳过要**具名**、"移出 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` **同形状**(都先跑套件再判定),
  不是我引入的新形状。
This commit is contained in:
2026-09-18 05:05:38 +08:00
parent 4f386199cf
commit 0822993e76
3 changed files with 211 additions and 1 deletions

View File

@ -104,6 +104,51 @@ export function busyStreak(criterion) {
/** 供报文用:本机繁忙上限 K。 */
export const busyLimit = () => busyLimitOf();
/*
* ─── "本机真的跑过行为层"的记录(pi 2026-09-18 闸 (ii) 的证据)───
*
* 问题:把一条判据从 `STATIC_ONLY` **移除**(7→6)现在是个纯**记账动作** ——
* 它只是从"到期闸点名"挪到"行为条管辖",而**没有任何东西要求行为条真的执行过**。
* ⇒ 一条判据可以在"已升级"的名义下**永远不跑**。
*
* pi 用变异证明了后果:设备不在时注入一条真实回归,红清单与基线**逐条一致**(零痕迹)。
* 于是 `static=6` 这个余额读起来像"又清了一条",实际可能是"又一条进了永久 skip"。
* 与上面那条同源:**余额里读不出来的东西等于不存在**。
*
* ⇒ 行为条**真绿一次**就在这里留一条记录;`run-all` 拿它当"结算(移出 STATIC_ONLY)"
* 的前提。记录是**本机的**(`.tmp/`,已 gitignore)—— 判的是"**这台机器上**验过",
* 换机器不继承,这正是对的:换一台没设备的机器,"验过"不该跟着走。
*/
const ranPath = () => process.env.AGENTMAIL_BEHAVIORAL_RAN
|| join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..', '.tmp', 'harmony-behavioral-ran.json');
function readRan() {
try {
const d = JSON.parse(prose(ranPath()));
return (d && typeof d === 'object') ? d : {};
} catch {
return {};
}
}
/**
* 行为条**真的跑绿了**(断言全过)⇒ 记一笔。
* ⚠️ 只在**断言之后**调用:红了也记,就等于"把没验过的当成验过了",比不记更糟。
*/
export function noteBehavioralRan(criterion) {
const d = readRan();
d[criterion] = new Date().toISOString();
try {
mkdirSync(dirname(ranPath()), { recursive: true });
writeFileSync(ranPath(), JSON.stringify(d, null, 2));
} catch { /* 记不下不该让判据崩;run-all 那边把"没记录"当未升级(保守方向) */ }
}
/** 本机有没有"这条行为层真跑过"的记录?返回时间戳或 null。 */
export function behavioralRanAt(criterion) {
return readRan()[criterion] || null;
}
/**
* 找到能跑的 hdc 二进制。候选顺序:SDK 文档安装根下的 hdc,再 PATH 上的 hdc。
* 返回**跑通了 `list targets`** 的那条(status 0);都不行返回 null。