规范化历史文档并合入 AGC 插件宿主能力 (#339)
Project CI / Repository checks (push) Successful in 2m55s
Project CI / Frontend tests (push) Successful in 3m25s
Project CI / Backend tests (push) Successful in 6m32s
Project CI / Native shell tests (push) Successful in 17m43s

## 变更

- 建立文档生命周期与现状索引,统一 `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:
2026-09-13 16:21:34 +08:00
committed by 段舒康
parent 4a46f89c9b
commit 96b45e6ffc
28 changed files with 556 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;
}
}