修复: 第四个落点 —— **变异条目的锚点失效**(hits=0):守具有齿,却不在位

pi `6aa2b17f` 报的第三例(`unlisted`/`ghosts` 退 0)**我复核后已经在 `6ee9902` 修好了**
—— 而它的**父提交正是 pi 报信时读的 `4b841e0`**(差 11 分钟)。又是同一形状的竞态。

但顺着同一条线**审计全部 61 个变异条目的锚点**,发现**还有一个同形状的落点,
而且它在真树上是活的**(不是构造的):

    hits=0  SettingsPage.ets 「管理入口不做门禁」(jobs-all.json)

根因:该锚点写的是 **6 空格**,而 `c523c21`(09-17 17:43)把该文件**重排成 4 空格**
⇒ 锚点从此命中 0 次(写进清单时 `bcd4f97`(09-15)它是**对的**)。

为什么没人发现:`summary.py` 把「hits=0 … 过期条目」**只打印**、**不进严重度链**
⇒ `rc=0`、`diag=none` ⇒ 套件 `whyLines: (note || status!==0) ? … : []` 为假 ⇒
**整段丢掉**。端到端实测(真树、干净工作树):套件输出里 grep「过期条目」= **0 次**。

★ 危害是"**一个变异守具被静默关掉**",不是"数字错了":`ran` 少 1、`skipped=1`
是个**中性数字**,读者看不出少了哪一个。而**手工施加那个变异仍能让判据红**
(`harmony-admin.test.mjs` `# fail 1`)⇒ **有齿,只是没挂上**。

修法(沿用 `summary.py` 的**唯一严重度链**):
  · 新增 `mutant-anchor-stale` ⇒ **1 档**(清单/数据该改;**不是** 2 档 ——
    照 `env-defaults.sh:25` 的反方向:别让"清单没跟上"冒充环境)。
  · 修锚点 6→4 空格 ⇒ `ran` 47→48、`skipped` 1→0,守具重新挂上(实测变异能红)。
  · `whyLines` 过滤器加 `^\s*hits=`:只说"有锚点过期"不够,**点名的才是可行动的**。
  · 两张表都登记(`UPSTREAM_RC` + `DIAG`,`blocksGreen: true`)+ 真跑案例
    (迷你仓库里放一个不含锚点的同名文件 ⇒ `hits=0`)+ 读者侧具名案例。

★ 射程如实标出:判据只看 **`hits == 0`**,**不看 `hits == -1`**(文件打不开是另一回事,
且迷你夹具里目标文件本来就不在 ⇒ 算进来会造假红)。**`-1` 那一半无判据守着**(真树 0 条)。

变异验证(4 个方向全抓):
  ① 新档关闭(回到"只打印")⇒ exitcode-selftest rc=1、2 条红 ✓
  ② 从 UPSTREAM_RC 删掉新码 ⇒ 反向覆盖点名 ✓
  ③ blocksGreen true→false ⇒ 双向口径漂移红 ✓
  ④ 放宽成任何 skipped_detail(含 hits=-1)⇒ rc=1、**5 条红**(夹具假红)
     ⇒ **`h == 0` 这个射程是承重的** ✓

端到端 A/B(隔离 worktree,同刻对照):
  A 锚点已修  ⇒ rc=0、diag=none、ran=48/skipped=0、"锚点已失效" grep **0**
  B 锚点退 6 格 ⇒ rc=1、diag=mutant-anchor-stale、ran=47/skipped=1、grep **2**

★ 通用规则(本仓第 4 次同一形状):**"跑了多少个"与"该跑多少个"之间也要有判据。**
被跳过时 `ran` 只少 1、`skipped` 只多 1 —— 都是中性数字,而"少了哪一个"没有通道。
凡"登记一批东西、再逐个挂上"的结构(变异条目、判据、样本表)都要问:
**挂不上的那一个,谁来说?**

自检 5 个各 rc=0;全套 checks=516 pass=511 fail=5 red=5 verdict=red;
mutants=48 ran=48 skipped=0 on_new_criteria=36 diag=none baseline=7/7✓。
CRITERIA.md §16.1.1 记这一笔(含 4 个变异与 A/B 表)。
This commit is contained in:
2026-09-20 04:49:50 +08:00
parent c1465e09ab
commit efb1c1d2ca
4 changed files with 165 additions and 4 deletions

