Regression / regression (push) Canceled after 0s
安全:CHANGELOG.md 是仓库内部变更流水(含服务器目录、镜像回滚标签、内网网段、
部署时序、AI 工作流用语),此前被 build-site.ps1 / precompute.mjs / build-mobile.mjs
原样注入站点,而站点是公网可下载的静态文件 —— 抓一次 /site/m/changelog.html
即可拿到内网地址段与服务器目录布局。
- 新增 tools/lib/redact-publish.mjs(零依赖,构建期过滤,不改 CHANGELOG.md,
内部可追溯性完整保留)
- 接入 precompute.mjs(PC data.json/changelog.json)与 build-mobile.mjs
(移动端更新日志页);build-site.ps1 不碰(ASCII-only 铁律)
- 只处理 changelog 字段:components[].sources 是规范实现源码,逐字保真
(详情页代码区主动高亮注释,剥注释会破坏该功能)
- 实测消除:/opt/aurora-admin.prev-*、kole-ui-showcase:pre-*、docker compose、
192.168.5.7、16 位产物指纹、1531/1531、并发会话/本会话/派子 agent
- settings.html 演示占位 IP 192.168.5.0/24(= 真实网段)改为 RFC 5737 的 192.0.2.0/24
品牌:Kole Cup 饮料杯标记定稿(几何 K → 圆角杯盖 + 杯身负空间 K,无吸管),
brand-mark.json 升 schemaVersion 3(paths 支持 { d, evenodd }),
verify:brand 增至 25 条(新增 B9b:负空间必须带 fill-rule)。
验证:发布集 2465 文件全量扫描 0 泄露;PC 回归 100%(1464/1464) ·
移动端 100%(807/807);门禁品牌 25 / 隔离 31 / 移动文档 12 / 示例 9 / 版本 40 / i18n 17 全绿;
已按 AGENTS §九 发布公网,2461/2461 逐字节一致,五项验收全过。
已知未处理(既有缺口,ROADMAP S7-P28 已记录):data.mobile.json 的
meta.generated 为墙上时钟,会让 CI 的「生成物可复现」断言在重跑构建后永远非空;
该 CI 流水线本身亦从未通过(无 runner)。
128 lines
7.3 KiB
JavaScript
128 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* verify-brand-mark.mjs — 项目品牌图标静态门禁。
|
||
* 组件 Icon registry 是另一套系统,本脚本只检查 favicon 与文档站品牌标记。
|
||
*/
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { collectPublishSet } from './lib/publish-set.mjs';
|
||
import {
|
||
generatedData,
|
||
readBrandSpec,
|
||
renderFaviconSvg,
|
||
renderMonoSvg,
|
||
validateBrandSpec,
|
||
BRAND_ASSET_REL,
|
||
BRAND_DATA_REL,
|
||
BRAND_MONO_REL,
|
||
BRAND_SPEC_REL,
|
||
} from './lib/brand-mark.mjs';
|
||
|
||
const ROOT = process.cwd();
|
||
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/^\uFEFF/, '');
|
||
const exists = (rel) => fs.existsSync(path.join(ROOT, rel));
|
||
let pass = 0;
|
||
const failures = [];
|
||
function check(ok, label, detail = '') {
|
||
if (ok) {
|
||
pass++;
|
||
console.log(` PASS ${label}${detail ? ` — ${detail}` : ''}`);
|
||
} else {
|
||
failures.push(`${label}${detail ? ` — ${detail}` : ''}`);
|
||
console.log(` FAIL ${label}${detail ? ` — ${detail}` : ''}`);
|
||
}
|
||
}
|
||
|
||
console.log('Kole UI 项目品牌图标门禁');
|
||
check(exists(BRAND_SPEC_REL), 'B1 品牌规格存在', BRAND_SPEC_REL);
|
||
check(exists(BRAND_ASSET_REL), 'B2 favicon SVG 存在', BRAND_ASSET_REL);
|
||
check(exists(BRAND_MONO_REL), 'B3 单色 SVG 存在', BRAND_MONO_REL);
|
||
check(exists(BRAND_DATA_REL), 'B4 生成数据存在', BRAND_DATA_REL);
|
||
|
||
const spec = readBrandSpec(ROOT);
|
||
const specErrors = validateBrandSpec(spec);
|
||
check(specErrors.length === 0, 'B5 品牌规格结构有效', specErrors.join(';'));
|
||
|
||
const expectedFavicon = renderFaviconSvg(spec) + '\n';
|
||
const expectedMono = renderMonoSvg(spec) + '\n';
|
||
const expectedData = JSON.stringify(generatedData(spec), null, 2) + '\n';
|
||
const favicon = exists(BRAND_ASSET_REL) ? read(BRAND_ASSET_REL) : '';
|
||
const mono = exists(BRAND_MONO_REL) ? read(BRAND_MONO_REL) : '';
|
||
const data = exists(BRAND_DATA_REL) ? read(BRAND_DATA_REL) : '';
|
||
check(favicon === expectedFavicon, 'B6 favicon 是规格生成物');
|
||
check(mono === expectedMono, 'B7 单色标记是规格生成物');
|
||
check(data === expectedData, 'B8 generated JSON 是规格生成物');
|
||
check(!/<image\b|(?:href|src)=["']https?:\/\//i.test(favicon + mono), 'B9 品牌 SVG 不含外部资源');
|
||
/* 负空间断言:品牌图形靠 evenodd 挖空 K(杯身实心块 + K 反向子路径)。
|
||
若生成期漏掉 fill-rule,K 会被填成实心 —— 图标仍然"渲染成功",
|
||
只是变成一块实心杯子,**不会报任何错**。所以这条必须卡住。 */
|
||
const negativePaths = (spec.geometry.paths || []).filter((p) => p && typeof p === 'object' && p.evenodd);
|
||
const subpathCount = (d) => (String(d).match(/M/gi) || []).length;
|
||
check(
|
||
negativePaths.length === 0
|
||
|| (favicon.includes('fill-rule="evenodd"') && mono.includes('fill-rule="evenodd"')),
|
||
'B9b 负空间路径带 fill-rule="evenodd"(否则 K 会被填实)',
|
||
negativePaths.length ? `${negativePaths.length} 条负空间路径 · 子路径 ${negativePaths.map((p) => subpathCount(p.d)).join('+')}` : '无负空间路径'
|
||
);
|
||
check((favicon.match(/<path\b/g) || []).length === spec.geometry.paths.length, 'B10 favicon 完整包含全部几何路径', `${spec.geometry.paths.length} 条`);
|
||
check((mono.match(/<path\b/g) || []).length === spec.geometry.paths.length, 'B11 单色标记完整包含全部几何路径', `${spec.geometry.paths.length} 条`);
|
||
const faviconPaint = spec.geometry.mode === 'fill' ? `fill="${spec.favicon.foreground}"` : `stroke="${spec.favicon.foreground}"`;
|
||
check(favicon.includes(`fill="${spec.favicon.background}"`) && favicon.includes(faviconPaint), 'B12 favicon 颜色固定');
|
||
const monoPaint = spec.geometry.mode === 'fill' ? 'fill="#000000"' : 'stroke="#000000"';
|
||
check(mono.includes(monoPaint) && !mono.includes(`fill="${spec.favicon.background}"`), 'B13 单色标记透明背景');
|
||
|
||
const pc = read('site/index.html');
|
||
const mobileBuilder = read('tools/build-mobile.mjs');
|
||
const mobileStyle = read('site/m/style.css');
|
||
check(pc.includes('href="assets/kole-mark.svg"'), 'B14 PC 使用 favicon 资产');
|
||
check(pc.includes('class="logo-mark"') && pc.includes('data-brand-mark="mono"'), 'B15 PC 顶栏使用单色品牌标记');
|
||
check(!pc.includes('M7 5.5V18.5'), 'B16 PC 不再内联旧几何路径');
|
||
check(mobileBuilder.includes("readBrandSpec(ROOT)"), 'B17 移动端构建读取品牌规格');
|
||
check(!mobileBuilder.includes('const BRAND_PATHS ='), 'B18 移动端构建不重复维护几何路径');
|
||
check(mobileStyle.includes('data-brand-mark="mono"') || mobileStyle.includes('kole-mark-mono.svg'), 'B19 移动端样式声明单色品牌资产');
|
||
check((pc.match(/theme-color/g) || []).length >= 2, 'B20 PC theme-color 保留亮暗两态');
|
||
|
||
const mobileIndex = JSON.parse(read('.design_library/kole-ui-mobile/components/index.json'));
|
||
const mobilePages = [
|
||
...['index', 'guide', 'design', 'faq', 'changelog', 'platform'].map((name) => `site/m/${name}.html`),
|
||
...mobileIndex.components.map((component) => `site/m/component/${component.slug}.html`),
|
||
];
|
||
const pageProblems = [];
|
||
for (const rel of mobilePages) {
|
||
if (!exists(rel)) { pageProblems.push(`${rel}: 文件缺失`); continue; }
|
||
const html = read(rel);
|
||
const head = (html.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i) || [])[1] || '';
|
||
const icons = [...head.matchAll(/<link\b[^>]*rel="icon"[^>]*>/g)];
|
||
const href = icons.length === 1 ? (icons[0][0].match(/\bhref="([^"]+)"/) || [])[1] : '';
|
||
if (!href || path.posix.normalize(path.posix.join(path.posix.dirname(rel), href)) !== BRAND_ASSET_REL) {
|
||
pageProblems.push(`${rel}: favicon 未指向统一资产`);
|
||
}
|
||
const header = (html.match(/<header class="m-top">([\s\S]*?)<\/header>/) || [])[1] || '';
|
||
if (!header.includes('<span class="m-logo-mark" data-brand-mark="mono" aria-hidden="true"></span>')) {
|
||
pageProblems.push(`${rel}: 顶栏标记或装饰性语义缺失`);
|
||
}
|
||
if (header.includes('M7 5.5V18.5') || (head.match(/name="theme-color"/g) || []).length !== 2) {
|
||
pageProblems.push(`${rel}: 旧几何残留或 theme-color 缺失`);
|
||
}
|
||
}
|
||
check(pageProblems.length === 0, 'B21 移动端全部生成页使用统一品牌资产', pageProblems.slice(0, 5).join(';') || `${mobilePages.length} 页`);
|
||
const pcStyle = read('site/style.css');
|
||
const pcMask = (pcStyle.match(/\.logo-mark\[data-brand-mark="mono"\]::before\s*\{([^}]+)\}/) || [])[1] || '';
|
||
const mobileMask = (mobileStyle.match(/\.m-logo-mark\[data-brand-mark="mono"\]::before\s*\{([^}]+)\}/) || [])[1] || '';
|
||
check(pcMask.includes('background: currentColor') && pcMask.includes("url('assets/kole-mark-mono.svg')")
|
||
&& mobileMask.includes('background: currentColor') && mobileMask.includes("url('../assets/kole-mark-mono.svg')"),
|
||
'B22 两端 mask 路径同源且继承 currentColor');
|
||
const published = new Set(collectPublishSet(ROOT).kept);
|
||
check([BRAND_ASSET_REL, BRAND_MONO_REL, BRAND_DATA_REL].every((rel) => published.has(rel)), 'B23 发布集包含全部品牌产物');
|
||
const scripts = JSON.parse(read('package.json')).scripts;
|
||
check(scripts['build:brand'] === 'node tools/build-brand-mark.mjs'
|
||
&& scripts['verify:brand'] === 'node tools/verify-brand-mark.mjs'
|
||
&& scripts.build.startsWith('node tools/build-brand-mark.mjs && '), 'B24 品牌构建与门禁入口完整');
|
||
|
||
if (failures.length) {
|
||
console.error(`\n[FAIL] ${failures.length} 条品牌图标断言失败(通过 ${pass} 条)`);
|
||
failures.forEach((failure) => console.error(' - ' + failure));
|
||
process.exit(1);
|
||
}
|
||
console.log(`\n[OK] 品牌图标门禁全部通过(${pass} 条断言)`);
|