pi 的两条"真实的洞",都落了,而且第一条当场抓到实证。 1. **探针三值**(可用 / 不可用 / 拿不准→红):`RESULT static=5 probe=ok|unknown` 把"欠账余额"和"探针是否健康"拆成两个数字。 **换完第一次运行就报 probe=unknown** —— 一查:探针调的是 `execFileSync`, 而这个文件 import 的是 `spawnSync`,**名字根本没定义**。也就是说 **探针从写下的那天起一次都没跑成过**,旧的两值设计把 `ReferenceError` 和"没有设备"一起吞掉、统一报成"设备不可用":机制在、闸门从没开过, 而它看起来完全健康。这正是 pi 描述的"恒不开闸",只是比预想更彻底。 现在:命令在但跑不成 → unknown → 红;所有候选都不存在(本机没装 hdc)→ 可判的 "没有设备工具" → false,避免没装 SDK 的机器天天假红。 附 `--probe-selftest`(只跑分类器,不跑套件)+ 变异验证(把 unknown 当"不成立"→ 红)。 2. **releaseCandidate = !gitDirty**(从展示升成标签):BUILD_INFO 现在自报 `releaseCandidate`,发布脚本在脏树时会打印"这个包不是发布候选"。 判据 `build-stamp` 断言"标签与 gitDirty 必须一致"。 **实证**:本轮我打的包正是这种情况 —— `gitDirty: true`(含着 gui-lab 未提交的 NarrowStack/index.css),`releaseCandidate: false`,日志里明确说了"不是发布候选"。 3. 附带:`criteria-hygiene` 加一条"用到 `code/prose/bytes` 就必须真的 import"。 理由是同一形状我这轮在三个文件里各犯过一次(最后一次是 `execFileSync`/`spawnSync`), 而它表现为"判据红了"(ReferenceError 抛在判据自己身上),看起来像判据失败、 不像判据写错。这条至少把最常写错的那几个名字变成明确的红。
132 lines
6.8 KiB
JavaScript
132 lines
6.8 KiB
JavaScript
/**
|
||
* 判据目录自身的卫生:**读文本必须走 `test/lib/read.mjs` 的两个具名入口**。
|
||
*
|
||
* # 为什么这条判据存在(pi 2026-09-14 §4)
|
||
*
|
||
* 规范里写着"判代码读剥离版(`code`)、判理由/文档读原文(`prose`)",
|
||
* 我 P5 写过一次、当天又踩了一次:那条断言读的是**原文**,而它要找的标识符
|
||
* 恰好出现在一段解释性注释里 → 误报。**第二次犯规说明问题不在记性,在形态**:
|
||
* 靠人记得执行的规范一定会有下一次。
|
||
*
|
||
* 所以把"用哪个读取器"从**记忆**变成**代码里的一个词**,并且可被检查:
|
||
* - `code(path)` —— 剥掉注释;判"代码里有没有这个调用/这个值";
|
||
* - `prose(path)` —— 原文;判"注释/文档里写了什么";
|
||
* - `bytes(path)` —— 二进制(安装包等)。
|
||
*
|
||
* # 判据
|
||
*
|
||
* 判据目录(`test/**` 里跑的判据 + `run-all.mjs`)中**不得出现裸 `readFileSync`**,
|
||
* 唯一例外是 `test/lib/read.mjs` 自己。`test/manual/**` 是人工脚本、不是判据,不在范围内。
|
||
*
|
||
* 附两条自检:读取器本身要真的剥注释(否则 `code` 退化成 `prose` 这条判据就废了)、
|
||
* 以及探测器要能认出裸调用(否则"都没有"与"探测器坏了"结果一样)。
|
||
*/
|
||
import assert from 'node:assert/strict';
|
||
import { readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
||
import { dirname, join, relative } from 'node:path';
|
||
import { test } from 'node:test';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
import { code, prose } from './lib/read.mjs';
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const RELECTRON = join(HERE, '..'); // test/ 的上一级就是 client/electron
|
||
const SELF = join(HERE, 'lib', 'read.mjs');
|
||
|
||
/** 判据文件清单:`test/**` 下会跑的判据 + 编排器;不含 lib/ 与 manual/ */
|
||
function criteriaFiles(dir = HERE, out = []) {
|
||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||
const p = join(dir, e.name);
|
||
if (e.isDirectory()) {
|
||
if (e.name === 'lib' || e.name === 'manual' || e.name === 'node_modules') continue;
|
||
criteriaFiles(p, out);
|
||
} else if (/\.(test\.mjs|test\.ts|test\.tsx|mjs)$/.test(e.name) && !e.name.endsWith('.d.ts')) {
|
||
out.push(p);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** 探测器:一段源码里有没有裸 readFileSync */
|
||
const BARE = /\breadFileSync\s*\(/;
|
||
|
||
test('探测器自检 + 读取器自检', () => {
|
||
// ① 探测器能认出裸调用(否则"都没有"与"探测器坏了"分不开)
|
||
assert.equal(BARE.test("const s = " + "readFile" + "Sync(p, 'utf8');"), true);
|
||
assert.equal(BARE.test('const s = prose(p);'), false);
|
||
|
||
// ② code 真的剥注释、prose 不剥 —— 这条是整套用法的地基:
|
||
// 若 code 退化成 prose,那么"读剥离版"的规范就变成一句空话,而且没人会发现。
|
||
const probe = join(RELECTRON, 'test', '_reader_probe.tmp.ts');
|
||
// 注:这个探针文本**故意拼接**而不是写字面量 —— 否则本判据自己会被自己判红
|
||
// (它扫的就是"文本里有没有这个写法",判据文件也在扫描范围内)。
|
||
writeFileSync(probe, "const REAL = 1; // " + "readFile" + "Sync( 注释里的假调用\n/* allowed-once */\n");
|
||
try {
|
||
assert.ok(!code(probe).includes('allowed-once'), 'code() 必须剥掉块注释');
|
||
assert.ok(!code(probe).includes('假调用'), 'code() 必须剥掉行注释');
|
||
assert.ok(code(probe).includes('REAL'), 'code() 要保留真代码');
|
||
assert.ok(prose(probe).includes('allowed-once') && prose(probe).includes('假调用'), 'prose() 必须保留注释');
|
||
} finally {
|
||
unlinkSync(probe);
|
||
}
|
||
});
|
||
|
||
test('★ 判据目录里不得出现裸 readFileSync(必须走 code/prose/bytes)', () => {
|
||
const offenders = [];
|
||
for (const f of criteriaFiles()) {
|
||
if (f === SELF) continue; // 读取器的实现自己当然要用它
|
||
const src = prose(f); // 扫的是"文本里有没有这个写法",所以读原文
|
||
if (BARE.test(src)) {
|
||
const line = src.split('\n').findIndex(l => BARE.test(l)) + 1;
|
||
offenders.push(`${relative(RELECTRON, f)}:${line}`);
|
||
}
|
||
}
|
||
assert.deepEqual(offenders, [],
|
||
`这些判据文件里还在裸用 readFileSync:\n ${offenders.join('\n ')}\n` +
|
||
" 改用 test/lib/read.mjs 的具名入口:\n" +
|
||
" · code(path) —— 剥掉注释。判「代码里有没有这个调用/这个值」时用它(默认选它);\n" +
|
||
" · prose(path) —— 原文。判「注释/文档里写了什么」时用它;\n" +
|
||
" · bytes(path) —— 二进制(安装包等)。\n" +
|
||
" 为什么不能裸用:读原文去判代码,会被解释性注释骗(同一个坑已经踩过两次)。");
|
||
});
|
||
|
||
/**
|
||
* ★ 用到 `lib/read.mjs` 的导出名就必须真的 import(我这轮在三个文件里各犯过一次)。
|
||
*
|
||
* 形状一模一样:`code(...)` / `prose(...)` 写下去,import 里却只有另一个 ——
|
||
* 于是在**跑起来的那一刻**才炸 `ReferenceError`,而它抛在判据自己身上,
|
||
* 看起来像"这条判据红了",不像"判据写错了"。dsh 桥那边也栽过同一形状
|
||
* (`MODE_FULL` 没 import,而且被 `tsc | tail` 的退出码骗过)。
|
||
*
|
||
* 判据做法:把每个判据文件里出现的 `code(`/`prose(`/`bytes(` 收集起来,
|
||
* 与它从 `lib/read.mjs` 实际 import 的名字比对;缺一个就红,并点名文件与名字。
|
||
* **例外**:文件里自己定义了同名函数(本地实现)时不算缺 —— 但那种情况要显式声明。
|
||
*/
|
||
test('★ 用到 code/prose/bytes 就必须 import(不许靠运行时才发现)', () => {
|
||
const EXPORTS = ['code', 'prose', 'bytes'];
|
||
const problems = [];
|
||
const SELF_PATH = fileURLToPath(import.meta.url);
|
||
for (const f of criteriaFiles()) {
|
||
// 它自己的源码里就写着 code/prose/bytes 这几个名字(EXPORTS 列表),跳过自己
|
||
if (f === SELF_PATH) continue;
|
||
const src = prose(f);
|
||
if (src.includes("from './lib/read.mjs'") || src.includes("from '../lib/read.mjs'")) {
|
||
const m = /import \{([^}]*)\} from '\.\.?\/lib\/read\.mjs'/.exec(src);
|
||
const bound = new Set((m ? m[1] : '').split(',').map(x => x.trim().split(/\s+as\s+/).pop()).filter(Boolean));
|
||
for (const name of EXPORTS) {
|
||
if (new RegExp(`\\b${name}\\(`).test(src) && !bound.has(name)) {
|
||
problems.push(`${relative(RELECTRON, f)} 用了 ${name}(…) 但没 import(已 import:${[...bound].join('、') || '无'})`);
|
||
}
|
||
}
|
||
} else {
|
||
for (const name of EXPORTS) {
|
||
if (new RegExp(`\\b${name}\\(`).test(src)) {
|
||
problems.push(`${relative(RELECTRON, f)} 用了 ${name}(…) 但根本没从 lib/read.mjs import`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
assert.deepEqual(problems, [],
|
||
`这些判据会以 ReferenceError 的形式红,看起来像"判据失败了",其实是"判据写错了":\n ${problems.join('\n ')}`);
|
||
});
|