把项目从「手写 dist/」改成「wallpapers/ 是唯一真相来源,dist/ 由 pnpm build 生成」,
并补上配套的类型、门禁与抓取器。一次提交落地整条管线,因为拆开会留下不能构建的中间态。
- src/:运行时与模拟器源码(TS,strict),编译到 build/ 再拷进各分发
- tools/:build / dev / check-{syntax,paths,dist},以及抓取器与回归门禁 tools/checks/
(.scratch/ 下那批一次性脚本移入 tools/checks/ 并入库为长期门禁)
- wallpapers/:七档壁纸的源数据 + README.md(id/音频/预设的完整规范)
- docs/adr/0005-0008:构建管线与分发拓扑、模拟器契约、自包含 sim、每骨架资源布局
- .gitignore:排除 .scratch/ 的参考资料副本(上游 spine 整仓克隆 ~1.2 GB、
抓取侦查数据 ~680 MB)与调试转储;这些是本地调查材料,补偿会让仓库无法克隆
- 归一化 .gitignore/CONTEXT.md 行尾(工作区 CRLF、索引 LF 造成的整文件假 diff)
同时修掉三档卡住构建的未完工壁纸:
- kv45 的 meta.json 里 id 还是抓取期场景名 scene_main,经 downloader promote 正名为 kv45
- shajin / zhigengniao_juheye 的 meta.json 误用了骨架描述文件(name/spine/animations/pages)、
且都缺 preset.template.json;现按规范重建:骨架沉到 spines/<名>/(spine-ts 按 atlas 所在
目录解析贴图页)、补上元数据与单骨架预设,并清掉 zhigengniao 骨架里指向作者机的绝对路径
- 顺带 promote 已在 sources.yml 里的 kv46(月升之前,与兽共舞)
pnpm check 五道门全绿:10 个分发 / 7 档壁纸 / 141 处引用自包含。
80 lines
3.3 KiB
TypeScript
80 lines
3.3 KiB
TypeScript
// 视觉等价验收:当前 dist 与**重构前**的存档截图逐像素比对。
|
|
//
|
|
// 为什么需要它:`tools/checks/verify-sim-page.mts` 只证明"自包含包能跑",不证明"画面没变"。
|
|
// 内联、模块合成、URL 重写这些改动都可能悄悄改掉渲染结果。
|
|
//
|
|
// 判据:mean|diff| = 0 且 max|diff| = 0(逐像素相同)。
|
|
// 注意 `tools/capture.mjs` 打印的 "spine 层贡献" 是**同一轮里 ref 帧 vs 主帧**的差
|
|
// (即"spine 层占了多少像素"),不是与基线的比对——我一开始把它读成了后者,
|
|
// 白紧张了一场。真正的基线比对只有这里。
|
|
//
|
|
// 用法:
|
|
// node tools/serve.mjs 8190 --root dist/releases/collection-all
|
|
// node tools/capture.mjs visual-current --base http://127.0.0.1:8190
|
|
// node tools/checks/verify-visual-equivalence.mts
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
|
|
const BASELINE = "tools/shots/baseline-pre-refactor";
|
|
const CURRENT = process.argv[2] ?? "tools/shots/visual-current";
|
|
const combos: { preset: string; size: string }[] = [];
|
|
for (const preset of ["kv37", "xilian"]) {
|
|
for (const size of ["3440x1440", "1920x1080", "1080x1920"]) combos.push({ preset, size });
|
|
}
|
|
|
|
function diff(a, b) {
|
|
const out = execFileSync("node", ["tools/diff.mjs", a, b], { encoding: "utf8" });
|
|
return out.trim().split("\n").pop() ?? "";
|
|
}
|
|
|
|
/*
|
|
* 缺文件时**直接失败**,不跳过。
|
|
*
|
|
* 这里原来写的是"缺少文件就跳过",于是有一次当前截图目录被清掉之后,
|
|
* 它把 0 组比对报成了"0 组全部逐像素相同"——一句绿色的空话。
|
|
* 一个不检查前置条件的验证等于没有验证:先把该在的文件都确认在,再谈结论。
|
|
*/
|
|
const missing: string[] = [];
|
|
for (const { preset, size } of combos) {
|
|
for (const file of [`${BASELINE}/${preset}-${size}.png`, `${CURRENT}/${preset}-${size}.png`]) {
|
|
if (!existsSync(file)) missing.push(file);
|
|
}
|
|
}
|
|
if (missing.length > 0) {
|
|
console.error(`✗ 缺少 ${missing.length} 个截图,无法比对:`);
|
|
for (const file of missing.slice(0, 4)) console.error(` ${file}`);
|
|
if (missing.length > 4) console.error(` …还有 ${missing.length - 4} 个`);
|
|
console.error(`
|
|
基线在 ${BASELINE}(重构前存档,不能重新生成)。
|
|
当前截图需要现拍:
|
|
node tools/serve.mjs 8190 --root dist/releases/collection-all
|
|
node tools/capture.mjs visual-current --base http://127.0.0.1:8190
|
|
`);
|
|
process.exit(1);
|
|
}
|
|
|
|
let failed = 0;
|
|
let checked = 0;
|
|
for (const { preset, size } of combos) {
|
|
const a = `${BASELINE}/${preset}-${size}.png`;
|
|
const b = `${CURRENT}/${preset}-${size}.png`;
|
|
checked++;
|
|
const line = diff(a, b);
|
|
const identical = /mean\|diff\|=0\/255\s+max\|diff\|=0/.test(line);
|
|
if (!identical) failed++;
|
|
console.log(` ${identical ? "✓" : "✗"} ${preset.padEnd(7)} ${size.padEnd(11)} ${line}`);
|
|
}
|
|
|
|
// 走到这里 checked 必然等于 combos.length(上面缺文件就退了),这条是防回归的兜底。
|
|
if (checked !== combos.length) {
|
|
console.error(`✗ 只比对了 ${checked}/${combos.length} 组,结论不可信`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
failed === 0
|
|
? `\n视觉等价:${checked} 组全部与重构前逐像素相同`
|
|
: `\n视觉等价:${failed}/${checked} 组与基线不同`,
|
|
);
|
|
process.exit(failed === 0 ? 0 : 1);
|