Files
aurora-admin/tools/verify-families.mjs
T
aurora-admin eb25feedaf feat(S6-P21): 组件族参数化(13族/44成员/导航族15文件同源/79→48概念组件)
【本次核心 · S6-P21】
- 族层数据:families.json + 44 份契约注入 family/familyRole/familyParams;
  data.json / data.js / site/details 同步。13 族 / 44 成员 / 35 独立 → 概念组件 79→48。
- 79 个 slug 全保留、集合逐一不变(铁律 5 对外承诺未破);frameworks 仍 395 文件、薄壳仍 79。
- 归族判据为契约中可核对字段(semanticTypeCandidates 重叠 / anatomy 为同一骨架子集 /
  变体维度同构 / doNotInvent 显式从属声明),每族 mergeBasis 写明依据,不按名字猜。
- 实现层合并(导航族端到端切片):tools/gen-family-impl.mjs 从 5 端模板生成
  TopMenu / SideMenu / MixedNavigation 共 15 文件,参数 direction=top|side|mixed;
  三份 CSS md5 完全相同 = 一份样式表服务三个组件。
- 新增 tools/gen-families.mjs、tools/gen-family-impl.mjs、tools/verify-families.mjs、
  tools/lib/family-model.mjs、tools/lib/family-impl/nav-menu/*.tpl。

【同时清掉此前已完成但未提交的批次】
生成物(data.json / data.js / site/sources / site/components 薄壳 / sitemap.xml / tests 报告)
跨阶段交织,无法拆成互相自洽的多个提交,故按既有批量风格合并提交:
- Package:三端可 import(S5-P18)+ 发布到私有 npm 源
- Docs site:导航语言改下拉(S5-P19)、详情页代码块默认展开、中英切换完整性
- Security:生产部署链审计修复(2026-09-19)+ 线上部署
- Theme modes 日间/夜间/自动;S1-P4 data.js 瘦身;S2-P5 暗色;S2-P6 跨端一致性;
  S2-P7 行为断言;S2-P9 FAQ;S3-P8 RTL;S3-P9 契约缺口解释层;S4-P12 发布流程
- 补入 tools/pack-deploy.mjs、run-site-smoke.mjs、verify-*.mjs,.dockerignore、
  安全审计修复与待决策项.md

【验收】
- node tools/verify-families.mjs → OK: 族层端到端一致(13 族 / 44 成员 / 79 组件不变 / 395 文件不变)
- node tools/verify-cross-platform.mjs → 79/79 identical(HEAD 基线 high 44)
- node tools/run-regression.mjs → 100%(79/79 页,1017/1017 断言,N/A 34),连跑 8 次一致,0 超时
- 逐页实测:topmenu / sidemenu / mixednavigation 各 13/13,帧内 direction 参数正确,0 JS 错误
- 零运行时依赖 OK;build-site.ps1 ASCII-only OK

【未纳入】site/components/<slug>/ 平台薄壳 316 个 —— 历史从未跟踪且属构建产物,保持现状。
2026-09-20 03:32:31 +08:00

259 lines
11 KiB
JavaScript
Raw 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-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', 'aurora-admin');
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 EXPECT_COMPONENTS = 79;
const EXPECT_FRAMEWORK_FILES = 395;
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. 79 份契约的族字段 ---------- */
let injected = 0;
let standalone = 0;
const index = readJson(path.join(COMPONENTS, 'index.json'));
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} 文件(79 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();