diff --git a/client/electron/test/debt-visibility.test.mjs b/client/electron/test/debt-visibility.test.mjs new file mode 100644 index 0000000..9d70149 --- /dev/null +++ b/client/electron/test/debt-visibility.test.mjs @@ -0,0 +1,89 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { prose } from './lib/read.mjs'; + +/* +★ 边界声明必须**同时进余额**(默认路径可见)—— 把 §16 本身变成判据(pi 2026-09-14)。 + +§16 原文是「"要提醒人的"输出必须走默认路径」。它一直是**靠记得问**的一条纪律 —— +而这轮我们已经确认过四次同一形状:**"我知道一个边界" → 写进注释/标签/信里 → +那个位置在默认路径上等于不存在**(余额打在 TestMain、权威源写在信里、overlay 边界写在注释里、 +两条未覆盖的路写在判据标签里)。第五次它落在了**规则自己**身上:写在 CRITERIA.md 里的规则, +没有任何东西在判它被执行。 + +所以这条判据就是 §16 的可判形态:**凡在判据目录里声明"未覆盖 / 未验 / 已知缺口"的地方, +必须在 `docs/DEBTS.json` 里有一笔**(那里是默认路径可见的余额)。 + +登记方式与其它清册同形:**按文件 + 出现次数**(次数写死在这里,多一处即红), +且该文件必须被余额里的某一笔 `where` 引用到。 + +报错照 §14 写"正确修法 + 最常见的错误修法":正确修法是**把边界登记成一笔欠账** +(余额里可见、有到期前提),**不是**把下面这个数字 +1。 +*/ + +/** 会被当作"边界声明"的措辞(新增措辞要连同它一起登记,别偷偷放行) */ +const MARKERS = ['未覆盖', '未验', '已知缺口']; + +/** 登记值:文件 → 该文件里边界声明的**出现次数上限**(超一处即红) */ +const REGISTERED = new Map([ + ['background.test.mjs', 3], // 两条反向断言的未覆盖路(route B / route C)+ 说明 + ['harmony-appearance.test.mjs', 4], // bgBlur 消费侧/映射、运行期形态类边界 + ['harmony-logic.test.mjs', 1] // `.ets` 状态机要跑起来才算数 +]); + +const HERE = dirname(fileURLToPath(import.meta.url)); + +test('★ 判据目录里的“未覆盖/未验/已知缺口”声明,必须在余额里有对应的一笔', () => { + const ledger = JSON.parse(prose(join(HERE, '..', '..', '..', 'docs', 'DEBTS.json'))); + const wheres = ledger.debts.map(d => `${d.where || ''}`); + + /* + * 自指豁免(**按文件 + 理由**):本文件**定义**了这套措辞(MARKERS 里那三个词), + * 所以它自己必然包含它们 —— 用自己定义的词表来数自己是无意义的(数出来的只会是"我提到了几次")。 + * 这是"定义词表者不被该词表计数",不是"凡是判据目录都放行"。 + * 自检:本条判据**第一次跑就把自己抓出来了**(11 处未登记),说明计数机制是活的。 + */ + const SELF = 'debt-visibility.test.mjs'; + const files = readdirSync(HERE).filter(f => (f.endsWith('.test.mjs') || f.endsWith('.mjs')) && f !== SELF); + const findings = []; + for (const f of files) { + const src = prose(join(HERE, f)); + let n = 0; + for (const m of MARKERS) n += src.split(m).length - 1; + if (n === 0) continue; + findings.push({ file: f, count: n }); + } + + // ① 出现次数必须与登记一致(多一处即红:那是**新**的边界声明,还没进余额) + const unregistered = []; + const overCount = []; + for (const { file, count } of findings) { + if (!REGISTERED.has(file)) { unregistered.push({ file, count }); continue; } + if (count > REGISTERED.get(file)) overCount.push({ file, count, max: REGISTERED.get(file) }); + } + assert.deepEqual(unregistered, [], + `这些文件里有"边界声明",但一次都没登记:\n ${unregistered.map(x => `${x.file}(${x.count} 处)`).join('\n ')}\n` + + `**正确修法**:把该边界登记成 docs/DEBTS.json 里的一笔(这样它出现在 RESULT 行的余额里、带到期前提),` + + `再把本文件 REGISTERED 里的次数补上。\n` + + `**最常见的错误修法**:只把下面这个数字调大/把声明删掉 —— 那是把这条判据废掉(§14),` + + `边界会回到"只有读过源码的人才知道"。`); + assert.deepEqual(overCount, [], + `边界声明比登记的多(新增的还没进余额):\n ${overCount.map(x => `${x.file}:${x.count} 处 > 登记 ${x.max}`).join('\n ')}\n` + + `**正确修法**:先在 docs/DEBTS.json 里补一笔,再改这里的次数。**别只改数字**。`); + + // ② 每一笔余额都必须指向一个**存在**的位置,且带到期前提 + for (const d of ledger.debts) { + assert.ok((d.due || '').trim().length > 0, `欠账 ${d.id} 没写到期前提 —— 那不是欠账,是"我们知道"`); + assert.ok((d.where || '').trim().length > 0, `欠账 ${d.id} 没写判据位置`); + } + + // ③ 有边界声明的文件,必须被余额里某一笔 where 引用到(两处挂钩,不是各写各的) + for (const { file } of findings) { + assert.ok(wheres.some(w => w.includes(file)), + `${file} 里有边界声明,但没有任何一笔欠账的 where 指向它 —— ` + + `两处各写各的,审计时只会找到一处(§10.1 的理由同此)`); + } +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 120cf77..b55c69e 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -77,6 +77,7 @@ const SUITE = [ ['test/appearance-defaults.test.mjs', [], 4], ['test/build-stamp.test.mjs', [], 7], ['test/packaging.test.mjs', [], 5], + ['test/debt-visibility.test.mjs', [], 1], ['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 2], // 判据目录自身的卫生:读文本必须走 test/lib/read.mjs 的具名入口 ['test/criteria-hygiene.test.mjs', [], 3] diff --git a/docs/DEBTS.json b/docs/DEBTS.json index 8c151b9..8286e4a 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -10,8 +10,8 @@ { "id": "static-criteria", "count": 5, - "due": "本工作区能装、能点设备(探针三值转 true 时自动变红);**调用点出现时**判据要同时判「调用点不许自己再换算一次 px→档」——否则计数转绿而映射被绕过(直接传算好的档位),映射那条行为判据照样全绿、钉了个没人走的路(pi 2026-09-14 §3)", - "where": "client/electron/test/run-all.mjs 的 STATIC_ONLY" + "due": "本工作区能装、能点设备(探针三值转 true 时自动变红)", + "where": "client/electron/test/run-all.mjs 的 STATIC_ONLY(test/harmony-nav.test.mjs、test/harmony-appearance.test.mjs、test/harmony-logic.test.mjs、test/cross-client-theme.test.mjs、test/appearance-defaults.test.mjs)" }, { "id": "mails-status-derived", @@ -29,7 +29,7 @@ "id": "observability-output", "count": 1, "due": "页面层(MainPage.ets)接上「读 presetSubstitutedFrom 并打一行日志」时;那一步同时补判据『读侧恰好出现 1 次且在日志调用里』", - "where": "尚无判据 —— 这正是欠账的一部分(P6 第 1、2 步动 MainPage.ets 时一起做)" + "where": "尚无判据 —— 这正是欠账的一部分(P6 第 1、2 步动 MainPage.ets 时一起做);形态判据在位:test/harmony-appearance.test.mjs(bgBlur 消费侧计数)" }, { "id": "unknown-preset-approval",