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` 状态机要跑起来才算数 ['cross-client-theme.test.mjs', 1], // 设备条里那句「声明层全绿时,渲染那一层没人看过」的出处说明—— // 它正是本轮补上的那个缺口(渲染 vs 声明),留着当出处 ['debt-visibility.test.mjs', 12], // 本文件:N 处是词表定义 + 报错文案 + 上面那段解释(第 N+1 处即红) ['harmony-admin.test.mjs', 1], // 用户管理页:本机无设备 ⇒ 只能证明"代码里这么写" /* * `harmony-imageprep.test.mjs` 4 → 6(2026-09-19):新增的设备判据里如实标了两处 * 如实标了那两处「这一半还没做到」—— ① 应用真的用 `image.createImagePacker()` 压一次时 * 产出的字节是否落在预期区间;② "只有测试在用的代码"那条理由。 * 这两处**不是新欠账**:它们正是 `docs/DEBTS.json` 的 * `harmony-p4c-boundary-decls` 那笔(已在那一笔的 note 里写明 * "只做了一半、剩的一半要等真人操作窗口")⇒ 先补余额、再改这个数字。 */ ['harmony-imageprep.test.mjs', 6], /* * `harmony-deviceprobe.test.mjs` 的 2 处:**都不是"这块没验"的边界声明**,而是 * 对**词表本身**的断言 —— ① `unverifiedReason(VERDICT_OTHER)` 必须含「未验」; * ② `mayAssertOn(…)` 那条的说明文字("失效方式是永远未验")。 * 但**按次数登记、不整文件放行**(π 2026-09-15 指出的形状):整文件放行的话, * 将来在这个文件里写一句真实的「这里没判 / 已知缺口」就**不会红** —— * 那正是"新文件不会自动跑一遍守卫"这个洞。2 就是上限,第 3 处即红。 */ ['harmony-deviceprobe.test.mjs', 2] ]); 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 || ''}`); /* * 本文件**也**在受判之列 —— 而且**按次数**登记,不整文件放行(pi 2026-09-14)。 * * 我第一版给它的是"整文件豁免",理由是"用自己定义的词表数自己无意义"。这句话是对的, * 但它推出的是"**这 N 处**无意义",不是"**这个文件**无意义":整文件放行的后果很具体 —— * 将来有人在这个文件里写一句真实的「这里没判 / 已知缺口」,①不会红。 * 那就是 `migrate.go` 那个形状换了个落点,这次落在判据自己身上。 * * 所以现在把 N 当成普通登记值:N 处是**词表定义与报错文案**(它们必须提到这些词), * 第 N+1 处就是新的边界声明 ⇒ 红。 * * **词表是采样、不是完备**(同一个词表键控的盲区):换 `TODO` / 「这里没判」/「跳过」 * 等同义说法声明同一个边界,本判据**抓不到** —— 这条盲区已登记在 `docs/DEBTS.json` * 的 `boundary-vocabulary-incomplete`,所以在默认路径的余额里看得见,不靠记得。 */ const files = readdirSync(HERE).filter(f => f.endsWith('.test.mjs') || f.endsWith('.mjs')); 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 的理由同此)`); } });