View File

@ -900,6 +900,75 @@ Go 在同一包内**按源文件顺序**跑测试 ⇒ 登记那条先跑就读
| "未列入清单" 在套件输出 | grep **0** | grep **1** |
| `(summary.py)manifest-mismatch` 红 | **0** | **1** |
#### 16.1.1 ★★ 第四个落点:**变异条目的锚点失效**(`hits=0`)—— 守具有齿,却不在位(dsh 2026-09-19)
pi 报的第 1–3 例都修好之后,我顺着同一条线**审计全部 61 个变异条目的锚点**,
发现**还有一个同形状的落点**,而且它在**真树上是活的**(不是构造的):
```
活跃条目 61 个 · 锚点命中 ≠ 1 的:**1 个**
hits=0 client/harmony/.../pages/SettingsPage.ets 「管理入口不做门禁」(jobs-all.json)
```
**根因**:该锚点写的是 **6 空格**(` if (this.isAdmin) {`),
而 `f4b8bc1`(09-17 17:43,"邮件详情与「我的」页 1:1 对齐 WebUI")把该文件**重排成 4 空格**
⇒ 锚点从此命中 **0** 次。写进清单时(`e2f117f`,09-15)它是**对的**(当时 6 空格命中 1 次)。
**为什么没人发现**:`summary.py` 把「hits=0 … 过期条目」**只打印**,
**完全不进严重度链** ⇒ `rc=0`、`diag=none` ⇒ 套件那边
`whyLines: (note || status !== 0) ? … : []` 为假 ⇒ **整段丢掉**。
端到端实测(真树、干净工作树):
| | 修前(锚点 6 空格) | 修后(锚点 4 空格) |
|---|---|---|
| `mutants= / ran= / skipped=` | `48 / 47 / 1` | `48 / **48** / **0**` |
| `diag=` | `none` | `none`(无此码,因为已修) |
| "过期条目"在套件输出 | grep **0** | — |
★ **危害是"一个变异守具被静默关掉",不是"数字错了"**:
`ran` 少 1、`skipped=1` 是个**中性数字**,读者看不出少了哪一个、也不知道少了。
而**手工施加那个变异仍能让判据红**(`harmony-admin.test.mjs` `# fail 1`,
`★ 管理入口没有 isAdmin 门禁 ⇒ 每个普通用户都会看到一个点进去 403 的入口`)
⇒ **守具有齿,只是不再被挂上**。
**修法**(与第 3 例同一条链,`summary.py` 的**唯一严重度链**):
新增 `mutant-anchor-stale` ⇒ **1 档**(清单/数据该改),
不是 2 档(那才是环境问题)——照 `env-defaults.sh:25` 的**反方向**:别让"清单没跟上"冒充环境。
★ **射程如实标出**:判据只看 **`hits == 0`**,**不看 `hits == -1`**。
`-1` 是"目标文件打不开"(文件没了/权限),与"锚点写法过期"是两回事,
且 `--exitcode-selftest` 的迷你夹具里 `jobs-one.json` 指向的文件**本来就不在**
(夹具设计,不是缺陷)⇒ 把 `-1` 算进来会造**假红**。
**`-1` 那一半目前没有判据守着**(真树实测 0 条)。
**端到端 A/B**(dsh,隔离 worktree,同刻对照):
| | A:锚点已修(4 空格) | B:锚点退回 6 空格(制造 hits=0) |
|---|---|---|
| `summary.py` rc | `0` | **`1`** |
| `diag=` | `none` | **`mutant-anchor-stale`** |
| `ran= / skipped=` | `48 / 0` | `47 / 1` |
| "锚点已失效"在套件输出 | grep **0** | grep **2** |
**变异验证**(dsh,4 个方向全部抓住):
| 变异 | 结果 |
|---|---|
| 新档关闭(`elif False`)=回到旧的"只打印" | `exitcode-selftest` rc=1、2 条红(`rc=0(期望 rc=1`)✓ |
| `UPSTREAM_RC` 删掉新码 | 反向覆盖点名 + 案例不符 ✓ |
| `blocksGreen: true` → `false` | `两侧口径漂移(双向比对 UPSTREAM_RC ↔ blocksGreen)` ✓ |
| 判定放宽成任何 `skipped_detail`(含 `hits=-1`) | rc=1、**5 条红**(迷你夹具假红)⇒ **`h == 0` 这个射程是承重的** ✓ |
★★ **通用规则(本仓第 4 次同一形状,值得单列)**:
> **"跑了多少个"与"该跑多少个"之间,也要有一条判据。**
> 一个变异/检查被**跳过**时,`ran` 只少 1、`skipped` 只多 1 —— 两个**中性数字**,
> 而"少了哪一个、为什么少"没有任何通道。
> ⇒ 凡有"登记了一批东西、再逐个挂上"的结构(变异条目、判据、样本表),
> 都要问一句:**挂不上的那一个,谁来说?**
> 同族的第三个自查问(pi 归纳):"守着它的判据,它自己的样本由谁守";
> 这一条是它的**镜像**:"**被守的那个东西没挂上时,谁来说**"。
### 16.2 ★★ **同一优先级写了两遍 ⇒ 两个顺序**:不变式只在**对角线**上被验过(pi + dsh 2026-09-18)
§16.1 讲"结论到不了决定颜色的那一格"。这一节是它的**镜像**:

