Files
aurora-admin/tools/verify-brand-mark.mjs
aurora-admin b59247231a
Regression / regression (push) Canceled after 0s
feat(安全+品牌): 对外产物发布脱敏(3注入点/2465文件0泄露) + Kole Cup 定稿
安全: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)。
2026-09-23 07:31:06 +08:00

128 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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} 条断言)`);