From 72f35ff9cde664211a29501515b93e228c1ad4f9 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 18 Sep 2026 08:31:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E6=9D=A1=E6=95=B0**?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E4=BC=AA=E9=80=A0**=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E6=89=93=E6=BB=A1=2013=20=E6=9D=A1=E7=A9=BA=20`ok`=20=E5=B0=B1?= =?UTF-8?q?=E7=BB=95=E8=BF=87=E4=BA=86=E5=B1=82=206=20=E7=9A=84=E4=B8=8B?= =?UTF-8?q?=E7=95=8C=EF=BC=88=E5=B1=82=207=EF=BC=9A=E9=94=9A=E7=82=B9?= =?UTF-8?q?=E4=B8=8D=E8=83=BD=E8=90=BD=E5=9C=A8=E8=87=AA=E8=BF=B0=E4=B8=8A?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`。 --- client/electron/test/CRITERIA.md | 48 ++++++++++++++++ client/electron/test/run-all.mjs | 98 ++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index 83db880..f28b2fc 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -213,6 +213,54 @@ const ALLOW = [ /* { name, replacement, why } */ ]; ★★ 而它们共同的教训是:**每根轴的"底"看起来都像整体的底。** "到底了"永远只对**当前那一根轴**成立 —— 这条我等了三轮才学会,写在这里给下一个人。 +### 6.1.1 条数下界**也**是自述 —— 必须再补一层**因果**判据(pi 2026-09-18,层 7) + +上面那条下界判据(层 6)的输入是**输出行数**:**数的是它"打了几条",不是它"验了几条"。** +所以把函数体换成"循环打满 13 条空 `ok`、`return 0`"就**恰好绕过**它: + +``` +基线: red=9 … verdict=red +假 ok×13: red=9 … 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 ⇒ 绿。 +⇒ 这是"自检真的读了那个符号"的**行为**证据,不是它的自述。 +``` + +实现要点(`run-all.mjs` 的 `CAUSAL` 表 + 快入口): + +- 快入口 `--only-selftest=<名>`:实测 **52~696ms/条** + (对比 `--X-selftest` 要跑整套 suite 的 4.3~4.9s); +- 隔离用 `mkdtempSync` + `cpSync(join(HERE), …)`(**只拷 `test/`,1.6MB / 6ms**)—— + 照本仓"隔离要用**最小**夹具"那条; + ⚠️ 但**必须**拷整个 `test/`:run-all 的**自检 1/2** 要 `readdirSync(test)` 与 `SUITE` 对齐, + 缺文件会让它在跑自检**之前**就 `exit(1)` ⇒ **读到的是假红** + (我第一次少拷东西时就这么读错过:`rc=1` 看着像"改坏生效",其实是清单自检停了整套); +- 关系表(自检 ↔ 它守的符号)也是手写数据,**但它被真跑锚住了**: + 表里每一对都必须实测"改坏了它真会红";把符号写错(写成它不读的)⇒ 那条自检改坏后**不红** ⇒ 红。 + ⇒ 表自己也被因果判据守着,这是它与"下界数字"的**关键区别**。 +- ⚠️ 锚文本的唯一性必须在**除去 `CAUSAL` 表本身**的源码里数, + 否则会**匹配到表里那一行字面量**(我第一版就踩了:probe/verdict 立刻报"不唯一", + 真因是表自己,不是生产里有两处)。★ 又是那句:"提到"与"就是"在文本上长得一样。 + +★ 层级表(**每层都是一根轴**):写法(4) → 存在性(5,"它还在吗") → 行为(6,"它做了吗") +→ **自述(7,"它说的话可信吗")**。 + ## 6.5 涉及**运行时行为**的结论,必须实测过才能写进规范/判据(pi 2026-09-14) 同一个错在本仓犯了两次,方向相反但错法相同:**从观察推断机制、没跑**。 diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 9262c56..14172ad 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -304,7 +304,9 @@ const brokens = []; const phaseWillRun = SUITE.map(([f]) => f) .filter(f => !(PHASE === 'install' && sideOf(f) === SIDE.SOURCE)); const skippedByPhase = []; +const ONLY_SELFTEST = (process.argv.find(a => a.startsWith('--only-selftest=')) || '').split('=')[1] || ''; for (const [file, flags, expected] of SUITE) { + if (ONLY_SELFTEST) { skippedByPhase.push(file); continue; } if (PHASE === 'install' && sideOf(file) === SIDE.SOURCE) { skippedByPhase.push(file); continue; @@ -2292,6 +2294,14 @@ if (process.argv.includes('--mutants-line-selftest')) process.exit(mutantsLineSe * ⇒ 任何"调用自检"的代码都必须排在**所有** `const` 表与自检函数定义之后。 * (所以这里紧贴生产区段入口:`let mutantsLine` 之前。) */ +if (ONLY_SELFTEST) { + const T = { 'probe-selftest': probeSelfTest, 'exitcode-selftest': exitcodeSelfTest, + 'skip-selftest': skipSelfTest, 'verdict-selftest': verdictSelfTest, + 'mutants-line-selftest': mutantsLineSelfTest }; + const fn = T[ONLY_SELFTEST]; + if (!fn) { console.error(`✗ 未知自检 ${ONLY_SELFTEST}`); process.exit(2); } + process.exit(fn() > 0 ? 1 : 0); +} let selfCheckRuns = 0; { const SELFTESTS = [ @@ -2623,6 +2633,94 @@ let selfCheckRuns = 0; + ' —— **存在性不等于在做事情**'); } } + /* + * ★★★ **因果锚点**:把被守的**生产符号**真的改坏,那条自检**必须红** + * (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` + * 与基线**逐字相同**、下界相关 RED **0 条**(条数 13 ≥ 下界 12 ⇒ 相等,无从对比)。 + * 最尖形式(假 ok×13 + `UPSTREAM_RC['baseline-residue']=99`)⇒ 0 条 RED、0 次提及 UPSTREAM; + * **对照**(不掏空、只改值)⇒ `red=9→10` 且真报"与真跑出来的 rc=1 不符"。 + * + * ⇒ 根因一句话:**下界判据的输入是"自我报告"** —— 判据自己说它做了多少,而它可以撒谎。 + * 这与这条线是同一句:**锚点不能落在"被守对象的自述"上,必须落在它管不着的东西上。** + * + * ★ 我**没有**采用 pi 提的三种替代(黄金输出 / 扫源码关键词 / 毒化环境变量): + * · 黄金输出 = 又一份手写数据(刚被证过那族); + * · 扫源码关键词 = "提到 vs 就是"(本文件反复证过它脆); + * · 毒化模式 = 把"必须报红"的断言写在**自检函数体内** ⇒ **假体不打它**。 + * 我实测过:模拟毒化后假体 `red=9`、下界红 0 —— 与层 7 同一问题(仍是自述)。 + * ⇒ 三种都仍在"被守对象自己说了算"这一侧。所以锚点改成**外部因果**: + * + * **改坏生产符号 → 那条自检必须红。** + * · 假体(空 `ok`)在符号被改坏时**仍然打 ok** ⇒ 它不红 ⇒ **红**(判据抓它); + * · 真自检读了那个符号 ⇒ 符号坏 ⇒ 它报 RED ⇒ 绿。 + * ⇒ 这是"**自检真的读了那个符号**"的**行为**证据,不是它的自述。 + * + * ★ 成本:实测单条自检的**快入口** `--only-selftest=<名>` 是 **52~696ms** + * (对比 `--X-selftest` 要跑整套 suite 的 4.3~4.9s),`cp -a test` 是 6ms。 + * 隔离用 `mkdtempSync` + `cpSync`(**只拷 `test/`,1.6MB**)—— + * 照本仓那条"隔离要用最小夹具"(整棵复制会换掉判据的输入)。 + * ⚠️ 但这里**必须**拷 `test/` 整份:run-all.mjs 的**自检 1/2** 要 `readdirSync(test)` + * 与 `SUITE` 对齐,缺文件会让它在跑自检**之前**就 exit(1) ⇒ 读到的是假红。 + * (我第一次少拷东西时就这么读错过:rc=1 看着像"改坏生效",其实是清单自检停了整套。) + * + * ★ 关系表(自检 ↔ 它守的生产符号)也是手写数据 —— **但它现在被真跑锚住了**: + * 表里每一对都必须实测"改坏了它真会红";写错了(比如把符号写成它不读的) + * ⇒ 那一条自检在改坏后**不会红** ⇒ **红**。⇒ 表自己也被因果判据守着。 + */ + const CAUSAL = [ + ['probe-selftest', "const probeIsDue = v => v === true || v === 'unknown';", + 'const probeIsDue = v => v === true;'], + ['skip-selftest', 'return [...(out || \'\').matchAll(/^(?:ok|not ok) \\d+ - (.+?) # SKIP(.*)$/gm)]', + 'return [...(out || \'\').matchAll(/^(?:ok|not ok) \\d+ - (.+?) # SKIPPED(.*)$/gm)]'], + ['verdict-selftest', "? 'green' : 'red';", "? 'red' : 'red';"], + /* + * ★ 这一条是 pi 点名的**最尖形式**:`UPSTREAM_RC` 是"真脚本真跑"那个锚点的表。 + * 把 `baseline-residue` 的值改坏 ⇒ `--exitcode-selftest` **必须红** + * (它真跑 `summary.py` 拿 rc 来比)。假体不读表 ⇒ 仍然全绿 ⇒ 被这条抓住。 + */ + ['exitcode-selftest', " 'baseline-residue': 1,", " 'baseline-residue': 99,"], + ]; + for (const [name, from, to] of CAUSAL) { + const tmp = mkdtempSync(join(tmpdir(), 'am-causal-')); + try { + cpSync(join(HERE), join(tmp, 'test'), { recursive: true }); + const target = join(tmp, 'test', 'run-all.mjs'); + const src = prose(target); + /* + * ⚠️ 必须在**除去这张因果表本身**的源码里数唯一性 —— + * 否则锚文本会**匹配到它自己那一行**(我第一版就踩了:probe/verdict 两条 + * 立刻报"锚文本不唯一",而真因是**表里的字面量**,不是生产里有两处)。 + * ★ 又是本文件那句:"提到"与"就是"在文本上长得一样。 + */ + const CAUSAL_BLOCK_RE = /const CAUSAL = \[[\s\S]*?\n \];/; + const searchable = src.replace(CAUSAL_BLOCK_RE, (m) => m.replace(/[^\n]/g, ' ')); + if (searchable.split(from).length - 1 !== 1) { + console.log(`RED 因果判据的锚文本在源码里不唯一(${name})⇒ 这条判据没在测它以为的东西`); + reds.push(`(自检)因果锚文本不唯一:${name}`); + continue; + } + writeFileSync(target, src.replace(from, to)); + const got = spawnSync(process.execPath, [target, `--only-selftest=${name}`], + { encoding: 'utf8', cwd: ROOT }); + if (got.status === 0) { + console.log(`RED 把 \`${name}\` 守的生产符号改坏之后,它**仍然全绿**` + + ' ⇒ 它没在读那个符号(或它是个空壳)'); + reds.push(`(自检)**因果判据**:改坏生产符号后 \`${name}\` 不红 —— 它没在读它`); + } else if (got.status !== 1) { + console.log(`RED 因果判据跑 \`${name}\` 时退出码是 ${got.status}(不是 1)` + + ' ⇒ 它不是"报红",是崩了/环境不对,这个读数无效'); + reds.push(`(自检)因果判据里 \`${name}\` 退出码异常:${got.status}`); + } + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + } const SAMPLE_FAKE_NAMES = ['fooSelfTest']; const allNames = [...new Set([...selfSource.matchAll(/\b(\w+SelfTest)\b/g)].map(m => m[1]))]; const nameUnwired = allNames.filter(n => !wired.has(n) && !SAMPLE_FAKE_NAMES.includes(n));