View File

@ -141,8 +141,8 @@
},
{
"file": "client/harmony/entry/src/main/ets/pages/SettingsPage.ets",
"pat": " if \\(this\\.isAdmin\\) \\{",
"repl": " if (true) {",
"pat": " if \\(this\\.isAdmin\\) \\{",
"repl": " if (true) {",
"test": "admin",
"why": "管理入口不做门禁"
},

View File

@ -335,7 +335,7 @@ def main():
#
# 顺序按**严重度递减**:
# 2 档(**没读数**,连是环境还是代码都判不了):blind / unreadable / baseline-unrunnable|unknown
# 1 档(**数据/清单该修**):unlisted / ghosts / baseline-residue|deleted
# 1 档(**数据/清单该修**):unlisted / ghosts / mutant-anchor-stale / baseline-residue|deleted
# 0 档:其余(`baseline-stale`/`baseline-absent` 仍带码,但**不必红**)
if blind:
severity = ('counts-unusable', 2)
@ -347,6 +347,34 @@ def main():
severity = ('manifest-mismatch', 1)
elif bl_diag in ('baseline-residue', 'baseline-deleted'):
severity = (bl_diag, 1)
# ★★ 第五类(dsh 2026-09-19,**同一形状的第四个落点**):**变异条目的锚点失效**。
#
# 这一格原来**只打印**(上面那段 `hits=0 … 过期条目`)、**完全不进严重度链**
# ⇒ `rc=0`、`diag=none` ⇒ 套件那边 `whyLines: (note || status!==0) ? … : []`
# 把它整段丢掉。端到端实测(真树、干净工作树):
# `summary.py` 说「hits=0 … 过期条目」 · 套件输出里 grep **0 次** · `rc=0`
# ⇒ 后果不是"数字错",是**一个变异守具被静默关掉**:
# 实测那一条(`SettingsPage.ets` 的「管理入口不做门禁」)锚点写成 6 空格,
# 而 `f4b8bc1`(09-17 17:43)把该文件重排成 4 空格 ⇒ 锚点从此命中 **0** 次、
# `ran` 少 1(47 而非 48)、`skipped=1`、**没有任何东西变红**;而手工施加那个
# 变异**仍能让判据红**(`harmony-admin.test.mjs` `# fail 1`)
# —— 也就是说**守具有齿,只是不再被挂上**。
#
# 归 **1** 档(不是 2):锚点过期是**清单/数据该改**(去改 `jobs/jobs-*.json`
# 里的锚点,或标 `retired`),与 `manifest-mismatch` 同类;
# 与"权限不够读不到"(2 档)**不是一回事** —— 照 `env-defaults.sh:25` 的
# 反方向:**别让"清单没跟上"冒充环境**。
# ★ 放在 `residue`/`deleted` **之后**:同为 1 档时,让最危险的那两个码优先报出来
# (锚点过期只是"守具没挂上",残留是"守具正在被绕过")。
# ★ 判据只看 **`hits == 0`**(旧的锚点写法漂移了 ⇒ 去改锚点),**不看 `hits == -1`**:
# `-1` 是"目标文件打不开"(文件没了/权限),与"锚点写法过期"是两回事,
# 而且 `--exitcode-selftest` 的迷你夹具里 `jobs-one.json` 指向的文件**本来就不在**
# (那是夹具的设计,不是仓库的缺陷)⇒ 把 `-1` 也算进来会造出**假红**。
# ⚠️ 如实标出射程:**`-1` 那一半没有判据守着**(真树实测 0 条,故暂不覆盖)。
elif any(h == 0 for _, _, h in skipped_detail):
severity = ('mutant-anchor-stale', 1)
elif bl_diag in ('baseline-residue', 'baseline-deleted'):
severity = (bl_diag, 1)
elif bl_diag != 'none':
severity = (bl_diag, 0)
else:

