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 未做部分)
111 lines
6.4 KiB
JavaScript
111 lines
6.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* verify-api-docs.mjs — 组件页 API 表「说明」的出处可溯性验收(零依赖)。
|
||
*
|
||
* 背景:API 表的说明列在 S6-P37 之前 235 条里 230 条是 `—`。补说明时立了两条硬规矩,
|
||
* 这个脚本就是把它们变成可复跑的判据 —— 说明可以来自规格原文、API 命名释义或源码注释,
|
||
* 但**每一条都必须能被指认出来源**,且「规格」类必须是规格原文的逐字引用:
|
||
*
|
||
* 1) 每条 prop 都有说明,且 descSrc ∈ {spec, name, code, contract}(none 即失败)
|
||
* 2) descSrc=spec 的 cite 必须逐字出现在该组件的规格原文分条里(防「看起来像规格」的编造)
|
||
* 3) 事件说明不得回落到模板句(「组件交互触发事件」),update:* 必须标注 v-model
|
||
* 4) 说明与事件文案必须都在 site/i18n.js 的英文字典里(否则英文模式回退中文)
|
||
* 5) 规格取值副行只在「分条标签 === 该 prop 的说明」时出现,且片段可回查规格原文
|
||
* (反例:card.title 曾被「结构:标题区、内容区、操作区」误配、tag.color 曾切出 `绿#F6FFED`)
|
||
*
|
||
* 前置:site/data.json 已是当前构建产物(node tools/precompute.mjs 产出)。
|
||
*/
|
||
import { readFileSync, existsSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
|
||
const ROOT = join(import.meta.dirname, '..');
|
||
const failures = [];
|
||
const checks = [];
|
||
function check(name, ok, detail = '') {
|
||
checks.push({ name, ok, detail });
|
||
if (!ok) failures.push(`${name}${detail ? `: ${detail}` : ''}`);
|
||
}
|
||
|
||
const dataPath = join(ROOT, 'site', 'data.json');
|
||
if (!existsSync(dataPath)) {
|
||
console.error('[api-docs] 缺少 site/data.json —— 先跑 node tools/precompute.mjs');
|
||
process.exit(2);
|
||
}
|
||
const data = JSON.parse(readFileSync(dataPath, 'utf8'));
|
||
const i18nSrc = readFileSync(join(ROOT, 'site', 'i18n.js'), 'utf8');
|
||
|
||
const unesc = (s) => s.replace(/\\(['"])/g, '$1');
|
||
const dict = new Set();
|
||
for (const m of i18nSrc.matchAll(/^\s*(['"])((?:\\.|(?!\1)[\s\S])*?)\1\s*:/gm)) dict.add(unesc(m[2]));
|
||
|
||
const props = [];
|
||
const emits = [];
|
||
for (const c of data.components) {
|
||
for (const p of (c.api && c.api.props) || []) props.push({ slug: c.slug, c, p });
|
||
for (const e of (c.api && c.api.emits) || []) emits.push({ slug: c.slug, e });
|
||
}
|
||
|
||
const specLines = (c) => {
|
||
const raw = Array.isArray(c.specLines) ? c.specLines : String(c.specLines || '').split(/\r?\n/);
|
||
return raw.map((l) => String(l).replace(/^\s*[-•*]\s*/, '').trim()).filter(Boolean);
|
||
};
|
||
|
||
/* 1) 说明覆盖与出处 */
|
||
const srcCount = {};
|
||
for (const { p } of props) srcCount[p.descSrc || 'none'] = (srcCount[p.descSrc || 'none'] || 0) + 1;
|
||
const emptyDesc = props.filter(({ p }) => !p.desc || p.desc === '—');
|
||
check('prop 说明非空', emptyDesc.length === 0,
|
||
`${props.length - emptyDesc.length}/${props.length} 有说明(出处 ${JSON.stringify(srcCount)})`);
|
||
check('无 none 出处', !srcCount.none, srcCount.none ? `${srcCount.none} 条没有出处` : '全部可溯源');
|
||
|
||
/* 2) spec 类必须是规格原文逐字引用 */
|
||
const specProps = props.filter(({ p }) => p.descSrc === 'spec');
|
||
const badCite = specProps.filter(({ c, p }) => !specLines(c).some((l) => l.startsWith(p.cite) || p.cite.startsWith(l)));
|
||
check('spec 出处 cite 逐字命中规格原文', badCite.length === 0,
|
||
`${specProps.length} 条 spec${badCite.length ? `,异常 ${badCite.length}:${badCite.slice(0, 3).map((x) => `${x.slug}.${x.p.name}`).join(', ')}` : ''}`);
|
||
|
||
/* 3) 事件说明 */
|
||
const TEMPLATES = ['组件交互触发事件', '交互回调触发事件'];
|
||
const genericEmits = emits.filter(({ e }) => !e.descZh || TEMPLATES.includes(e.descZh));
|
||
check('事件说明无模板句', genericEmits.length === 0,
|
||
`${emits.length - genericEmits.length}/${emits.length} 具体化${genericEmits.length ? `,异常:${genericEmits.slice(0, 3).map((x) => `${x.slug}.${x.e.name}`).join(', ')}` : ''}`);
|
||
const vmodel = emits.filter(({ e }) => String(e.name).startsWith('update:'));
|
||
check('update:* 标注 v-model', vmodel.every(({ e }) => e.descZh.includes('v-model')), `${vmodel.length} 条`);
|
||
|
||
/* 4) 英文词条覆盖(否则英文模式回退中文) */
|
||
const descVals = [...new Set(props.map((x) => x.p.desc))].filter((d) => d && d !== '—');
|
||
const missDesc = descVals.filter((d) => !dict.has(d));
|
||
check('属性说明词条齐全', missDesc.length === 0,
|
||
`${descVals.length} 个唯一值${missDesc.length ? `,缺失 ${missDesc.slice(0, 6).join(' / ')}` : ''}`);
|
||
const emitVals = [...new Set(emits.map((x) => x.e.descZh))].filter(Boolean);
|
||
const missEmit = emitVals.filter((d) => !dict.has(d));
|
||
check('事件说明词条齐全', missEmit.length === 0,
|
||
`${emitVals.length} 个唯一值${missEmit.length ? `,缺失 ${missEmit.slice(0, 6).join(' / ')}` : ''}`);
|
||
|
||
/* 5) 规格取值副行:标签一致 + 可回查 */
|
||
const withOpts = props.filter((x) => x.p.opts && x.p.opts.length);
|
||
const badOpts = withOpts.filter(({ c, p }) => {
|
||
const label = String(p.cite || '').split(/[::]/)[0].trim();
|
||
if (label !== p.desc && label.indexOf(p.desc) !== 0) return true;
|
||
return !specLines(c).some((l) => l.includes(p.opts[0]));
|
||
});
|
||
check('规格取值副行可回查且标签一致', badOpts.length === 0,
|
||
`${withOpts.length} 条${withOpts.length ? `(${withOpts.map((x) => `${x.slug}.${x.p.name}×${x.p.opts.length}`).join(', ')})` : ''}${badOpts.length ? `,异常 ${badOpts.slice(0, 3).map((x) => `${x.slug}.${x.p.name}`).join(', ')}` : ''}`);
|
||
|
||
/* 6) 覆盖面(有 API 的组件数不应无故缩水) */
|
||
const withApi = data.components.filter((c) => {
|
||
const a = c.api || {};
|
||
return (a.props || []).length || (a.emits || []).length || (a.slots || []).length;
|
||
});
|
||
const allowedWithoutApi = new Set(['enhancedtabnav', 'dragupload', 'signaturepad']);
|
||
const withoutApi = data.components.filter((c) => !withApi.includes(c));
|
||
check('有 API 表的组件数', withoutApi.every((c) => allowedWithoutApi.has(c.slug)), `${withApi.length}/${data.components.length};无 API:${withoutApi.map((c) => c.slug).join(', ') || '无'}`);
|
||
check('无 API 组件为已知例外', withoutApi.length === [...allowedWithoutApi].filter((slug) => data.components.some((c) => c.slug === slug)).length, `${withoutApi.length} 个`);
|
||
|
||
for (const r of checks) console.log(`[api-docs] ${r.ok ? 'OK' : 'FAIL'} ${r.name}${r.detail ? ` — ${r.detail}` : ''}`);
|
||
if (failures.length) {
|
||
console.error(`\n[api-docs] ${failures.length} check(s) failed`);
|
||
process.exit(1);
|
||
}
|
||
console.log(`\n[api-docs] OK — ${checks.length} checks passed`);
|