#!/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();