Files
MailUI4Agents/client/electron/scripts/build-info.mjs
JianFeeeee 456ae5a66d 跨端: pi 五条评审落地 —— 产物自证替代时间戳代理、静态判据可到期、判据能读一层标识符、值/来源配对规则、归因不许动别人的树
pi 2026-09-14 的评审(`9839f8a9`)五条,逐条落地;其中 §6 的两条是**核对后已成立**,不重复劳动。

## 1 产物自证:`dist/BUILD_INFO.json`(pi §1)

原来那条判据是"`dist` 比 `src` 新"——**代理变量**,pi 指出两层都靠不住:看不见"构建是否
成功"(实测过),而"`dist` 比 `src` 新"也不等于"dist 是从这份 src 构建的"(`checkout`/`cp`/时钟
都骗得过 mtime;我确实用 checkout 造过一次假红)。现在改成**内容自证**:
`scripts/build-info.mjs` 在构建最后一步写 `{gitRev, gitDirty, srcHash, srcFiles, buildCmd, builtAt}`,
判据重算当前指纹再**精确比对**(`test/build-stamp.test.mjs`)——"代理"两个字没有了,
报错能直接读出两边指纹。`release-linux.sh` 打完包把同一份信息打进日志(一个包自带
"它对应哪个源码状态")。`gitDirty` **只展示不判定**:共享工作区常年是脏的,拿它当红/绿依据
会天天误报。

实测三变异:改 src 内容不重建 → 红(两个指纹都打出来);BUILD_INFO 记成别的提交 → 红;
**`touch`(只动 mtime)→ 绿** —— 旧判据在这里是**假红**,新判据不误伤,这是它严格更好的地方。

## 2 静态判据的**欠账**与到期(pi §5)

"暂时"不是状态、是待办,规范里写下的"暂时"没有任何机制回来读它。改成可机检的形状:
`run-all.mjs` 登记 `STATIC_ONLY`(5 条:`.ets` 只能验形态)+ **必填到期前提**(探针,真跑
`hdc list targets`);汇总打 `RESULT static=N`(**欠账余额**);**前提一旦为真,这些判据当场
变红**并要求"改成行为判据或换更准的前提"。实测:探针恒真 → 5 条同时报"到期";把前提写成
`'vibes'`(未知探针/陈述)→ 套件红。这是"自报条数 < 登记条数"的**时间版本**。

## 3 判据能解析一层标识符(pi §3)

Go 默认值判据原来"值不是字面量 → 判据读不懂 → 红",长期结局是有人做一次无害重构
(`BgDim: defaultDim`)就把判据逼宽、再下一步少核一个字段。现在:字面量直接用;
标识符在**同一文件**查 `NAME = <字面量>`;查不到(跨包/计算/iota)才报"读不懂"。
实测:`BgDim: defaultDim` + `const defaultDim = 12` → **绿**(无害重构不再误伤);
`const defaultDim = baseDim + 0` → 红且报文说"读不懂"。**只解析一层**:再深就是"执行 Go"了。

## 4 §6.7 的可机检分流规则(pi §2)

新增 §6.7.0:**值 → 行为判据,来源 → 静态判据**,理由写成覆盖问题(行为判据只覆盖它跑到的
路径 ⇒ 原理上判不了"有没有别的路绕过去";来源约束要的是全程序可达性 ⇒ 只有读代码能答)。
两条推论:静态判据判值永远差一个反例、行为判据判来源永远差一条路径;**不是强弱,是分工**,
缺任一条那一对就是假判据。附本仓已有的完整样例(P5 命中区:值判据"≥44"+ 来源判据
"应用点必须引用常量"),新增 §6.8 记欠账机制、§8 记共享树归因纪律。

## 5 值/来源配对补齐 + 让位派生(pi §6 的两条)

- 命中区原来只有值那一半:补**来源**判据(`.ets` 里出现 `minHeight: 44` 这类裸数字 → 红,
  报文点出"值判据管数字够不够大、来源判据管用的是不是同一个数字")。变异:写死 44 → 红。
- "`76` 应从条高派生":**核对后已成立**(`NAV_CONTENT_RESERVE = NAV_BAR_HEIGHT +
  NAV_BAR_BOTTOM + 8`,判据也按常量算)。顺手把裸的 `8` 起名 `NAV_CONTENT_GAP`
  (这一族里唯一还需要人判断的数),并加判据钉住**派生关系**:写回字面量 `76` → 红。

## 6 `legacy.bak` 的生命周期(pi §4):**有意永久残留**,并说清代价

pi 质疑成立:判据钉死"没有任何代码读它"⇒ 也没有任何代码能删它。**否决了"点重置外观时删"**:
最可能点重置的人正是外观被接管的那个人,而这份备份是他唯一的旧值,那时删等于把恢复数据
毁在最需要的时刻。所以定为"有意永久残留"(没有代码路径能判定何时安全删除——那取决于人),
并把代价量出来写进 §7.12:值受 `MAX_DATA_URL_BYTES`(2.4MB)约束,**最坏是一张用户原图的
整份副本**,共享机器上即一份别人的外观;要清就手工 `removeItem`。判据钉住这行**同时**给出
"为什么不删"与"怎么删"(变异:删掉删除方法 → 红)。

## 7 归因方法(pi §6 第三条):认错并写成纪律

我当天归因 `narrow-layout` 的 3 条红时用了 `git stash push -- MailView.tsx`,动的是**并发写
作者的未提交改动**。结论对、方法不行:它写共享工作区(别人崩溃/`git add -A` 就丢他的活),
而且只影响 tracked 文件(untracked 的 WIP 还在 ⇒ "干净了"是假干净,结论也可能假)。
纪律写进 CRITERIA.md §8:只读手段(`git show HEAD:path > /tmp/...`、`git worktree add`)或直接问作者。

