From 6ad0765e4bc39415dd3ceb5e02ad8bfc340ad55e Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 11:36:33 +0800 Subject: [PATCH] =?UTF-8?q?iter(g26):=20verify=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BB=A4=E7=89=8C=E5=BC=95=E7=94=A8=E6=A0=A1=E9=AA=8C=E5=B9=B6?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=BB=A4=E7=89=8C=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 8 ++++ AGENTS.md | 2 +- README.md | 2 +- llms-full.txt | 19 +++++---- packages/tools/cli/bin/ua.js | 78 ++++++++++++++++++++++-------------- 5 files changed, 70 insertions(+), 39 deletions(-) diff --git a/.ua/backlog.json b/.ua/backlog.json index fd941a2..ac66cff 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -564,6 +564,14 @@ "acceptance": "ua tokens 列出全部令牌(颜色带暗色值)、--json 输出数组;MCP 新增 get_tokens 工具;llms-full 令牌名统一 kebab-case;npm run check 全绿", "status": "done", "notes": "自补:工具链改进;CLI 与 MCP 共用 loadTokenList 单一来源" + }, + { + "id": "g26", + "title": "verify 增加令牌引用校验", + "motivation": "皮肤样式表写错令牌名(如 --ua-color-textt)不会报错、静默失效;#hex 门禁只管硬编码,管不到拼错变量", + "acceptance": "var(--ua-*) 必须是 tokens.css 已定义或组件自设变量,否则 verify 失败;ua tokens / llms-full 令牌清单统一取 tokens.css(补全字号与复合间距);npm run check 全绿", + "status": "open", + "notes": "自补:构建/门禁工具链改进" } ] } diff --git a/AGENTS.md b/AGENTS.md index 4d12de0..1e239f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ - `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html - `apps/docs`:文档站,纯产物(ua docs 生成) -**门禁一条龙**:`npm run check` = build + style + typecheck + smoke + index + verify,提交前必过。`ua verify` 校验清单 ↔ 文档 ↔ 演示一致(docs.components 的 id 必须在 api.exports 有对应条目、props 必须带 n/t/d、demo 锚点必须在 `apps/demo/main.ts` 中存在),检查皮肤包样式表无硬编码颜色(`#hex` 一律禁止,必须走 `--ua-*` 令牌),核对 README 的 `` 区块与清单组件集合完全一致,并交叉校验 backlog 的 done 条目与 `.ua/iterations.log` 的 done 留痕一一对应。 +**门禁一条龙**:`npm run check` = build + style + typecheck + smoke + index + verify,提交前必过。`ua verify` 校验清单 ↔ 文档 ↔ 演示一致(docs.components 的 id 必须在 api.exports 有对应条目、props 必须带 n/t/d、demo 锚点必须在 `apps/demo/main.ts` 中存在),检查皮肤包样式表无硬编码颜色(`#hex` 一律禁止,必须走 `--ua-*` 令牌)与令牌引用有效(`var(--ua-*)` 必须是 tokens.css 已定义或组件自设的变量),核对 README 的 `` 区块与清单组件集合完全一致,并交叉校验 backlog 的 done 条目与 `.ua/iterations.log` 的 done 留痕一一对应。 **提交信息规范**:`COMMITTING.md`(`(): <中文祈使句>`),`ua commit-lint` + commit-msg 钩子自动校验,迭代提交用 `iter(<条目id>): `。 diff --git a/README.md b/README.md index 401ed5c..b71e197 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ accordion · alert · avatar · badge · breadcrumb · button · card · checkbo ## 三道门禁 1. `ua style`:注释规范——导出必须有 `/*** 作用 ***/`、接口字段行尾注释、行注释用中文。 -2. `ua verify`:清单 ↔ 文档 ↔ 演示一致(id 唯一、exports 对应、props 带 n/t/d、steps ≥ 3、demo 锚点存在),皮肤样式表禁止 `#hex` 硬编码颜色。 +2. `ua verify`:清单 ↔ 文档 ↔ 演示一致(id 唯一、exports 对应、props 带 n/t/d、steps ≥ 3、demo 锚点存在),皮肤样式表禁止 `#hex` 硬编码颜色,`var(--ua-*)` 必须引用 tokens.css 已定义或组件自设的变量。 3. `ua index`:刷新 `.ua/index.json` / `llms.txt`,供 MCP 与 Agent 查询。 ## Agent 接入 diff --git a/llms-full.txt b/llms-full.txt index bdd0975..c063901 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1195,19 +1195,22 @@ node packages/tools/cli/bin/ua.js |docs|tokens|style|veri - --ua-color-surface: #f6f7f9(暗色 #1a1c22) - --ua-color-text: #1c1e21(暗色 #e7e9ec) - --ua-color-text-muted: #6b7280(暗色 #888780) +- --ua-color-border: rgba(0, 0, 0, 0.15)(暗色 rgba(255, 255, 255, 0.15)) - --ua-color-accent: #185fa5(暗色 #378add) -- --ua-color-danger: #a32d2d -- --ua-color-success: #0f6e56 - -### radius - +- --ua-color-accent-contrast: #ffffff(暗色 #042c53) +- --ua-color-danger: #a32d2d(暗色 #d96a6a) +- --ua-color-success: #0f6e56(暗色 #4fc39e) +- --ua-color-warning: #9a6b1f(暗色 #d9a94f) - --ua-radius-sm: 4px - --ua-radius-md: 8px - --ua-radius-lg: 12px - -### space - - --ua-space-1: 4px - --ua-space-2: 8px - --ua-space-3: 12px - --ua-space-4: 16px +- --ua-space-table-cell: var(--ua-space-2) var(--ua-space-3) +- --ua-font-size-sm: 12px +- --ua-font-size-md: 13px +- --ua-font-size-lg: 14px + +皮肤包只允许消费这些变量:写死 #hex 会被 verify 拦截,拼错变量名会被令牌引用校验拦截。 diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index c31bef5..3e0f973 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -327,20 +327,11 @@ function renderAgentDocs(index, tokens) { } lines.push('---', ''); } - if (tokens?.color) { + const tokenList = loadTokenList(); // 令牌唯一来源:tokens.css(含字号、间距复合值) + if (tokenList.length) { lines.push('## 设计令牌(--ua-*)', ''); - const kebab = (s) => s.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); // textMuted → text-muted,与 ui.css 实际变量名一致 - for (const [k, v] of Object.entries(tokens.color)) { - const light = typeof v === 'string' ? v : v.light; - const dark = typeof v === 'string' ? v : v.dark; - lines.push(`- --ua-color-${kebab(k)}: ${light}${dark !== light ? `(暗色 ${dark})` : ''}`); - } - for (const [g, vals] of Object.entries(tokens)) { - if (g === 'color' || g === '$schema') continue; - lines.push('', `### ${g}`, ''); - for (const [k, v] of Object.entries(vals)) lines.push(`- --ua-${g}-${kebab(k)}: ${v}`); - } - lines.push(''); + for (const t of tokenList) lines.push(`- ${t.cssVar}: ${t.value}${t.dark && t.dark !== t.value ? `(暗色 ${t.dark})` : ''}`); + lines.push('', '皮肤包只允许消费这些变量:写死 #hex 会被 verify 拦截,拼错变量名会被令牌引用校验拦截。', ''); } return lines.join('\n'); } @@ -368,22 +359,22 @@ function cmdDocs() { // ---- 令牌查询:CLI 与 MCP 共用同一份拍平结果 ---- -/*** 读取 tokens.json 拍平为 CSS 变量清单(颜色区分亮/暗两态,其余单值;名字统一 kebab-case 与 ui.css 对齐) ***/ +/*** 读取 tokens.css 拍平为令牌清单(:root 为基准值,[data-theme='dark'] 覆盖为暗色值;名字即 ui.css 实际变量名) ***/ function loadTokenList() { - const p = path.join(ROOT, 'packages', 'design', 'tokens', 'tokens.json'); + const p = path.join(ROOT, 'packages', 'design', 'tokens', 'tokens.css'); if (!fs.existsSync(p)) return []; - const tokens = JSON.parse(fs.readFileSync(p, 'utf8')); - const kebab = (s) => s.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); // camelCase 键转 kebab-case - const out = []; - for (const [k, v] of Object.entries(tokens.color ?? {})) { - if (typeof v === 'string') out.push({ cssVar: `--ua-color-${kebab(k)}`, value: v }); - else out.push({ cssVar: `--ua-color-${kebab(k)}`, value: v.light, dark: v.dark }); - } - for (const [group, vals] of Object.entries(tokens)) { - if (group === 'color' || group.startsWith('$')) continue; - for (const [k, v] of Object.entries(vals)) out.push({ cssVar: `--ua-${group}-${kebab(k)}`, value: String(v) }); - } - return out; + const src = fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'); + const grab = (block) => { + const out = new Map(); // cssVar -> 值 + for (const m of block.matchAll(/(--ua-[\w-]+)\s*:\s*([^;]+);/g)) out.set(m[1], m[2].trim()); + return out; + }; + const rootBlock = (src.match(/:root\s*\{([\s\S]*?)\}/) ?? [, ''])[1]; // 亮色基准 + const darkBlock = (src.match(/\[data-theme='dark'\]\s*\{([\s\S]*?)\}/) ?? [, ''])[1]; // 暗色覆盖 + const dark = grab(darkBlock); + return [...grab(rootBlock).entries()].map(([cssVar, value]) => + dark.has(cssVar) ? { cssVar, value, dark: dark.get(cssVar) } : { cssVar, value }, + ); } /*** ua tokens:打印令牌清单;--json 输出结构化数组供脚本消费 ***/ @@ -636,6 +627,31 @@ function checkSkinCss(pkg) { return issues; } +/*** 皮肤包令牌引用校验:var(--ua-*) 必须是 tokens.css 令牌、同文件自定义变量,或组件脚本里 setProperty 的动态变量(防拼错名字静默失效) ***/ +function checkTokenRefs(pkg, allowed) { + const issues = []; + const dir = path.join(ROOT, pkg.dir); + if (!fs.existsSync(dir)) return issues; + const local = new Set(); // 组件自身定义/写入的 --ua-*:CSS 定义 + TS 里 setProperty 的动态变量 + const srcDir = path.join(dir, 'src'); + if (fs.existsSync(srcDir)) { + for (const f of fs.readdirSync(srcDir)) { + if (!f.endsWith('.ts')) continue; + for (const m of fs.readFileSync(path.join(srcDir, f), 'utf8').matchAll(/--ua-[\w-]+/g)) local.add(m[0]); + } + } + for (const name of fs.readdirSync(dir)) { + if (!name.endsWith('.css')) continue; // 只查样式表引用 + const src = fs.readFileSync(path.join(dir, name), 'utf8'); + for (const m of src.matchAll(/(--ua-[\w-]+)\s*:/g)) local.add(m[1]); // 样式表内自定义变量 + for (const hit of src.matchAll(/var\(\s*(--ua-[\w-]+)/g)) { + const ref = hit[1]; + if (!allowed.has(ref) && !local.has(ref)) issues.push(`${pkg.dir}/${name}: 引用未定义令牌 ${ref}`); + } + } + return issues; +} + /*** README 组件区块校验:marker 之间的 id 集合必须与全部清单的 docs.components 完全一致(防文档漂移) ***/ function checkReadme(pkgs) { const issues = []; @@ -754,12 +770,16 @@ function cmdVerify() { const demoPath = path.join(ROOT, 'apps', 'demo', 'main.ts'); // 演示区块定义在源码里(产物是打包后的 JS) const demoSource = fs.existsSync(demoPath) ? fs.readFileSync(demoPath, 'utf8') : ''; const anchors = new Set(demoSource.match(/sec-[a-z0-9-]+/g) ?? []); // 已定义的区块锚点 + const tokenVars = new Set(loadTokenList().map((t) => t.cssVar)); // 令牌白名单:tokens.json 拍平结果 if (!demoSource) console.log('verify: 未找到 apps/demo/main.ts,跳过 demo 锚点校验'); for (const pkg of pkgs) { const comps = pkg.docs?.components ?? []; // 清单已被 collectManifests 摊平 const exports = pkg.api?.exports ?? []; - if (pkg.type === 'skin-dom') issues.push(...checkSkinCss(pkg)); + if (pkg.type === 'skin-dom') { + issues.push(...checkSkinCss(pkg)); + issues.push(...checkTokenRefs(pkg, tokenVars)); + } if (comps.length && !exports.length) issues.push(`${pkg.name}: docs.components 有 ${comps.length} 项但 api.exports 为空`); const seen = new Set(); // 组件 id 去重(同一清单内不允许重复) @@ -795,7 +815,7 @@ function cmdVerify() { console.error(`${issues.length} violation(s)`); process.exit(1); } - console.log('verify OK (manifest ↔ docs ↔ demo 一致,皮肤无硬编码颜色,README 组件清单同步,backlog ↔ 留痕一致)'); + console.log('verify OK (manifest ↔ docs ↔ demo 一致,皮肤无硬编码颜色与未定义令牌,README 组件清单同步,backlog ↔ 留痕一致)'); } /*** 校验 git 提交信息是否符合 COMMITTING.md 规范(参数为 message 文件路径或直接 message 文本),违规退出码 1 ***/