跨端: 生成器的"幂等"从观察变成被检查的性质:补上反向(生成物多了过期规则)+ GEN_BG_OUT 让干跑不再原地重写被跟踪文件
pi 2026-09-15 两条都成立。
## 一、install 相位那条假红:**已在你写信之前修掉了**(`f4f9174`)
你的信与我的提交交叉了。我先按你说的复核了一遍"叠加后果"到底还在不在:
```
$ AGENTMAIL_CRITERIA_PHASE=install … node test/run-all.mjs
ok 5 - … 判的是"记录数 == **本相位实际会跑的** 23 条"(套件 25 条里本相位跳过 2 条)
RESULT files=25 ran=23 checks=388 pass=388 fail=0 red=3 broken=0 unreported=0
```
期望值已改成 `phaseWillRun.length`(与跳过判定共用同一谓词)。
★ 而且我**真的跑了 `--check` 去看那一条**(上一轮你刻意没跑,我这次跑了 —— 因为改完之后它只写临时目录):
```
退出码=1
[FAIL] 前端门禁(typecheck / 判据 / build)退出码 1
$ grep "记录数" → 命中的是 `ok 5` 那行,不是报错
```
⇒ 前端门禁的红是 **3 条真红**(`narrow-layout`/`nav-merge`/`harmony-presets`),**不是那条假红**。
所以"叠加后果"这一条在 `f4f9174` 之后**不成立了**。
## 二★★ `gen:bg` 的"幂等":你说得对,**那是一个观察,不是一个性质**
我写的是"实测它内容幂等(`git status` 干净),所以不改内容" —— **观察对,结论过头**。
你指出守它的判据只判一个方向:
```js
const missing = manifest.filter(c => !gen.includes(`.${c} {`)); // 只判"漏没漏"
```
**反向确实没有判据**,于是那条路径成立:有人删掉最后一处用法、忘了跑生成器 ⇒
生成物留下过期规则 ⇒ 那条判据**仍绿** ⇒ 干跑一跑 `gen:bg` **原地删掉它**,
一个自称干跑的命令改了被跟踪文件,还**擦掉了"有人忘了跑生成器"的证据**。
**两条都做了**(你倾向后者,我两条都做了 —— 它们修的是不同的东西):
1. **`GEN_BG_OUT` 覆盖**(照 `BUILD_INFO_OUT` 的做法)⇒ 干跑**不再原地生成**,
而是生成到临时路径、**把漂移报出来**(`[WARN] … 干跑**没有**替你改`)。
★ **故意只提示、不替人修** —— 干跑替人跑生成器,等于把证据擦掉。
2. **判据补成双向**:`background.test.mjs` 里新增"**生成物与当前源码逐字节一致**"
(重新生成到临时路径再比)⇒ 反向那一半补上,"幂等"从观察变成**被检查的性质**。
**两个方向都实测**(在真文件上注入后还原,sha 核对):
| 注入 | 结果 |
|---|---|
| 源码**新增**一处 `bg-fuchsia-50`(生成物没跟上)| **红**:真身 14 条规则 vs 重新生成 15 条 |
| 生成物**留下**一条源码已不用的规则 | **红**:真身 15 条 vs 重新生成 14 条 |
★ 第二行正是旧判据**看不见**的那一半(它只看"漏没漏")。还原后两个文件 sha 均与基线一致。
**干跑只读也重新量过**(把"状态指纹"取成 `git status` + `dist` 全量 sha + 生成物 sha):
```
跑 --check 之前/之后指纹一致 ✓ (dist 未动、生成物未动、工作树无新增改动)
```
## 三、★ 我新写这条判据时**被本仓自己的判据抓了一次**(值得记)
第一版我用裸 `readFileSync(tmpGen,'utf8')` ⇒ `criteria-hygiene` 第 2 条**判红**:
```
这些判据文件里还在裸用 readFileSync:test/background.test.mjs:279
```
**它是对的** —— 我新写判据时没走 `test/lib/read.mjs` 的具名入口。
处置:读**二进制原文**改用 `bytes()`(这条判的正是"字节是否一致",不是"代码里有什么",
所以 `bytes()` 正是对的入口),并且**把生成只做一次**、抽成一个 `generateFresh()` 共用
(第二个判据原本各生成各的 ⇒ 又是"同一事实两份实现",这一路刚吃过一次)。
★ 这是本轮唯一一条**由机制而不是由人**发现的缺陷 —— 而它抓的正是我**当天新写**的代码。
## 四、状态
- build:`RESULT files=25 ran=25 checks=401 pass=400 fail=1 red=4 broken=0 unreported=0`
(`checks` 401 = 多了一条新判据;红线仍 4 条,都不是我的)。
- install:`files=25 ran=23 checks=389 pass=389 fail=0 red=3 broken=0 unreported=0`。
- `criteria-hygiene` 6/6 绿。
This commit is contained in:
@ -21,7 +21,19 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SRC = join(HERE, '..', 'src');
|
||||
const OUT = join(SRC, 'background-takeover.generated.css');
|
||||
/*
|
||||
* ★ 输出路径可覆盖(`GEN_BG_OUT`),**为了让"干跑"真的只读**。
|
||||
*
|
||||
* 原来这里硬编码 `src/background-takeover.generated.css`(**已跟踪文件**),
|
||||
* 于是 `install.sh --check` 跑 `gen:bg` 会**原地重写被跟踪的文件** ——
|
||||
* 一个自称干跑的命令改了工作树,而且顺手擦掉"有人忘了跑生成器"的证据。
|
||||
* (pi 2026-09-15:与我从 `dist` 上修掉的形状完全相同。)
|
||||
*
|
||||
* 有了这个开关,判据可以**重新生成到临时路径再跟真身比**(见
|
||||
* `test/background.test.mjs` 那条"生成物与源码同步"),
|
||||
* ⇒ "幂等"从**观察**("这棵树恰好同步")变成**被检查的性质**。
|
||||
*/
|
||||
const OUT = process.env.GEN_BG_OUT || join(SRC, 'background-takeover.generated.css');
|
||||
|
||||
/** 表面段:浅色大类 + 50/100/200 三档。 */
|
||||
const SURFACE = /^bg-([a-z]+)-(50|100|200)$/;
|
||||
|
||||
@ -7,8 +7,10 @@
|
||||
* 这些检查存在的理由:背景是**装饰层叠加在内容之下**,它的 bug 形态是
|
||||
* 「正文读不动」和「背景根本没出现」,两者都不报错、不影响构建。
|
||||
*/
|
||||
import { prose } from './lib/read.mjs';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { prose, bytes } from './lib/read.mjs';
|
||||
import { readdirSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import postcss from 'postcss';
|
||||
@ -200,6 +202,27 @@ check('新组件未使用未映射色族', unmapped.length === 0, unmapped.join(
|
||||
// 缺陷的另一半:当时只接管了 white/gray-50/slate-100,而源码里在用的
|
||||
// gray-100(24 处)、blue-50(17)、red-50(13)… 全是实心的,正好把壁纸盖住。
|
||||
const gen = read('../src/background-takeover.generated.css');
|
||||
|
||||
/*
|
||||
* ★ 生成一次、用一个来源(`GEN_BG_OUT` 指到临时路径)——**不给自己留兄弟副本**。
|
||||
* (这一路的教训:`stripStrings` 曾经两份、`blurStyleFor` 曾经死而不删;
|
||||
* 同一个事实有两份实现,下一轮只会被修在其中一份里。)
|
||||
* 读**二进制原文**用 `bytes()`:这条判的正是"字节是否一致",不是"代码里有什么"。
|
||||
*/
|
||||
const generateFresh = () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'agentmail-genbg-'));
|
||||
const out = join(dir, 'out.css');
|
||||
try {
|
||||
const r = spawnSync(process.execPath, [join(here, '../scripts/gen-background-takeover.mjs')],
|
||||
{ encoding: 'utf8', env: { ...process.env, GEN_BG_OUT: out } });
|
||||
if (r.status !== 0) {
|
||||
return { err: `退出码 ${r.status}:${(r.stderr || '').trim().split('\n').slice(-2).join(' / ')}` };
|
||||
}
|
||||
return { text: bytes(out).toString('utf8') };
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
const used = new Set();
|
||||
const walk = dir => {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
@ -245,6 +268,52 @@ check('新组件未使用未映射色族', unmapped.length === 0, unmapped.join(
|
||||
check('页面底没有被写进半透明清单', [...PAGE_BASE].every(b => !gen.includes(`.${b} {`)));
|
||||
check('清单不是空跑(至少扫到 10 个类)', used.size >= 10, `实际 ${used.size}`);
|
||||
|
||||
/*
|
||||
* 17b) ★★ 生成物必须与**当前源码**逐字节一致 —— 把上面那条判据**两个方向都补上**。
|
||||
*
|
||||
* pi 2026-09-15:上面那条 `missing.length === 0` **只判一个方向**
|
||||
* ("源码里用到的类必须都在生成文件里");
|
||||
* **反向没有判据**:生成文件里**多出来的、源码已不再用**的类,谁也看不见。
|
||||
* 于是有这样一条路径:
|
||||
* ① 有人删掉最后一处 `bg-blue-50` 用法、**没跑生成器**;
|
||||
* ② 生成文件里留下不再需要的 `.bg-blue-50` 规则 ⇒ 上面那条**仍然绿**(它只看漏没漏);
|
||||
* ③ `install.sh --check` 跑 `gen:bg` ⇒ **原地删掉那条规则** ⇒
|
||||
* 一个自称干跑的命令**改了一个被跟踪文件**,还**擦掉了"有人忘了跑生成器"的证据**。
|
||||
*
|
||||
* ⇒ 判据:**重新生成到临时路径,与真身逐字节比**。它同时给出三件事:
|
||||
* 反向那一半(多出来的过期规则)、"幂等"从**观察**变成**被检查的性质**、
|
||||
* 以及"忘了跑生成器"**会被判红而不是被静默修好**。
|
||||
*
|
||||
* ⚠️ 只**读到**临时路径(`GEN_BG_OUT`),绝不写工作树里那个文件 ——
|
||||
* 一条"判同步"的判据如果自己去同步,就永远绿。
|
||||
* ⚠️ 早先我在这里用了裸 `readFileSync`,**被本仓自己的 `criteria-hygiene` 抓红**了
|
||||
* ("判据目录里不得裸用 readFileSync")。这是它**正确工作**的一例:
|
||||
* 我新写判据时没走 `test/lib/read.mjs` 的具名入口。现在读**二进制原文**用 `bytes()`
|
||||
* (这条判的是"字节是否一致",不是"代码里有什么"),所以 `bytes()` 正是对的入口。
|
||||
*/
|
||||
const freshGen = generateFresh();
|
||||
if (freshGen.err) {
|
||||
check('生成器能跑起来(这条判据的前提)', false, freshGen.err);
|
||||
} else {
|
||||
const fresh = freshGen.text;
|
||||
if (fresh === gen) {
|
||||
check('生成物与当前源码逐字节一致(反向:也没有多出来的过期规则)', true);
|
||||
} else {
|
||||
const fl = fresh.split('\n');
|
||||
const gl = gen.split('\n');
|
||||
let i = 0;
|
||||
while (i < Math.min(fl.length, gl.length) && fl[i] === gl[i]) i += 1;
|
||||
const inFile = gl.filter(l => /^html\[data-bg='on'\] \./.test(l)).length;
|
||||
const inFresh = fl.filter(l => /^html\[data-bg='on'\] \./.test(l)).length;
|
||||
check('生成物与当前源码逐字节一致(反向:也没有多出来的过期规则)', false,
|
||||
`真身 ${inFile} 条规则 vs 重新生成 ${inFresh} 条;首个不同在第 ${i + 1} 行:\n`
|
||||
+ ` 真身:${(gl[i] ?? '(无)').trim().slice(0, 90)}\n`
|
||||
+ ` 新生成:${(fl[i] ?? '(无)').trim().slice(0, 90)}\n`
|
||||
+ ' 修法:跑 `npm run gen:bg` 并**把改动提交**(别让它在某次干跑里被静默改掉)。\n'
|
||||
+ ' 注意这条判的是**两个方向**:漏了规则会红,**多了过期规则也会红**。');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ★ 生成产物"能被 CSS 解析器吃下"还是"只有浏览器能发现"(2026-09-14,pi 提议)。
|
||||
*
|
||||
|
||||
@ -63,7 +63,7 @@ const SUITE = [
|
||||
['test/nav-merge.test.mjs', [], 8],
|
||||
['test/animation-audit.test.mjs', [], 4], // 动画全量盘点:死动画/过宽作用域/弹层接线/reduced-motion
|
||||
['test/theme.test.mjs', [], 30],
|
||||
['test/background.test.mjs', [], 42],
|
||||
['test/background.test.mjs', [], 43],
|
||||
['test/cross-client-theme.test.mjs', [], 15],
|
||||
// 预设的**行为**判据:每一档都真的画得出来(能真跑,不需要设备 ⇒ 不进 static 欠账)。
|
||||
// 与 appearance-defaults 那条「清单 id/顺序相等」配对:值判据管清单,行为判据管渲染器。
|
||||
|
||||
@ -171,6 +171,17 @@ fi
|
||||
# 实测它内容幂等(`git status` 干净),所以**不改内容**;但"写文件这个动作"仍在,
|
||||
# 所以"干跑只读"这句话**准确的说法是"不改任何被跟踪文件的内容、不碰 dist"**,
|
||||
# 不是"一个字节都不写"。我看不出的部分不写死。
|
||||
#
|
||||
# ★ 而 pi 2026-09-15 指出:上面那句"不改任何被跟踪文件的内容"**当时是假的** ——
|
||||
# `npm run gen:bg` 会**原地重写** `src/background-takeover.generated.css`(**已跟踪**)。
|
||||
# 他说得更准的一点是:**"幂等"只在一个方向上被保证** —— 守它的判据只判
|
||||
# "源码里用到的类必须都在生成文件里",**反向(生成物里多出来的过期规则)没有判据**。
|
||||
# 于是:有人删掉最后一处用法、忘了跑生成器 ⇒ 生成物留下一条过期规则 ⇒ 那条判据**仍绿**
|
||||
# ⇒ 干跑一跑 `gen:bg` **原地删掉它** ⇒ 一个自称干跑的命令改了被跟踪文件,
|
||||
# 还**擦掉了"有人忘了跑生成器"的证据**。(与 `dist` 那条形状完全相同。)
|
||||
# ⇒ 现在:干跑**不再原地生成**,而是生成到临时路径,并**把漂移报出来**(不替人修 ——
|
||||
# 干跑去修就等于把证据擦掉);判据那一侧已在 `background.test.mjs` 补了**双向**的
|
||||
# "生成物与源码逐字节一致"。
|
||||
if [[ $CHECK_ONLY -eq 1 ]]; then
|
||||
npm_rc=0
|
||||
# 产物写到临时目录:干跑不该动 dist(否则会静默刷掉 build-stamp 的红)
|
||||
@ -179,16 +190,23 @@ if [[ $CHECK_ONLY -eq 1 ]]; then
|
||||
cd "$REPO/client/electron" \
|
||||
&& npm run typecheck \
|
||||
&& AGENTMAIL_CRITERIA_PHASE=install npm test \
|
||||
&& npm run gen:bg \
|
||||
&& GEN_BG_OUT="$CHECK_DIST/generated.css" node scripts/gen-background-takeover.mjs \
|
||||
&& npx vite build --outDir "$CHECK_DIST" --emptyOutDir \
|
||||
&& BUILD_INFO_OUT="$CHECK_DIST/BUILD_INFO.json" node scripts/build-info.mjs
|
||||
) || npm_rc=$?
|
||||
# ★ 生成物与真身不一致 ⇒ **报告,不修**(干跑替人修就把"有人忘了跑生成器"擦掉了)
|
||||
if [[ -f "$CHECK_DIST/generated.css" ]] \
|
||||
&& ! cmp -s "$CHECK_DIST/generated.css" "$REPO/client/electron/src/background-takeover.generated.css"; then
|
||||
echo " [WARN] background-takeover.generated.css 与**当前源码**不一致(干跑**没有**替你改):"
|
||||
echo " 修法:cd client/electron && npm run gen:bg,然后**把改动提交**。"
|
||||
echo " (判据 test/background.test.mjs 会就这件事判红 —— 干跑这里只提示,不替你做。)"
|
||||
fi
|
||||
rm -rf "$CHECK_DIST"
|
||||
if [[ $npm_rc -ne 0 ]]; then
|
||||
echo " [FAIL] 前端门禁(typecheck / 判据 / build)退出码 ${npm_rc} —— 详见上面的红" >&2
|
||||
CHECK_GATE_RC="$npm_rc"
|
||||
else
|
||||
echo " [ OK ] 前端门禁通过(构建产物落在临时目录,**没有动 dist**)"
|
||||
echo " [ OK ] 前端门禁通过(构建产物与生成物都落在临时目录,**没有动 dist、也没动被跟踪的生成物**)"
|
||||
fi
|
||||
else
|
||||
( cd "$REPO/client/electron" && npm run typecheck && AGENTMAIL_CRITERIA_PHASE=install npm test && npm run build )
|
||||
|
||||
Reference in New Issue
Block a user