View File

@ -902,6 +902,7 @@ const UPSTREAM_RC = {
'counts-unusable': 2,
'manifest-mismatch': 1,
'baseline-residue': 1,
'mutant-anchor-stale': 1,
'baseline-stale': 0,
'baseline-unrunnable': 2,
'baseline-unknown': 2,
@ -1050,6 +1051,18 @@ function exitcodeSelfTest() {
rmSync(join(d, 'baseline.sha'));
}
}
/*
* `anchorstale`:**变异条目的锚点失效**(`hits == 0`)——
* 标准夹具 `jobs-one.json` 指向 `client/harmony/.../api/ApiClient.ets`。
* 在迷你仓库里建一个**同名但不含那个锚点**的文件 ⇒ 锚点命中 **0** 次
* (`hits=-1`(文件不存在)是**另一回事**,见 `summary.py` 里那句射程说明)
* ⇒ `diag=mutant-anchor-stale`、`rc=1`:**守具没挂上必须显形**。
*/
if (mode === 'anchorstale') {
const anchorAbs = join(root, 'client/harmony/entry/src/main/ets/api/ApiClient.ets');
mkdirSync(dirname(anchorAbs), { recursive: true });
writeFileSync(anchorAbs, '// 故意不含锚点:模拟"锚点写法漂移后命中 0 次"\n');
}
if (mode === 'blind' || mode === 'unreadable') {
// 整棵给 nobody 可达(mkdtemp 默认 700),最后一层按场景收紧
spawnSync('chmod', ['755', root]);
@ -1177,6 +1190,14 @@ function exitcodeSelfTest() {
*/
['组合:未列入清单 + 跑不了 sha256sum ⇒ diag=baseline-unrunnable(2 档优先)且 rc=2',
['jobs-one.json'], ['jobs-one.json', 'jobs-extra.json'], 2, 'diag=baseline-unrunnable', 'no-sha256sum'],
/*
* ★★ 第五类(dsh 2026-09-19):**变异条目的锚点失效**(`hits == 0`)。
* 这条就是它的端到端锚点 —— 也是"反向覆盖"要求的那条真跑案例
* (加新码若没有案例真跑过,`--exitcode-selftest` 会当场点名)。
* 期望 `rc=1`(数据/清单该改),**不是 2**(那不是环境问题)。
*/
['变异条目锚点失效(hits=0)⇒ rc=1 且 diag=mutant-anchor-stale',
['jobs-one.json'], ['jobs-one.json'], 1, 'diag=mutant-anchor-stale', 'anchorstale'],
];
let bad = 0;
const seenDiags = []; // 反向覆盖用:本组案例**真跑出来**的码(不是声明的)
@ -2019,6 +2040,28 @@ const DIAG = {
note: '底本过期(文件与 HEAD 逐字节相同 ⇒ 是正常提交改过,不是残留)',
blocksGreen: false, // 不是缺陷;退非零会让"正常提交"天天假红(pi 与我 §七 同意)
},
/*
* ★★ 第五类(dsh 2026-09-19):**变异条目的锚点失效**(`hits == 0`)。
*
* 这一格原来只出现在 `summary.py` 的 stdout 里,**不进严重度链** ⇒ `rc=0`、
* `diag=none` ⇒ 套件那边 `whyLines` 的转印条件 `(note || status!==0)` 为假
* ⇒ **整段丢掉**。端到端实测(真树、干净工作树):`summary.py` 说「hits=0 …
* 过期条目」· 套件输出里 grep **0 次** · `rc=0`。
*
* 后果是**一个变异守具被静默关掉**:真树那一条(`SettingsPage.ets` 的
* 「管理入口不做门禁」)锚点是 6 空格,而 `f4b8bc1` 把该文件重排成 4 空格
* ⇒ 命中 0 次、`ran` 少 1、**没有任何东西红**;但手工施加那个变异**仍能让
* `harmony-admin.test.mjs` 红** —— 有齿,只是没挂上。
*
* `blocksGreen: true`:锚点过期是**清单该改**(去改锚点或标 `retired`),
* 与 `manifest-mismatch`(1 档)同类 —— **不是**环境问题(那才是 2 档)。
*/
'mutant-anchor-stale': {
checked: true,
note: '**注意:有变异条目的锚点已失效(`hits=0`)** —— 那个变异**根本没被挂上**'
+ '(守具有齿但不在位)⇒ 请改锚点或标 `retired`;`ran` 比登记的少',
blocksGreen: true,
},
'baseline-unknown': {
checked: false,
note: '**环境:判不了 baseline 那一格**(git 答不了 ⇒ 判不出是残留还是过期)—— '
@ -2071,7 +2114,12 @@ function summarizeMutants(stdout, status, stderr) {
*/
const whyLines = (stdout || '').split('\n')
.filter(l => !/^RESULT /.test(l.trim()))
.filter(l => /✗✗|⚠️|★ 清单与磁盘不一致|在但读不到|未列入清单|清单里有、磁盘上没有/.test(l))
/*
* ★ 加 `hits=` 这一支(dsh 2026-09-19):只说"有锚点过期"不够 ——
* 读者要知道**是哪一条**(哪个文件、哪句 why)才能去改。
* 与 `未列入清单`/`清单里有、磁盘上没有` 同形状:**点名的才是可行动的**。
*/
.filter(l => /✗✗|⚠️|★ 清单与磁盘不一致|在但读不到|未列入清单|清单里有、磁盘上没有|^\s*hits=/.test(l))
.map(l => l.trim());
/*
* 上游加了新码而这里不认识 ⇒ **报警,并且红**(不是静静当成"没问题")。
@ -2472,6 +2520,15 @@ function mutantsLineSelfTest() {
+ ' ★ 清单与磁盘不一致 —— 上面的数字**不代表"磁盘上现在有多少个变异体"**:\n'
+ ' 未列入清单:jobs-UNLISTED-probe.json(新加的 job 文件必须显式加进 jobs.manifest.json)\n'
+ ' 清单里有、磁盘上没有:jobs-ghost.json\n';
/*
* ⑤(dsh 2026-09-19)**变异条目锚点失效**:`hits=0` ⇒ 那个变异根本没被挂上。
* `whyLines` 的过滤器里 `hits=0` 那两行含 `⚠️` ⇒ 会被收进来;
* 关键是**转印条件必须为真**(`diag=mutant-anchor-stale` ⇒ `blocksGreen` ⇒ 红 ⇒ 转印)。
*/
const anchorstale = 'RESULT mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=mutant-anchor-stale\n'
+ '跳过(锚点命中≠1 ⇒ 跑不起来):\n'
+ ' hits=0 client/harmony/entry/src/main/ets/pages/SettingsPage.ets 「管理入口不做门禁」 (1 条条目:jobs-all.json)\n'
+ ' ⚠️ hits=0 通常是**过期条目**(锚点是旧写法)—— 请标 retired 或删除,\n';
/*
* `baseline-stale`:底本过期(与 HEAD 逐字节相同)⇒ **退 0**(不是缺陷),
* 但那一格必须能被读者看见。注意它在 `RESULT` 行里有 `baseline=` 段落。
@ -2498,6 +2555,13 @@ function mutantsLineSelfTest() {
*/
['清单与磁盘不一致(status 1,正则匹配)', unlisted, 1,
{ line: '不代表磁盘上现在有多少个变异体', why: 3 }],
/*
* ⑤(dsh 2026-09-19)**变异条目锚点失效**:`status=1`、正则匹配。
* 这一格是"守具没挂上"—— 必须把「锚点已失效」转印给读者,
* 否则 `ran` 少 1 而读者只看得到 `mutants=… skipped=1` 这个**中性数字**。
*/
['变异条目锚点失效(status 1,正则匹配)', anchorstale, 1,
{ line: '锚点已失效', why: 2 }],
// ④ 异常退出码但正则匹配(既不是 0/1/2)⇒ 也要留痕,不许静默
['未知退出码', normal, 3, { line: 'status=3', why: 0 }],
// ⑤ 完全没打出 RESULT(如脚本不存在)⇒ 报 status 与 stderr 末行