Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」, 那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」, 移动端站已是「K」;移动端文档站则完全没有 favicon)。 - 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边; 描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」 - 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark); 顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28)) - 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg) - 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留) - 移动端 7 个模板补 favicon(此前计数 0) 验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/ theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致; 两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。 ## 并行会话成果(本次一并入库) - 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具 + IconPreview 预览页 + ICON-SPEC.md 冻结规格 - 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁 - PC 组件:103 个大后台组件 / 组件11 批次 - uni-app:PC 端试点 + 移动端端实现 + 真实编译验证 ## 工程 - .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库) - CHANGELOG 补品牌标识条目 - ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
260 lines
11 KiB
JavaScript
260 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* verify-families.mjs - 族层端到端一致性验证
|
||
*
|
||
* 验证链路:family-model.mjs → 契约 JSON → build-site.ps1 → data.json → site/details
|
||
* 外加仓库级硬约束(frameworks 395 / 薄壳 79 / slug 全集不变)。
|
||
*
|
||
* 任一失败即退出码 1,输出 `FAIL: ...`。全部通过输出 `OK: ...` 与统计。
|
||
*
|
||
* 用法:node tools/verify-families.mjs
|
||
*/
|
||
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { FAMILIES, IMPLEMENTED_FAMILIES, memberIndex } from './lib/family-model.mjs';
|
||
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
const LIB = path.join(ROOT, '.design_library', 'kole-ui');
|
||
const COMPONENTS = path.join(LIB, 'components');
|
||
const FAMILIES_FILE = path.join(LIB, 'families.json');
|
||
const DATA_JSON = path.join(ROOT, 'site', 'data.json');
|
||
const DETAILS = path.join(ROOT, 'site', 'details');
|
||
const FRAMEWORKS = path.join(ROOT, 'frameworks');
|
||
const SHELLS = path.join(ROOT, 'site', 'components');
|
||
|
||
const COMPONENT_INDEX = JSON.parse(fs.readFileSync(path.join(COMPONENTS, 'index.json'), 'utf8'));
|
||
const EXPECT_COMPONENTS = COMPONENT_INDEX.components.length;
|
||
const EXPECT_FRAMEWORK_FILES = EXPECT_COMPONENTS * 5;
|
||
const KINDS = ['html', 'css', 'jsx', 'vue2', 'vue3'];
|
||
|
||
const failures = [];
|
||
const notes = [];
|
||
|
||
function fail(msg) {
|
||
failures.push(msg);
|
||
}
|
||
function check(cond, msg) {
|
||
if (!cond) fail(msg);
|
||
return cond;
|
||
}
|
||
function readJson(file) {
|
||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||
}
|
||
|
||
/* ---------- 0. 模型自检 ---------- */
|
||
let model = {};
|
||
try {
|
||
model = memberIndex();
|
||
} catch (e) {
|
||
fail('族模型自检失败:' + e.message);
|
||
}
|
||
|
||
const modelSlugs = Object.keys(model);
|
||
check(modelSlugs.length > 0, '族模型为空');
|
||
check(
|
||
modelSlugs.length === new Set(modelSlugs).size,
|
||
'族模型存在重复成员 slug'
|
||
);
|
||
|
||
/* 基座唯一性 + 成员参数必须落在参数面声明内 */
|
||
const declaredParams = new Map();
|
||
for (const f of FAMILIES) {
|
||
declaredParams.set(f.id, new Set((f.paramSurface || []).map((p) => p.name)));
|
||
const bases = f.members.filter((m) => m.role === 'base');
|
||
check(bases.length === 1, `族 ${f.id} 应恰好 1 个 base,实际 ${bases.length}`);
|
||
check(
|
||
bases.length === 1 && bases[0].slug === f.base,
|
||
`族 ${f.id} 的 base 字段 ${f.base} 与 role=base 的成员不一致`
|
||
);
|
||
for (const m of f.members) {
|
||
for (const k of Object.keys(m.params || {})) {
|
||
check(
|
||
declaredParams.get(f.id).has(k),
|
||
`族 ${f.id} 成员 ${m.slug} 的 ${k} 未在 paramSurface 声明`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---------- 1. families.json 与模型一致 ---------- */
|
||
if (!fs.existsSync(FAMILIES_FILE)) {
|
||
console.error('FAIL: families.json 不存在(先跑 node tools/gen-families.mjs)');
|
||
process.exit(1);
|
||
}
|
||
const famDoc = readJson(FAMILIES_FILE);
|
||
check(famDoc.totalFamilies === FAMILIES.length, `families.json totalFamilies=${famDoc.totalFamilies} ≠ 模型 ${FAMILIES.length}`);
|
||
check(famDoc.families.length === FAMILIES.length, 'families.json 族数量与模型不符');
|
||
check(famDoc.totalMembers === modelSlugs.length, `families.json totalMembers=${famDoc.totalMembers} ≠ 模型 ${modelSlugs.length}`);
|
||
for (const f of famDoc.families) {
|
||
const src = FAMILIES.find((x) => x.id === f.id);
|
||
check(!!src, `families.json 含模型未定义的族 ${f.id}`);
|
||
if (!src) continue;
|
||
check(
|
||
JSON.stringify(f.members.map((m) => m.slug).sort()) === JSON.stringify(src.members.map((m) => m.slug).sort()),
|
||
`族 ${f.id} 成员集合与模型不符`
|
||
);
|
||
check(!!f.basePrefix, `族 ${f.id} 缺少 basePrefix(index.json 里查不到 ${f.base})`);
|
||
check(!!f.mergeBasis && f.mergeBasis.length > 20, `族 ${f.id} 缺少 mergeBasis 依据`);
|
||
check(Array.isArray(f.paramSurface) && f.paramSurface.length > 0, `族 ${f.id} 缺少 paramSurface`);
|
||
}
|
||
check(
|
||
JSON.stringify(famDoc.implementedFamilies || []) === JSON.stringify(IMPLEMENTED_FAMILIES),
|
||
'families.json implementedFamilies 与模型不符'
|
||
);
|
||
|
||
/* ---------- 2. 全量契约的族字段 ---------- */
|
||
let injected = 0;
|
||
let standalone = 0;
|
||
const index = COMPONENT_INDEX;
|
||
const indexSlugs = index.components.map((c) => c.slug);
|
||
|
||
for (const c of index.components) {
|
||
const file = path.join(COMPONENTS, c.slug + '.json');
|
||
if (!check(fs.existsSync(file), `契约缺失 ${c.slug}.json`)) continue;
|
||
const doc = readJson(file);
|
||
const m = model[c.slug];
|
||
if (m) {
|
||
injected++;
|
||
check(doc.family === m.family, `${c.slug}.json family=${doc.family} ≠ ${m.family}`);
|
||
check(doc.familyRole === m.role, `${c.slug}.json familyRole=${doc.familyRole} ≠ ${m.role}`);
|
||
check(
|
||
JSON.stringify(doc.familyParams || {}) === JSON.stringify(m.params || {}),
|
||
`${c.slug}.json familyParams=${JSON.stringify(doc.familyParams)} ≠ ${JSON.stringify(m.params)}`
|
||
);
|
||
} else {
|
||
standalone++;
|
||
check(doc.family === undefined, `独立组件 ${c.slug}.json 不应有 family 字段`);
|
||
}
|
||
/* 注入不得破坏既有契约字段 */
|
||
check(Array.isArray(doc.semanticTypeCandidates), `${c.slug}.json 既有字段 semanticTypeCandidates 丢失`);
|
||
check(typeof doc.name === 'string', `${c.slug}.json 既有字段 name 丢失`);
|
||
}
|
||
check(injected === modelSlugs.length, `契约注入数 ${injected} ≠ 模型成员数 ${modelSlugs.length}`);
|
||
check(
|
||
injected + standalone === EXPECT_COMPONENTS,
|
||
`契约总数 ${injected + standalone} ≠ ${EXPECT_COMPONENTS}`
|
||
);
|
||
|
||
/* ---------- 3. data.json 产物 ---------- */
|
||
if (check(fs.existsSync(DATA_JSON), 'site/data.json 不存在(先跑 npm run build:site)')) {
|
||
const data = readJson(DATA_JSON);
|
||
check(data.components.length === EXPECT_COMPONENTS, `data.json 组件数 ${data.components.length} ≠ ${EXPECT_COMPONENTS}`);
|
||
|
||
/* 3a. slug 全集不变 —— 铁律 5:data.json 是对外承诺 */
|
||
const got = data.components.map((c) => c.slug);
|
||
check(
|
||
JSON.stringify([...got].sort()) === JSON.stringify([...indexSlugs].sort()),
|
||
'data.json 的 slug 集合与 index.json 不一致(对外承诺被破坏)'
|
||
);
|
||
check(new Set(got).size === got.length, 'data.json 存在重复 slug');
|
||
|
||
/* 3b. 族层存在且自洽 */
|
||
check(data.families != null, 'data.json 缺少顶层 families 字段');
|
||
if (data.families) {
|
||
check(data.families.totalFamilies === FAMILIES.length, `data.json families.totalFamilies=${data.families.totalFamilies} ≠ ${FAMILIES.length}`);
|
||
check(data.families.totalMembers === modelSlugs.length, `data.json families.totalMembers=${data.families.totalMembers} ≠ ${modelSlugs.length}`);
|
||
for (const f of data.families.families) {
|
||
check(!!f.basePrefix, `data.json 族 ${f.id} 缺少 basePrefix`);
|
||
for (const m of f.members) check(!!m.frameworksPrefix, `data.json 族 ${f.id} 成员 ${m.slug} 缺少 frameworksPrefix`);
|
||
}
|
||
}
|
||
check(data.meta.families === FAMILIES.length, `data.json meta.families=${data.meta.families} ≠ ${FAMILIES.length}`);
|
||
check(data.meta.familyMembers === modelSlugs.length, `data.json meta.familyMembers=${data.meta.familyMembers} ≠ ${modelSlugs.length}`);
|
||
|
||
/* 3c. 每个组件要么在族里且字段正确,要么字段为 null */
|
||
let withFam = 0;
|
||
for (const c of data.components) {
|
||
const m = model[c.slug];
|
||
if (m) {
|
||
withFam++;
|
||
check(c.family === m.family, `data.json ${c.slug}.family=${c.family} ≠ ${m.family}`);
|
||
check(c.familyRole === m.role, `data.json ${c.slug}.familyRole=${c.familyRole} ≠ ${m.role}`);
|
||
check(
|
||
JSON.stringify(c.familyParams || {}) === JSON.stringify(m.params || {}),
|
||
`data.json ${c.slug}.familyParams 与模型不符`
|
||
);
|
||
/* 参数面自洽:familyParams 的每个键都在该族 paramSurface 内 */
|
||
const surface = declaredParams.get(m.family);
|
||
for (const k of Object.keys(c.familyParams || {})) {
|
||
check(surface.has(k), `data.json ${c.slug} 的 familyParams.${k} 不在族参数面内`);
|
||
}
|
||
} else {
|
||
check(c.family === null && c.familyRole === null && c.familyParams === null, `独立组件 ${c.slug} 的族字段应为 null`);
|
||
}
|
||
}
|
||
check(withFam === modelSlugs.length, `data.json 带族字段组件数 ${withFam} ≠ ${modelSlugs.length}`);
|
||
}
|
||
|
||
/* ---------- 4. site/details 明细 ---------- */
|
||
if (check(fs.existsSync(DETAILS), 'site/details 不存在(先跑 npm run build:site)')) {
|
||
let detailOk = 0;
|
||
for (const slug of modelSlugs) {
|
||
const f = path.join(DETAILS, slug + '.json');
|
||
if (!check(fs.existsSync(f), `details 缺失 ${slug}.json`)) continue;
|
||
const d = readJson(f);
|
||
check(d.family === model[slug].family, `details/${slug}.json family 不符`);
|
||
check(d.familyRole === model[slug].role, `details/${slug}.json familyRole 不符`);
|
||
detailOk++;
|
||
}
|
||
notes.push(`details 携带族字段:${detailOk}/${modelSlugs.length}`);
|
||
}
|
||
|
||
/* ---------- 5. 仓库级硬约束 ---------- */
|
||
const fwFiles = fs.readdirSync(FRAMEWORKS).filter((f) => fs.statSync(path.join(FRAMEWORKS, f)).isFile());
|
||
check(
|
||
fwFiles.length === EXPECT_FRAMEWORK_FILES,
|
||
`frameworks/ 文件数 ${fwFiles.length} ≠ ${EXPECT_FRAMEWORK_FILES}(tools/pack-deploy.mjs 会硬断言)`
|
||
);
|
||
|
||
const shells = fs.readdirSync(SHELLS).filter((f) => f.endsWith('.html'));
|
||
check(shells.length === EXPECT_COMPONENTS, `site/components 薄壳数 ${shells.length} ≠ ${EXPECT_COMPONENTS}`);
|
||
|
||
/* 每个成员的 5 端文件齐全(PascalCase 前缀 × 5 后缀) */
|
||
let fileChecked = 0;
|
||
for (const c of index.components) {
|
||
const p = c.frameworksPrefix;
|
||
for (const k of KINDS) {
|
||
const ext = { html: '.html', css: '.css', jsx: '.jsx', vue2: '.vue2.vue', vue3: '.vue3.vue' }[k];
|
||
check(fs.existsSync(path.join(FRAMEWORKS, p + ext)), `${c.slug} 缺少 ${p + ext}`);
|
||
fileChecked++;
|
||
}
|
||
}
|
||
check(fileChecked === EXPECT_FRAMEWORK_FILES, `5 端文件校验数 ${fileChecked} ≠ ${EXPECT_FRAMEWORK_FILES}`);
|
||
|
||
/* ---------- 报告 ---------- */
|
||
function report() {
|
||
const familyCount = FAMILIES.length;
|
||
const memberCount = modelSlugs.length;
|
||
const collapsed = memberCount - familyCount;
|
||
console.log('族层验证');
|
||
console.log('─'.repeat(58));
|
||
console.log(` 族 ${familyCount} 个(其中已参数化实现 ${IMPLEMENTED_FAMILIES.length} 个)`);
|
||
console.log(` 族成员 ${memberCount} 个组件`);
|
||
console.log(` 独立组件 ${EXPECT_COMPONENTS - memberCount} 个`);
|
||
console.log(` 概念组件数 ${familyCount}(族)+ ${EXPECT_COMPONENTS - memberCount}(独立)= ${familyCount + EXPECT_COMPONENTS - memberCount} 个,替代原本 ${EXPECT_COMPONENTS} 个并列组件`);
|
||
console.log(` 契约注入 ${injected}/${memberCount}`);
|
||
console.log(` frameworks ${EXPECT_FRAMEWORK_FILES} 文件(${EXPECT_COMPONENTS} x 5)`);
|
||
console.log(` 测试页 ${shells ? shells.length : 0} 薄壳`);
|
||
for (const [id, slugs] of groupByFamily()) {
|
||
console.log(` · ${id.padEnd(20)} ${slugs.length} 成员 → 1 基座(净减 ${slugs.length - 1})`);
|
||
}
|
||
for (const n of notes) console.log(' ' + n);
|
||
console.log('─'.repeat(58));
|
||
|
||
if (failures.length) {
|
||
console.error(`\nFAIL: ${failures.length} 项`);
|
||
for (const f of failures) console.error(' - ' + f);
|
||
process.exit(1);
|
||
}
|
||
console.log(`\nOK: 族层端到端一致(${FAMILIES.length} 族 / ${memberCount} 成员 / ${EXPECT_COMPONENTS} 组件不变 / ${EXPECT_FRAMEWORK_FILES} 文件不变)`);
|
||
console.log(` 概念组件:${EXPECT_COMPONENTS} → ${FAMILIES.length + EXPECT_COMPONENTS - memberCount}(合并掉 ${collapsed} 个重复身份)`);
|
||
}
|
||
|
||
function groupByFamily() {
|
||
return FAMILIES.map((f) => [f.id, f.members.map((m) => m.slug)]);
|
||
}
|
||
|
||
report();
|