Files
Genarrative/scripts/check-doc-index.mjs
T
kdletters 96b45e6ffc
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
规范化历史文档并合入 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>
2026-09-13 16:21:34 +08:00

111 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}