fix(欠账): RESULT 行同时打"登记 vs 实测"(副本漂移可见 + 不等即红);共享树 stash 纪律与两条推广入册

pi 2026-09-14 三条。

1. **可见的那个数字是副本**:`docs/DEBTS.json` 手可编辑,而它唯一的守卫是 Go 那条断言
   —— 相位耦合(打印它的相位里 Go 断言未必跑过)+ 副本会漂移(我自己 §12 那句话)。
   现在 RESULT 行同时给出:

   `debts=9(registered) measured_static=5==登记 ✓(…;本相位只有 static 有权威实测,其余由 go test 侧判据比对)`

   即:**本相位能实测的(static,权威来源就是代码)当场比对并标注 ✓/✗**;
   **测不了的明说由谁比对**,不冒充已实测。并新增判据:**static 的登记值 ≠ 实测值即红**。

2. **共享树上不要用 `git stash` / `--autostash`**(pi:同一台机器,只是自动化了)——
   它把**别人**未提交的改动也收走,冲突/中断时可能留在 stash 里,而 ta 只看到"我的改动不见了"。
   入册 CRITERIA §8.1:优先 `git worktree add`;必须原地操作就先约定窗口;事后核对别人的文件还在原处。

3. **两条推广入册**(各只出现过一次,但形状会复发):
   · §15 **判据必须自足**:不许通过共享可变状态在判据之间传递结论 —— 顺序依赖的显形方式
     永远是**假绿**(我踩过:余额由 SKIP 那支写入、登记判据去读 ⇒ 同包按文件顺序跑 ⇒ 读到 0);
   · §16 **"要提醒人的"输出必须走默认路径**(`TestMain` 收尾打印在常态 `go test` 里不存在),
     配套动作是 pi 那句:**写完"可打印的余额"之后,去看一眼默认路径实际打出来的样子**。
This commit is contained in:
2026-09-14 17:21:26 +08:00
parent 512b3f8f80
commit b16e2d24fd
3 changed files with 85 additions and 3 deletions

View File

@ -325,6 +325,18 @@ git worktree add /tmp/attr HEAD #
**一句话**:归因手段的选择标准是"**会不会让别人的东西处于危险里**",不是"哪条命令最快"。
## 8.1 共享树上不要用 `git stash` / `--autostash`(同一台机器,自动化了而已)
`git stash` 与 `git rebase --autostash` 是同一族:**把工作树里全部未提交改动收走,稍后再放回**。
在共享树上这意味着**别人未提交的改动**也被收走了 —— 冲突或中断时它们可能**留在 stash 里**
而不是回到工作树,而 ta 那边只会看到"我的改动不见了"(pi 2026-09-14 指出)。
纪律(照 §8 那条"stash 只影响 tracked 文件 ⇒ 树干净是**假干净**"升级):
1. **优先 `git worktree add`**:在一棵干净的工作树里做 rebase/改写,碰不到任何人;
2. 必须原地操作时:**先与并发写者约定窗口**("我会 rebase,请先停手"),或明确告知;
3. 事后**核对别人的文件还在**(`git status` 里该有的 WIP 还在原处),别只看自己的改动回来了。
## 9. 已知限制(登记处)
判据的**能力边界**必须写在这里,否则它的长期结局只有两个:被当成"它就是全能的"而误用,
@ -445,3 +457,26 @@ pi 2026-09-14 提的形状:**不是无条件 fail-closed** —— 缺字段可
这与"清册条目的『允许』要给出一条能判的约束"(§11 族)是同一条纪律的两半:
前一半保证**判据有分辨力**,这一半保证**分辨力不会被"改数字"抹掉**。
## 15. 判据必须**自足**:不许通过共享可变状态在判据之间传递结论
**规则**:一条判据的结论必须由它自己测量出来。**判据之间不许共享可变状态**
(包级变量、临时文件、上一条判据留下的标记)。一旦共享,结果就取决于**执行顺序**,
而顺序依赖的显形方式永远是**假绿**(不是假红 —— 假红会被看见)。
真实例子:`debts` 的余额第一版由"SKIP 那一支"写入、由登记判据去读。
Go 在同一包内**按源文件顺序**跑测试 ⇒ 登记那条先跑就读到 `0` ⇒ 全绿而余额是错的。
修法:把测量抽成自足函数(`measureMailStatusDebt(t)`),**谁调用谁测**。
## 16. "要提醒人的"输出必须走**默认路径**(不能只活在 `-v` 后面)
**规则**:余额 / 自报 / 提醒类信息必须出现在**默认命令**的输出里;只有**诊断细节**
才可以藏在 `-v` 后面。
真实例子:我把欠账余额打在 `TestMain` 收尾 —— 而 `go test` 对**通过**的包
**不打印包的输出**(只在失败或 `-v` 时打),于是常态运行里一个字都看不见:
"可打印的余额"变成了"看不见的余额",与不显形等价。
修法:可见的那份挪进 electron 套件的 `RESULT` 行(默认路径),Go 侧只做同源比对。
**配套动作(pi 2026-09-14)**:写完一处"要提醒人的输出"之后,
**去看一眼默认路径的输出** —— 不是看代码,是看它实际打出来的样子。

