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 绿。
96 lines
4.5 KiB
JavaScript
96 lines
4.5 KiB
JavaScript
/**
|
||
* 生成"背景开启时哪些浅色表面类要变半透明"的 CSS。
|
||
*
|
||
* # 为什么要生成而不是手写
|
||
*
|
||
* 手写清单必然烂:2026-09-13 用户报「很多界面是不透明的,导致壁纸露不出来」,
|
||
* 一查代码里在用的浅色表面类有 gray-100(24 处)、blue-50(17)、red-50(13)、
|
||
* gray-200(8)、green-50(8)… 而当时的接管清单只有 white / gray-50 / slate-100 ——
|
||
* 漏掉的那些全是实心的,正好盖住壁纸。
|
||
*
|
||
* 所以清单从**源码里实际出现的类**推导:出现什么就接管什么。新增一个
|
||
* `bg-purple-50` 而忘了同步,`test/background.test.mjs` 里那条判据会红。
|
||
*
|
||
* 只接管"表面段"(50/100/200):它们是面板底、提示条、卡片这类大面积浅色。
|
||
* 600/700 那一段是按钮与徽标(小控件、要读得清),**故意保持不透明** —— 见
|
||
* index.css 里 chrome-600/700 那段注释(实测可读性从 4.46:1 掉到 AA 以下)。
|
||
*/
|
||
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
||
import { join, dirname } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const SRC = join(HERE, '..', 'src');
|
||
/*
|
||
* ★ 输出路径可覆盖(`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)$/;
|
||
|
||
/**
|
||
* 页面底:**必须完全透明**(背景开启时),不能进这份"半透明"清单。
|
||
*
|
||
* 它们铺满整屏,给它们 0.3 的不透明度等于在壁纸上盖一层白纱;而 index.css 里
|
||
* 已经把它们设成 transparent —— 谁后加载谁生效,生成物在 index.css 之后引入,
|
||
* 所以漏了这条排除就会把"完全透明"悄悄改回"半透明"。
|
||
*/
|
||
const PAGE_BASE = new Set(['bg-gray-50', 'bg-slate-100']);
|
||
|
||
async function walk(dir) {
|
||
const out = [];
|
||
for (const e of await readdir(dir, { withFileTypes: true })) {
|
||
const p = join(dir, e.name);
|
||
if (e.isDirectory()) out.push(...(await walk(p)));
|
||
else if (/\.(tsx?|jsx?)$/.test(e.name)) out.push(p);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const found = new Set();
|
||
for (const file of await walk(SRC)) {
|
||
const text = await readFile(file, 'utf8');
|
||
for (const m of text.matchAll(/bg-[a-z]+-\d{2,3}/g)) {
|
||
if (SURFACE.test(m[0]) && !PAGE_BASE.has(m[0])) found.add(m[0]);
|
||
}
|
||
}
|
||
|
||
const classes = [...found].sort();
|
||
const body = classes
|
||
.map(
|
||
(cls) =>
|
||
`html[data-bg='on'] .${cls} {\n` +
|
||
` background-color: rgb(var(--c-${cls.slice(3)}) / var(--bg-glass-inner));\n` +
|
||
` backdrop-filter: none; /* 外层已经模糊过,嵌套层再糊一次会把图案糊成灰块 */\n` +
|
||
` -webkit-backdrop-filter: none;\n}`,
|
||
)
|
||
.join('\n\n');
|
||
|
||
await writeFile(
|
||
OUT,
|
||
/*
|
||
* ★ 头注释里**不许写出「星号紧接斜杠」的 glob 字面量**(2026-09-14 修)。
|
||
*
|
||
* 这里原先写的是「src」加「/」加两颗星加「/」加「.tsx」,而中间那对
|
||
* 「星号 + 斜杠」会把 CSS 注释**提前闭合**:剩下的尾巴(`.tsx 的实际用法)。`)
|
||
* 变成 CSS 正文,跟后面第一条规则的**选择器连在一起** —— 非法选择器 ⇒
|
||
* **那条规则被浏览器整条丢掉**(`.bg-amber-100` 在壁纸模式下不再变半透明),
|
||
* 而构建只在日志里给一条 `[WARNING] Unexpected "14"`,不报错、不影响构建。
|
||
* 所以这里只描述、不写 glob 字面量。
|
||
*/
|
||
`/* 由 scripts/gen-background-takeover.mjs 生成 —— 不要手改。\n` +
|
||
` * 背景开启时把源码里在用的浅色表面类改成半透明(清单来自 src 目录下全部 .tsx 的实际用法)。\n` +
|
||
` * 共 ${classes.length} 个类:${classes.join(', ')}\n */\n\n${body}\n`,
|
||
);
|
||
console.log(` 生成 ${classes.length} 个类 → ${OUT}`);
|