## 验证

`npm test` 全绿(14 个判据文件 + vitest 266 + typecheck),`RESULT static=5`;
`hvigorw assembleHap` BUILD SUCCESSFUL;`scripts/release-linux.sh` 重打 deb(日志带产物指纹)。
提交前两道**真红**按预期挡了我:改了 src 未重建 → build-stamp 红;重建了 dist 未重打包 →
packaging 红。**未验**(照旧不写成已完成):鸿蒙侧视觉/交互观感、运行期换肤重算(静态判据,
到期前提见 `RESULT static=5`)。

注:本次 `dist`/deb 是在**共享工作区**上构建的,树里含并发写作者未提交的
`CalendarView.tsx`/`index.css` —— 产物里的 `gitDirty: true` + `gitRev` 正是为此留的,
复核时先看那一行。
2026-09-14 16:00:34 +08:00

111 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 构建产物的**自证**`dist/BUILD_INFO.json`。
*
* # 为什么不是时间戳
*
* 原来那条判据是"`dist` 比 `src` 新" —— pi 2026-09-14 指出它有两层代理:
* ① 看不见"构建是否成功"(实测:构建失败但碰过 dist 时它照样绿,见 CRITERIA.md §6.7.1
* ② **`dist` 比 `src` 新也不等于 `dist` 是从这份 `src` 构建的**`checkout`/`cp`/时钟都骗得过 mtime
* 换成内容指纹之后,②也消掉了:判据说的是"产物记的源状态 == 当前源状态"
* 而不是"谁的时间更晚"。任何"包不是从这份源码来的"都变成一条可读出的不等。
*
* # 这个文件同时是"套件自报来源"的构建侧同款
*
* pi 的建议:**一条红/一个包都应自带"它对应哪个源码状态"**`git status --porcelain` 那一族)。
* 消费方:
* - `test/build-stamp.test.mjs` 精确比对(不是比新旧);
* - `scripts/release-linux.sh` 打完包把同一份信息打进日志(出问题时先看它)。
*
* 指纹范围 = **会影响产物的一切**`src/` + 入口 html + 构建配置 + 生成脚本),
* 排除生成物 `background-takeover.generated.css`(它由 `gen:bg` 现生成,
* 内容已由生成脚本决定 —— 计入会让"先 build 后 gen"这种无害顺序变化产生假红)。
*/
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
export const ELECTRON_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
/** 参与指纹的文件/目录(相对 client/electron */
const TRACKED = [
'src',
'index.html',
'vite.config.ts',
'tsconfig.json',
'package.json',
'scripts/gen-background-takeover.mjs'
];
/** 生成物:由上面某个文件决定内容,计入会产生假红 */
const EXCLUDE = new Set(['src/background-takeover.generated.css']);
function walk(abs, rel, out) {
const st = statSync(abs);
if (st.isDirectory()) {
for (const name of readdirSync(abs).sort()) {
walk(join(abs, name), `${rel}/${name}`, out);
}
return;
}
const key = rel.split(sep).join('/');
if (EXCLUDE.has(key)) return;
out.push(key);
}
/**
* 当前源码状态。
*
* 返回 `{ gitRev, gitDirty, srcHash }`
* - `gitRev`HEAD 短哈希(读不到 git 时给 `''`,此时只看指纹);
* - `gitDirty`:工作树是否有未提交改动(**只作信息**:共享工作区常年是脏的,
* 判据不拿它当红/绿依据,拿它当"复核时先看这一行"
* - `srcHash`:影响产物的那批文件的**内容**指纹。
*/
export function srcState(root = ELECTRON_DIR) {
const files = [];
for (const t of TRACKED) {
const abs = join(root, t);
try { walk(abs, t, files); } catch { /* 缺文件就不计(例如还没有 dist 时) */ }
}
files.sort();
const h = createHash('sha256');
for (const f of files) {
h.update(f).update('\0').update(readFileSync(join(root, f))).update('\0');
}
let gitRev = '';
let gitDirty = false;
const repoRoot = join(root, '..', '..');
try {
gitRev = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' }).trim();
gitDirty = execFileSync('git', ['status', '--porcelain'], { cwd: repoRoot, encoding: 'utf8' }).trim().length > 0;
} catch { /* 不在 git 里(例如解包后的目录):指纹仍然有效 */ }
return { gitRev, gitDirty, srcHash: h.digest('hex'), files: files.length };
}
/** 写 `dist/BUILD_INFO.json`(构建路径的最后一步) */
export function writeBuildInfo(root = ELECTRON_DIR, buildCmd = process.env.npm_lifecycle_event || '') {
const st = srcState(root);
const info = {
gitRev: st.gitRev,
gitDirty: st.gitDirty,
srcHash: st.srcHash,
srcFiles: st.files,
buildCmd,
builtAt: new Date().toISOString(),
// 说明这份文件是什么、谁在比它(读到此文件的人不用去翻代码)
note: '构建自证test/build-stamp.test.mjs 用 srcHash/gitRev 精确比对,不是比时间戳'
};
const out = join(root, 'dist', 'BUILD_INFO.json');
mkdirSync(dirname(out), { recursive: true });
writeFileSync(out, `${JSON.stringify(info, null, 2)}\n`);
return info;
}
// 直接执行时 = 构建的最后一步
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
const info = writeBuildInfo();
console.log(`[build-info] dist/BUILD_INFO.json 已写rev=${info.gitRev || '(无 git)'} srcHash=${info.srcHash.slice(0, 12)} dirty=${info.gitDirty}`);
}