ci: 设计真相源检查挂 CI(PR + main push 自动跑四道闸)
Design Source Checks / design-source (push) Successful in 16s
Design Source Checks / design-source (push) Successful in 16s
- 新增 tools/check-l1-sync.mjs:L1 同源四道——tokens 快照逐字节、icons 两端同集同内容、官网 token 值对齐原型主题 A、web 硬编码 hex 扫描 (白名单 #fff/#1677ff,ds-allow 可豁免);注入测试验证红绿行为 - 新增 .gitea/workflows/checks.yml:pull_request + push(main) 触发, 串跑 check-ds 12 道 / check-l1-sync / check_ds_code / codegen 新鲜度 (regen→dart format→零 diff,消除重跑假 diff 问题) - 修复 web/checkout.njk 对公转账图标底色硬编码 #8A94A6 → var(--faint) - CLAUDE.md「设计真相源分层」登记 CI 闸与本地等价命令 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
name: Design Source Checks
|
||||
|
||||
# 设计真相源检查(治理规则见 CLAUDE.md「设计真相源分层」):
|
||||
# PR 与合并 main 时自动跑四道闸,全部纯静态检查(node + dart format),
|
||||
# 不跑 golden/fidelity(重型像素闸维持「定期体检」定位,本地手动跑)。
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: checks-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
design-source:
|
||||
runs-on: mac
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Provision (idempotent — auto-runs if host not initialized)
|
||||
run: sh scripts/ci/provision-mac.sh
|
||||
|
||||
- name: 原型设计系统闸(check-ds 12 道)
|
||||
run: node .superpowers/prototype/tools/check-ds.mjs
|
||||
|
||||
- name: L1 真相源同源闸(tokens 快照 / icons / 官网 token 值 / web hex)
|
||||
run: node tools/check-l1-sync.mjs
|
||||
|
||||
- name: Flutter 颜色单源闸(check_ds_code)
|
||||
run: node client/tool/check_ds_code.mjs
|
||||
|
||||
- name: token codegen 新鲜度(regen → format → 与入库产物零 diff)
|
||||
run: |
|
||||
cd client
|
||||
node tool/gen_tokens.mjs
|
||||
dart format lib/core/theme/app_tokens.g.dart lib/core/theme/app_dims.g.dart lib/core/theme/app_chrome.g.dart
|
||||
git diff --exit-code -- lib/core/theme/app_tokens.g.dart lib/core/theme/app_dims.g.dart lib/core/theme/app_chrome.g.dart
|
||||
@@ -265,6 +265,7 @@ cd client && flutter test
|
||||
- `代码先行`:无原型屏,golden 为唯一像素基准。
|
||||
- **L3 新屏/整屏改版**:design-first 不变(原型 → CONTRACT → 实现 → fidelity 验收),建成后入 `同步` 态。
|
||||
- **漂移体检**:不逐提交强制同步;大版本/整屏改版前跑 `node tools/fidelity.mjs`,按报告逐屏决定「重新对齐原型」或「降级快照」,结果回填 CONTRACT。
|
||||
- **CI 闸**(`.gitea/workflows/checks.yml`,PR + main push 自动跑):① `check-ds.mjs` 原型 12 道;② `tools/check-l1-sync.mjs` L1 同源四道(tokens 快照逐字节 / icons 两端同集同内容 / 官网 token 值对齐原型主题 A / web 硬编码 hex,白名单 #fff、#1677ff,`ds-allow` 可豁免);③ `client/tool/check_ds_code.mjs` Flutter 颜色单源;④ codegen 新鲜度(regen→`dart format`→与入库产物零 diff)。本地可直接跑同名命令。
|
||||
|
||||
### 表格与列头筛选(ds 真相源)
|
||||
- 列表屏统一 `DsTable`(`widgets/ds/ds_table.dart`:toolbar+表格+pager 连成一卡),列定义 `DsColumn`,可筛选列走其列头漏斗(`filtered` 态高亮),可隐藏列走列设置菜单
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
// tools/check-l1-sync.mjs — L1 设计真相源同源闸(四道,纯 Node 零依赖)
|
||||
//
|
||||
// 治理依据:CLAUDE.md「设计真相源分层」L1 层——tokens/atoms/icons 以
|
||||
// .superpowers/prototype/ 为单一真源,client 与 web 只能是它的同步副本。
|
||||
// CI(.gitea/workflows/checks.yml,PR + main push)与本地均可直接跑:
|
||||
// node tools/check-l1-sync.mjs
|
||||
//
|
||||
// 四道检查(任一违规 exit 1):
|
||||
// ① tokens 快照同源 原型 tokens.css ≡ client token_source/tokens.css(逐字节)
|
||||
// ② icons 同源 原型 icons.js 与 web icons.njk 的 symbol 同集且同内容
|
||||
// ③ 官网 token 值同源 web/assets/tokens.css 与原型(:root+主题A) 同名 token 值一致
|
||||
// (官网独有 --mk-*/兼容别名层、App 外壳专属 token 不算违规)
|
||||
// ④ web 硬编码色扫描 web/**/*.{njk,css,js} 禁裸 hex;白名单 #fff/#ffffff/#1677ff
|
||||
// (对齐原型 check-ds 豁免口径);行内 `ds-allow` 注释可豁免;
|
||||
// 排除 web/assets/tokens.css(token 定义位)与 web/dist/。
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
const read = (p) => readFileSync(join(ROOT, p), 'utf8');
|
||||
const problems = [];
|
||||
|
||||
// ── ① tokens 快照同源 ──────────────────────────────────────
|
||||
{
|
||||
const proto = read('.superpowers/prototype/tokens.css');
|
||||
const snap = read('client/lib/core/theme/token_source/tokens.css');
|
||||
if (proto !== snap) {
|
||||
problems.push(
|
||||
'[tokens快照] .superpowers/prototype/tokens.css 与 ' +
|
||||
'client/lib/core/theme/token_source/tokens.css 不一致 —— ' +
|
||||
'改原型后需拷贝快照并重跑 client/lib/core/theme/tool/gen_tokens.mjs',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ② icons 同源 ───────────────────────────────────────────
|
||||
{
|
||||
const symbols = (s) => {
|
||||
const map = {};
|
||||
for (const [, id, body] of s.matchAll(/<symbol id="([^"]+)"[^>]*>(.*?)<\/symbol>/gs)) {
|
||||
map[id] = body.trim();
|
||||
}
|
||||
return map;
|
||||
};
|
||||
const a = symbols(read('.superpowers/prototype/screens/icons.js'));
|
||||
const b = symbols(read('web/_includes/icons.njk'));
|
||||
for (const id of Object.keys(a)) {
|
||||
if (!(id in b)) problems.push(`[icons] symbol "${id}" 仅原型有,web/_includes/icons.njk 缺 —— 改原型后同步官网`);
|
||||
}
|
||||
for (const id of Object.keys(b)) {
|
||||
if (!(id in a)) problems.push(`[icons] symbol "${id}" 仅官网有 —— 图标必须先登记原型 icons.js 再同步`);
|
||||
}
|
||||
for (const id of Object.keys(a)) {
|
||||
if (id in b && a[id] !== b[id]) problems.push(`[icons] symbol "${id}" 两端内容不一致`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ③ 官网 token 值同源 ────────────────────────────────────
|
||||
{
|
||||
const vars = (css) =>
|
||||
Object.fromEntries(
|
||||
[...css.matchAll(/(--[\w-]+)\s*:\s*([^;}]+)[;}]/g)].map(([, k, v]) => [k, v.replace(/\s+/g, '')]),
|
||||
);
|
||||
const proto = read('.superpowers/prototype/tokens.css');
|
||||
const root = proto.match(/:root\s*{([^}]*)}/s)?.[1] ?? '';
|
||||
const themeA = proto.match(/\[data-theme="a"\]\s*{([^}]*)}/s)?.[1] ?? '';
|
||||
const pv = { ...vars(root), ...vars(themeA) };
|
||||
const wv = vars(read('web/assets/tokens.css'));
|
||||
for (const [k, v] of Object.entries(pv)) {
|
||||
if (k in wv && wv[k] !== v) {
|
||||
problems.push(`[web-token] ${k} 值漂移:原型=${v} 官网=${wv[k]} —— web/assets/tokens.css 需同步原型主题 A`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ④ web 硬编码色扫描 ─────────────────────────────────────
|
||||
{
|
||||
const ALLOW_HEX = new Set(['fff', 'ffffff', '1677ff']); // 白 + 支付宝品牌蓝(同原型 check-ds 口径)
|
||||
const SKIP = new Set(['web/assets/tokens.css']); // token 定义位
|
||||
const files = [];
|
||||
const walk = (dir) => {
|
||||
for (const name of readdirSync(join(ROOT, dir))) {
|
||||
const rel = `${dir}/${name}`;
|
||||
if (rel === 'web/dist' || name === 'node_modules') continue;
|
||||
const st = statSync(join(ROOT, rel));
|
||||
if (st.isDirectory()) walk(rel);
|
||||
else if (/\.(njk|css|js)$/.test(name) && !SKIP.has(rel)) files.push(rel);
|
||||
}
|
||||
};
|
||||
walk('web');
|
||||
for (const f of files) {
|
||||
const lines = read(f).split('\n');
|
||||
lines.forEach((line, i) => {
|
||||
if (line.includes('ds-allow')) return;
|
||||
for (const [hex] of line.matchAll(/#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g)) {
|
||||
const v = hex.slice(1).toLowerCase();
|
||||
if (!ALLOW_HEX.has(v)) problems.push(`[web-hex] ${f}:${i + 1} 硬编码颜色 ${hex} —— 改用 var(--token)(web/assets/tokens.css),确需保留加 ds-allow 注释`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 汇总 ───────────────────────────────────────────────────
|
||||
const line = '='.repeat(60);
|
||||
if (problems.length) {
|
||||
console.error(line);
|
||||
console.error(`✗ L1 真相源同源闸未过(${problems.length} 处):`);
|
||||
for (const p of problems) console.error(' · ' + p);
|
||||
console.error(line);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(line);
|
||||
console.log('✓ 通过:L1 真相源同源(tokens 快照 / icons / 官网 token 值 / web 无硬编码色)');
|
||||
console.log(line);
|
||||
+1
-1
@@ -49,7 +49,7 @@ permalink: /checkout/
|
||||
<div class="radio"></div>
|
||||
</div>
|
||||
<div class="pm off">
|
||||
<div class="pic" style="background:#8A94A6"><svg class="ic"><use href="#i-ic11"/></svg></div>
|
||||
<div class="pic" style="background:var(--faint)"><svg class="ic"><use href="#i-ic11"/></svg></div>
|
||||
<div class="nm"><b>对公转账</b><span>需要合同与对公收款请<a href="mailto:{{ site.support.email }}">联系我们</a></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user