iter(g134): 新增 ua sync-counts 自动同步组件计数

This commit is contained in:
HIsheR
2026-10-26 15:25:52 +08:00
parent 6701c226fb
commit 2a9193ce19
4 changed files with 51 additions and 12 deletions
+8
View File
@@ -1428,6 +1428,14 @@
"acceptance": "treeSelect 点击开弹层、支持搜索过滤、单选/多选、父子联动、清空,弹层沿用 overlay 的 Esc 栈与外部点击收起;样式只消费 --ua-*;文档与演示同步;npm run check 全绿", "acceptance": "treeSelect 点击开弹层、支持搜索过滤、单选/多选、父子联动、清空,弹层沿用 overlay 的 Esc 栈与外部点击收起;样式只消费 --ua-*;文档与演示同步;npm run check 全绿",
"status": "done", "status": "done",
"notes": "自补:组件补充方向" "notes": "自补:组件补充方向"
},
{
"id": "g134",
"title": "组件计数自动同步 ua sync-counts",
"motivation": "新增组件要手改 README 标题 / README 包说明 / AGENTS / ARCHITECTURE 四处数字,本轮四次新增每次都靠人工替换;且 README 标题「共 N 个」此前不在 verify 正则里,漏改不报错",
"acceptance": "新增 ua sync-counts(幂等,无变化不写盘)把三份文档的「N 个组件 / N 个 DOM 组件 / 共 N 个」统一改写为清单实际数量;verify 的计数正则覆盖 README 标题形态并提示修法;npm run check 全绿",
"notes": "自补:构建/门禁工具链改进(实测把 README 标题改成 61 后 verify 能报行号,sync-counts 一键修回)",
"status": "open"
} }
] ]
} }
+2 -2
View File
@@ -76,8 +76,8 @@
| 5 | `packages/skin/ui/ua.manifest.json` → `api.exports` | 一行 `xxx(props):一句话说明` | | 5 | `packages/skin/ui/ua.manifest.json` → `api.exports` | 一行 `xxx(props):一句话说明` |
| 6 | 同上 → `docs.components` | `{ id, title, demo, props[], example, steps[] }`:props 里每个字段名必须在实现源码里真实出现,且 `<组件>Props` 的字段一个都不能漏(verify 双向校验) | | 6 | 同上 → `docs.components` | `{ id, title, demo, props[], example, steps[] }`:props 里每个字段名必须在实现源码里真实出现,且 `<组件>Props` 的字段一个都不能漏(verify 双向校验) |
| 7 | `apps/demo/main.ts` | 新增 `section('<demo 锚点>', ...)` 并**真实调用**该组件(verify 会查) | | 7 | `apps/demo/main.ts` | 新增 `section('<demo 锚点>', ...)` 并**真实调用**该组件(verify 会查) |
| 8 | `README.md` | `<!-- ua:components -->` 区块补 id、「分组:」行补归组、标题「共 N 个」与包说明里的数字 | | 8 | `README.md` | `<!-- ua:components -->` 区块补 id、「分组:」行补归组(数字交给下一步自动同步) |
| 9 | `AGENTS.md` / `ARCHITECTURE.md` | 两处的「N 个组件」计数同步 | | 9 | `npm run sync-counts` | 自动把三份文档里的「N 个组件 / 共 N 个」改成清单实际数量(幂等;漏改时 verify 报行号) |
| 10 | `npm run docs` | 重新生成 `llms-full.txt` 与 `apps/docs/index.html`(不跑 verify 会报 llms-full 漂移) | | 10 | `npm run docs` | 重新生成 `llms-full.txt` 与 `apps/docs/index.html`(不跑 verify 会报 llms-full 漂移) |
改**既有**组件的入参同样要走 5、6、10 三步;只加一个可选参数也要补文档行,否则双向校验失败。 改**既有**组件的入参同样要走 5、6、10 三步;只加一个可选参数也要补文档行,否则双向校验失败。
+2 -1
View File
@@ -17,7 +17,8 @@
"verify": "node packages/tools/cli/bin/ua.js verify", "verify": "node packages/tools/cli/bin/ua.js verify",
"lint-iterations": "node packages/tools/cli/bin/ua.js lint-iterations", "lint-iterations": "node packages/tools/cli/bin/ua.js lint-iterations",
"changelog": "node packages/tools/cli/bin/ua.js changelog", "changelog": "node packages/tools/cli/bin/ua.js changelog",
"changelog:check": "node packages/tools/cli/bin/ua.js changelog --check" "changelog:check": "node packages/tools/cli/bin/ua.js changelog --check",
"sync-counts": "node packages/tools/cli/bin/ua.js sync-counts"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.6.0" "typescript": "^5.6.0"
+39 -9
View File
@@ -627,7 +627,10 @@ function styleCheckFile(file, src) {
let j = i - 1; let j = i - 1;
while (j >= 0 && lines[j].trim() === '') j--; // 找上一个非空行 while (j >= 0 && lines[j].trim() === '') j--; // 找上一个非空行
if (j < 0 || !lines[j].includes('/***')) { if (j < 0 || !lines[j].includes('/***')) {
issues.push(`${file}:${i + 1} 导出 ${decl[1]} 缺少 /*** 作用 ***/ 注释`); // 上一行是注释续行(以 ***/ 结尾却没有 /***)说明写成了多行,给出可直接照做的修法
const cont = j >= 0 && lines[j].includes('***/') && !lines[j].includes('/***');
const hint = cont ? '(注释被拆成多行:压成单行 /*** 作用 ***/ 并紧贴导出上一行)' : '';
issues.push(`${file}:${i + 1} 导出 ${decl[1]} 缺少 /*** 作用 ***/ 注释${hint}`);
} }
} }
@@ -749,22 +752,46 @@ function checkReadme(pkgs) {
} }
/*** 组件数漂移校验:AGENTS.md / ARCHITECTURE.md 中的「N 个组件」必须与 skin-dom 清单实际总数一致 ***/ /*** 组件数漂移校验:AGENTS.md / ARCHITECTURE.md 中的「N 个组件」必须与 skin-dom 清单实际总数一致 ***/
// 组件计数的三种写法:「N 个组件」「N 个 DOM 组件」「共 N 个」
const COUNT_RE = /(\d+)\s*个\s*(?:DOM\s*)?组件|共\s*(\d+)\s*个/g;
/*** 逐行扫文档里的组件计数,返回 { line, value, text } 便于校验与改写共用 ***/
function scanCountLines(text) {
const hits = [];
text.split(/\r?\n/).forEach((line, i) => {
for (const m of line.matchAll(COUNT_RE)) hits.push({ line: i + 1, value: Number(m[1] ?? m[2]), text: line });
});
return hits;
}
function checkCountDocs(actual) { function checkCountDocs(actual) {
const issues = []; const issues = [];
const re = /(\d+)\s*个(?:\s*DOM\s*)?组件/;
for (const name of ['AGENTS.md', 'ARCHITECTURE.md', 'README.md']) { for (const name of ['AGENTS.md', 'ARCHITECTURE.md', 'README.md']) {
const file = path.join(ROOT, name); const file = path.join(ROOT, name);
if (!fs.existsSync(file)) continue; // 卫语句:文档缺失不校验 if (!fs.existsSync(file)) continue; // 卫语句:文档缺失不校验
fs.readFileSync(file, 'utf8').split(/\r?\n/).forEach((line, i) => { for (const hit of scanCountLines(fs.readFileSync(file, 'utf8'))) {
const m = line.match(re); if (hit.value !== actual) issues.push(`${name}:${hit.line} 组件数「${hit.value}」与清单实际 ${actual} 不一致,跑 ua sync-counts 自动同步`);
if (m && Number(m[1]) !== actual) { }
issues.push(`${name}:${i + 1} 组件数「${m[1]}」与清单实际 ${actual} 不一致,请同步`);
}
});
} }
return issues; return issues;
} }
/*** 组件计数同步:把三份文档里的「N 个组件 / 共 N 个」一律改成清单实际数量(幂等,无变化不写盘) ***/
function cmdSyncCounts() {
const actual = collectComponents(collectManifests()).length;
let changed = 0;
for (const name of ['AGENTS.md', 'ARCHITECTURE.md', 'README.md']) {
const file = path.join(ROOT, name);
if (!fs.existsSync(file)) continue; // 卫语句:文档缺失跳过
const before = fs.readFileSync(file, 'utf8');
const after = before.replace(COUNT_RE, (m, a, b) => (a ? m.replace(a, String(actual)) : m.replace(b, String(actual))));
if (after === before) continue;
fs.writeFileSync(file, after, 'utf8');
changed += 1;
}
console.log(`sync-counts: 清单实际 ${actual} 个组件,改写 ${changed} 个文件`);
}
/*** llms-full.txt 同步校验:内联重渲染 Agent 文档(时间戳归一化)与落盘产物比对,防 manifest 改后忘跑 docs ***/ /*** llms-full.txt 同步校验:内联重渲染 Agent 文档(时间戳归一化)与落盘产物比对,防 manifest 改后忘跑 docs ***/
function checkLlmsFull(pkgs, issues) { function checkLlmsFull(pkgs, issues) {
if (!fs.existsSync(LLMS_FULL)) return; // 卫语句:产物缺失不算漂移(可能是新仓库) if (!fs.existsSync(LLMS_FULL)) return; // 卫语句:产物缺失不算漂移(可能是新仓库)
@@ -1101,6 +1128,9 @@ switch (cmd) {
case 'style': case 'style':
cmdStyle(); cmdStyle();
break; break;
case 'sync-counts':
cmdSyncCounts();
break;
case 'verify': case 'verify':
cmdVerify(); cmdVerify();
break; break;
@@ -1120,6 +1150,6 @@ switch (cmd) {
cmdMcp(); cmdMcp();
break; break;
default: default:
console.log('usage: ua <index|list|info <name>|docs|tokens [--json]|style|verify|commit-lint|lint-iterations|changelog|mcp>'); console.log('usage: ua <index|list|info <name>|docs|tokens [--json]|style|sync-counts|verify|commit-lint|lint-iterations|changelog|mcp>');
process.exit(cmd ? 1 : 0); process.exit(cmd ? 1 : 0);
} }