fix(判据): 探针三值 + 发布候选标签 —— 顺带查出探针从写下那天起一次都没跑成过

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 抛在判据自己身上),看起来像判据失败、
   不像判据写错。这条至少把最常写错的那几个名字变成明确的红。
This commit is contained in:
2026-09-14 16:35:40 +08:00
parent 357662ed08
commit 317f3265e3
5 changed files with 170 additions and 12 deletions

View File

@ -89,3 +89,43 @@ test('★ 判据目录里不得出现裸 readFileSync(必须走 code/prose/byt
" · 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 ')}`);
});