规范化历史文档并接入文档索引门禁

新增文档生命周期分类索引与可见状态头

补充规范驱动开发模板和仓库级 skill

新增文档索引检查并接入 lint

修复文档入口链接并纳入现役专题
This commit is contained in:
2026-09-13 14:53:18 +08:00
parent b12a81e9c2
commit a01709c55a
26 changed files with 548 additions and 11 deletions
+110
View File
@@ -0,0 +1,110 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const docsRoot = path.join(root, 'docs');
const readmePath = path.join(docsRoot, 'README.md');
const indexPath = path.join(
docsRoot,
'【协作规范】文档生命周期与现状索引-2026-09-12.md',
);
const read = (file) => fs.readFileSync(file, 'utf8');
const rel = (file) => path.relative(root, file).replaceAll(path.sep, '/');
const resolveDocPath = (value) => {
const decoded = decodeURIComponent(value.replaceAll('\\', '/'));
const withoutPrefix = decoded.startsWith('./') ? decoded.slice(2) : decoded;
const target = path.normalize(path.join(docsRoot, withoutPrefix));
const relative = path.relative(docsRoot, target);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error(`docs/README.md 链接越过 docs 根目录: ${value}`);
}
return target;
};
const readme = read(readmePath);
const index = read(indexPath);
const current = new Set(['docs/README.md', 'docs/project-memory/README.md']);
for (const match of readme.matchAll(/\]\((?:<)?([^)>]+)(?:>)?\)/g)) {
const value = match[1];
if (
value.startsWith('http') ||
value.includes('#') ||
!(value.endsWith('.md') || value.endsWith('.json'))
)
continue;
const file = resolveDocPath(value);
if (!fs.existsSync(file)) {
throw new Error(`docs/README.md 链接不存在: ${value}`);
}
if (value.endsWith('.md')) current.add(rel(file));
}
for (const file of fs.readdirSync(
path.join(docsRoot, 'project-memory', 'shared-memory'),
)) {
if (file.endsWith('.md'))
current.add(`docs/project-memory/shared-memory/${file}`);
}
const sectionPaths = (heading, nextHeading) => {
const body = index.split(heading, 2)[1]?.split(nextHeading, 2)[0] ?? '';
return new Set(
[...body.matchAll(/^- `([^`]+\.md)`$/gm)].map((match) => match[1]),
);
};
const historical = sectionPaths('## 历史集合', '## 待复核集合');
const review = sectionPaths('## 待复核集合', '## 开放事项与活动计划');
for (const file of historical) {
if (review.has(file))
throw new Error(`文件同时出现在 historical 和 review: ${file}`);
}
const allDocs = [...walkMarkdown(docsRoot)].map(rel);
const classified = new Set([...current, ...historical, ...review]);
for (const file of [...historical, ...review]) {
const absolute = path.join(root, file);
if (!fs.existsSync(absolute)) throw new Error(`索引中的文件不存在: ${file}`);
const body = read(absolute).slice(0, 1200);
const expected = historical.has(file) ? 'historical' : 'review';
if (!body.includes(`文档状态:\`${expected}\``)) {
throw new Error(`${file} 缺少 ${expected} 状态头`);
}
if (current.has(file))
throw new Error(`${file} 同时被 current 和 ${expected} 分类`);
}
for (const file of current) {
if (!file.endsWith('.md')) continue;
if (file === rel(indexPath)) continue;
const body = read(path.join(root, file)).slice(0, 1200);
if (/^> 文档状态:`(?:historical|review)`/m.test(body)) {
throw new Error(
`${file} 已进入 current,但仍保留 historical/review 状态头`,
);
}
}
for (const file of allDocs) {
if (file.includes('/plans/') || file.includes('/todos/')) continue;
if (!classified.has(file)) throw new Error(`文档未分类: ${file}`);
}
for (const file of [...historical, ...review]) {
if (!allDocs.includes(file))
throw new Error(`索引包含非 Markdown 文件: ${file}`);
}
console.log(
`文档索引检查通过:${allDocs.length} 份 Markdowncurrent=${current.size}historical=${historical.size}review=${review.size}`,
);
function* walkMarkdown(directory) {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const file = path.join(directory, entry.name);
if (entry.isDirectory()) yield* walkMarkdown(file);
else if (entry.isFile() && entry.name.endsWith('.md')) yield file;
}
}