build(tools): ds-compare.mjs 还原保真目检编排 + gitignore 对比图
一键流程:对某屏按 A/B/C 截原型基准 + 与 Flutter golden 用 montage 并排 → design/_compare/<prefix>_<theme>.png 人工目检。封装 design-distill skill 的 shoot-prototype + montage。对比图可重生,已 gitignore。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
This commit is contained in:
@@ -69,3 +69,6 @@ backend/gencode
|
||||
|
||||
# brainstorming visual companion
|
||||
.superpowers/
|
||||
|
||||
# design-distill 保真目检对比图(可重生,见 tools/ds-compare.mjs)
|
||||
design/_compare/
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* ds-compare.mjs — 还原保真目检编排(原型基准 ↔ Flutter golden 并排)
|
||||
*
|
||||
* design-distill 阶段4「保真闸」的项目侧一键流程:对某屏,按 A/B/C 三主题
|
||||
* 截原型基准图,再与已生成的 Flutter golden 用 montage 并排,产出对比图供人工目检。
|
||||
* 跨渲染器(Chromium↔Skia)不跑严格 pixelmatch;回归靠 Flutter golden 自比(flutter test)。
|
||||
*
|
||||
* 前置:先生成 Flutter golden:
|
||||
* cd client && flutter test --update-goldens test/golden/<screen>_golden_test.dart
|
||||
*
|
||||
* 用法(从仓库根运行):
|
||||
* node tools/ds-compare.mjs --html .superpowers/prototype/screens/inventory.html \
|
||||
* --prefix inventory_list [--themes a,b,c] [--width 1280] [--height 860] [--wait-for .app]
|
||||
*
|
||||
* 输出:design/_compare/<prefix>_<theme>.png(gitignore,可重生)。
|
||||
* 依赖:design-distill skill(shoot-prototype.mjs / montage.mjs),默认 ~/.claude/skills/design-distill,可用 DD_SKILL 覆盖。
|
||||
*/
|
||||
import { parseArgs } from 'node:util';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdirSync, existsSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
html: { type: 'string' },
|
||||
prefix: { type: 'string' },
|
||||
themes: { type: 'string', default: 'a,b,c' },
|
||||
width: { type: 'string', default: '1280' },
|
||||
height: { type: 'string', default: '860' },
|
||||
'wait-for': { type: 'string', default: '.app' },
|
||||
'goldens-dir': { type: 'string', default: 'client/test/golden/goldens' },
|
||||
help: { type: 'boolean', default: false },
|
||||
},
|
||||
});
|
||||
|
||||
if (values.help || !values.html || !values.prefix) {
|
||||
console.log(`用法: node tools/ds-compare.mjs --html <原型.html> --prefix <golden前缀> [--themes a,b,c --width 1280 --height 860 --wait-for .app]`);
|
||||
process.exit(values.help ? 0 : 1);
|
||||
}
|
||||
|
||||
const SKILL = process.env.DD_SKILL || join(homedir(), '.claude/skills/design-distill');
|
||||
const shoot = join(SKILL, 'tools/shoot-prototype.mjs');
|
||||
const montage = join(SKILL, 'tools/montage.mjs');
|
||||
for (const t of [shoot, montage]) {
|
||||
if (!existsSync(t)) { console.error(`[ds-compare] 找不到 skill 工具: ${t}(设 DD_SKILL 覆盖)`); process.exit(2); }
|
||||
}
|
||||
|
||||
const outDir = 'design/_compare';
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const themes = values.themes.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
|
||||
for (const theme of themes) {
|
||||
const base = join(outDir, `${values.prefix}_proto_${theme}.png`);
|
||||
const golden = resolve(values['goldens-dir'], `${values.prefix}_${theme}.png`);
|
||||
const out = join(outDir, `${values.prefix}_${theme}.png`);
|
||||
|
||||
execFileSync('node', [shoot, values.html, base,
|
||||
'--width', values.width, '--height', values.height,
|
||||
'--theme', theme, '--lang', 'zh', '--wait-for', values['wait-for']],
|
||||
{ stdio: 'inherit' });
|
||||
|
||||
if (!existsSync(golden)) {
|
||||
console.error(`[ds-compare] ⚠ 缺 Flutter golden: ${golden} —— 先跑 flutter test --update-goldens。跳过 ${theme} 的并排。`);
|
||||
continue;
|
||||
}
|
||||
execFileSync('node', [montage, out, base, golden, '--dir', 'v'], { stdio: 'inherit' });
|
||||
console.log(`[ds-compare] ✅ ${theme}: ${out}(上=原型 下=Flutter,人工目检)`);
|
||||
}
|
||||
Reference in New Issue
Block a user