View File

@ -152,3 +152,32 @@ test('自检:分类逻辑对合成输入有分辨力(否则这条判据可
assert.equal(parsed[0].files.length, 2, '文件清单要解析到(否则 isMixed 永远 false)');
assert.equal(parsed[1].files.length, 1);
});
/**
* ★ 欠账余额的**可见副本**必须与实测一致(pi 2026-09-14 裁定 §1)。
*
* `docs/DEBTS.json` 是手可编辑的副本;只打登记值的话,可见数字可以相对现实过期而全绿
* —— 这正是"门挂错了相位"的又一格。本判据管得住的那一笔是 `static-criteria`:
* 它的权威来源就是代码本身(`STATIC_ONLY.length`,在 `run-all.mjs` 里)。
* (Go 侧那几笔由 `go test` 的 `TestDebtLedgerMatchesMeasurement` 比对;本相位测不了它,
* 所以 **不**在这里假装比对 —— 只把"能测的那笔"钉住。)
*/
export async function debtLedgerStaticMatches() {
const { readFileSync } = await import('node:fs');
const { join } = await import('node:path');
const src = readFileSync(join(HERE, 'run-all.mjs'), 'utf8');
const registered = JSON.parse(readFileSync(join(HERE, '..', '..', '..', 'docs', 'DEBTS.json'), 'utf8'))
.debts.find(d => d.id === 'static-criteria').count;
// 实测:STATIC_ONLY 的条目数(数 `'test/xxx.test.mjs', '...', '...'` 形状的行)
const block = /const STATIC_ONLY = \[([\s\S]*?)\n\];/.exec(src);
assert.ok(block, '要能取到 STATIC_ONLY 清单');
const measured = (block[1].match(/^\s*\['test\//gm) || []).length;
return { registered, measured };
}
test('★ 欠账登记里的 static-criteria 必须等于实测(可见副本不许漂移)', async () => {
const { registered, measured } = await debtLedgerStaticMatches();
assert.equal(measured, registered,
`欠账登记说 static-criteria=${registered},实测 ${measured} —— 可见的那个数字是副本,` +
`漂移了就必须两边一起改(这类"登记类"判据别改数字了事:先看是哪笔欠账还清了)`);
});

View File

@ -512,12 +512,30 @@ if (skippedByPhase.length) {
* 所以 Go 那边的 Skip 余额必须在**这边**可见。两边读**同一个**登记(docs/DEBTS.json),
* 于是"三笔欠账一处看全"且有余额、有到期。
*/
/*
* 欠账余额:**登记值与实测值一起打**(pi 2026-09-14 裁定 §1)。
*
* 为什么不能只打登记值:`docs/DEBTS.json` 是**手可编辑的副本**,而"可见的那个数字就是副本"
* 恰好违反我自己写进 §12 的那句 —— 比较的对象必须是权威来源,不是手边那份会过期的副本。
* 所以这一行同时给出:
* · `registered`:登记值(副本,谁都能改);
* · `measured`:**本相位能实测的部分**(这里有权威来源的只有 static:`STATIC_ONLY.length`);
* · 本相位测不了的(Go 侧那几笔)**明说由谁比对**,不冒充已实测。
* 不等即红:下面这条判据就是这一行(static 的登记值必须等于实测值)。
*/
const DEBTS_PATH = join(HERE, '..', '..', '..', 'docs', 'DEBTS.json');
let debtsLine = '';
let debtRegStatic = null;
try {
const led = JSON.parse(prose(join(HERE, '..', '..', '..', 'docs', 'DEBTS.json')));
const items = led.debts.map(d => `${d.id}:${d.count}`).join(',');
const led = JSON.parse(prose(DEBTS_PATH));
const total = led.debts.reduce((n, d) => n + d.count, 0);
debtsLine = ` debts=${total}(${items})`;
const items = led.debts.map(d => `${d.id}:${d.count}`).join(',');
const regStatic = led.debts.find(d => d.id === 'static-criteria');
debtRegStatic = regStatic ? regStatic.count : null;
const measuredStatic = STATIC_ONLY.length;
debtsLine = ` debts=${total}(registered) measured_static=${measuredStatic}` +
`${debtRegStatic === null ? '(登记无此项)' : debtRegStatic === measuredStatic ? '==登记 ✓' : `≠登记${debtRegStatic} ✗`}` +
`(${items};本相位只有 static 有权威实测,其余由 go test 侧判据比对)`;
} catch (e) {
debtsLine = ` debts=登记读不到(${e.code || e.message})`;
}