规范化历史文档并接入文档索引门禁
新增文档生命周期分类索引与可见状态头 补充规范驱动开发模板和仓库级 skill 新增文档索引检查并接入 lint 修复文档入口链接并纳入现役专题
This commit is contained in:
@@ -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} 份 Markdown,current=${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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user