Files
vrsub/scripts/check_doc_links.py
T
cat-shark 966f3e6b4b docs: AGENTS.md 只保留代理规则,项目信息迁入 README 与 docs/
把 AGENTS.md(545 行)中的项目知识按性质拆开,只留下对代理的要求:

- AGENTS.md(176 行)只写规则:读文档指引、提交许可、等待规则、TDD、
  测试规则(模块边界/三段结构/功能覆盖优先)、注释规范、目标运行环境、
  PowerShell 规则、文档维护规则;
- 项目信息新建 docs/ 专题:architecture、node-protocol、workflows、
  configuration、operations、testing、decisions;
- README 改为项目索引(定位、快速开始、文档导航、目录、工作流、结论摘要);
- 修正旧文档错误:README 的"详细约定见 AGENTS.md"与"100% 行覆盖率"
  (pytest 已移除该门槛);环境变量表补齐 WOV_AUTO_VAD 等 3 项。

新增 scripts/check_doc_links.py 校验相对链接与锚点,当前 14 个文档全部可达。
2026-09-13 15:40:31 +08:00

68 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""校验 vrsub 文档中的相对 Markdown 链接是否都指向真实文件。
用法:uv run python scripts/check_doc_links.py(在 vrsub 目录下执行)
职责:扫描 README.md、AGENTS.md 与 docs/*.md 中的所有相对链接
(含 #锚点),确认目标文件存在、锚点能在目标文件中找到对应标题。
退出码非 0 表示存在断链,供 CI 或提交前检查使用。
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
# Markdown 链接语法 [文本](目标),目标不含协议头即为仓库内相对链接
LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
# GitHub 风格锚点生成:小写、去反引号与标点、空格转连字符(此处按中文文档习惯保留中文)
_ANCHOR_STRIP = re.compile(r"[^\w\u4e00-\u9fff\s-]")
def slug(title: str) -> str:
"""把 Markdown 标题转成锚点 id,规则与 GitHub/Gitea 一致。"""
text = title.strip().lower()
text = _ANCHOR_STRIP.sub("", text)
return text.replace(" ", "-")
def collect_anchors(path: Path) -> set[str]:
"""收集文件中全部标题的锚点,用于校验 #片段。"""
anchors: set[str] = set()
for line in path.read_text(encoding="utf-8").splitlines():
if line.startswith("#"):
anchors.add(slug(line.lstrip("#").strip()))
return anchors
def main() -> int:
root = Path(__file__).resolve().parent.parent
files = [root / "README.md", root / "AGENTS.md", *sorted((root / "docs").glob("*.md"))]
problems: list[str] = []
for md in files:
if not md.is_file():
problems.append(f"缺少文档:{md.relative_to(root)}")
continue
for target in LINK_RE.findall(md.read_text(encoding="utf-8")):
if target.startswith(("http://", "https://", "mailto:")):
continue
path_part, _, anchor = target.partition("#")
resolved = (md.parent / path_part).resolve() if path_part else md.resolve()
# 目录链接(如 ./docs/)视为可达,只要求路径存在
if path_part and not resolved.exists():
problems.append(f"{md.relative_to(root)} -> 文件不存在:{target}")
continue
if anchor and resolved.suffix == ".md" and anchor not in collect_anchors(resolved):
problems.append(f"{md.relative_to(root)} -> 锚点不存在:{target}")
if problems:
print("\n".join(problems))
print(f"\n{len(problems)} 处断链")
return 1
print(f"检查 {len(files)} 个文档,全部链接与锚点可达")
return 0
if __name__ == "__main__":
sys.exit(main())