规范化历史文档并合入 AGC 插件宿主能力 (#339)
## 变更 - 建立文档生命周期与现状索引,统一 `current`、`historical`、`review`、开放事项和活动计划边界。 - 为历史/待复核文档补充状态头,将有源码与测试证据的专题纳入当前入口。 - 新增规范驱动开发 skill 与 `check:doc-index` 门禁,并接入 `lint`。 - 合入 `origin/master` 的 AGC 通用插件宿主与 Cocos Creator 编辑器能力,并补充父子规范关系和验收范围。 ## 合并说明 - 合入基线:`origin/master` `4a46f89c9`。 - 合并提交:`259134b59`。 - 合并过程无冲突。 ## 验证 - `npm run check:doc-index` - `npm run check:encoding` - `git diff --check` - `python -X utf8 C:/Users/kdletters/.codex/skills/.system/skill-creator/scripts/quick_validate.py .codex/skills/spec-driven-development` - 推送前 pre-push master 门禁通过。 ## CI 请以本 PR 的 Gitea CI 检查为准;提交后持续关注,失败时按失败 job 继续修复。 Reviewed-on: #339 Co-authored-by: kdletters <kdletters@qq.com> Co-committed-by: kdletters <kdletters@qq.com>
This commit was merged in pull request #339.
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