mirror of
https://github.com/tradecatlabs/vibe-coding-cn.git
synced 2026-08-09 00:57:45 +00:00
docs: align onboarding and harden docs checks
This commit is contained in:
+2
-1
@@ -1,10 +1,11 @@
|
||||
# scripts
|
||||
|
||||
本目录存放仓库级自动化脚本,例如链接检查、索引生成、taxonomy 校验和迁移辅助脚本。
|
||||
本目录存放仓库级自动化脚本,例如链接检查、索引生成、taxonomy 校验和文档结构校验脚本。
|
||||
|
||||
当前已有:
|
||||
|
||||
- `check-local-links.py`:仓库内 Markdown 相对链接与锚点检查脚本。
|
||||
- `check-markdown-details.py`:仓库内 Markdown `<details>/<summary>` 折叠块结构检查脚本。
|
||||
- `check-doc-structure.py`:`docs/` 线性 README 的主章节顺序、重复锚点与细粒度目录入口检查脚本。
|
||||
- `check-metadata.py`:`metadata/taxonomy.yml` 与 `metadata/redirects.yml` 路径和锚点检查脚本。
|
||||
- `check-ai-citation.py`:`llms.txt`、`assets/ai-citation/llms-full.txt` 与 AI 引用语料路径和锚点检查脚本。
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check docs README structure, stable anchors and duplicate manual anchors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOC_READMES: dict[Path, list[str]] = {
|
||||
Path("docs/getting-started/README.md"): [
|
||||
"vibe-coding-experience",
|
||||
"learning-map",
|
||||
"network-environment",
|
||||
"cli-setup",
|
||||
"development-environment",
|
||||
],
|
||||
Path("docs/concepts/README.md"): [
|
||||
"concept-problem-solving",
|
||||
"concept-glue-coding",
|
||||
"concept-system-building",
|
||||
"concept-development-paradigms",
|
||||
"concept-language-layers",
|
||||
"concept-recursive-self-optimizing-system",
|
||||
],
|
||||
Path("docs/philosophy/README.md"): [
|
||||
"philosophy-thinking-models",
|
||||
"philosophy-compositional-description-model",
|
||||
"philosophy-programming-dao",
|
||||
"philosophy-methodology-toolbox",
|
||||
],
|
||||
Path("docs/references/README.md"): [
|
||||
"reference-engineering-practice",
|
||||
"reference-technology-stack",
|
||||
],
|
||||
Path("docs/research/README.md"): [
|
||||
"research-harness-engineering",
|
||||
],
|
||||
}
|
||||
SKIP_PARTS = {".git", ".history", "node_modules"}
|
||||
SKIP_PREFIXES = [
|
||||
Path(".github/wiki"),
|
||||
Path("tools/external"),
|
||||
]
|
||||
ANCHOR_PATTERN = re.compile(r"<a\s+id=[\"']([^\"']+)[\"']")
|
||||
|
||||
|
||||
def strip_fenced_code(text: str) -> str:
|
||||
lines: list[str] = []
|
||||
in_fence = False
|
||||
fence_marker = ""
|
||||
|
||||
for line in text.splitlines():
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith(("```", "~~~")):
|
||||
marker = stripped[:3]
|
||||
if not in_fence:
|
||||
in_fence = True
|
||||
fence_marker = marker
|
||||
elif marker == fence_marker:
|
||||
in_fence = False
|
||||
fence_marker = ""
|
||||
lines.append("")
|
||||
continue
|
||||
lines.append("" if in_fence else line)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def should_skip(path: Path) -> bool:
|
||||
rel = path.relative_to(ROOT)
|
||||
if any(part in SKIP_PARTS for part in rel.parts):
|
||||
return True
|
||||
return any(rel == prefix or prefix in rel.parents for prefix in SKIP_PREFIXES)
|
||||
|
||||
|
||||
def duplicate_manual_anchors(path: Path) -> list[str]:
|
||||
text = strip_fenced_code(path.read_text(encoding="utf-8", errors="ignore"))
|
||||
seen: dict[str, int] = {}
|
||||
errors: list[str] = []
|
||||
|
||||
for lineno, line in enumerate(text.splitlines(), start=1):
|
||||
for anchor in ANCHOR_PATTERN.findall(line):
|
||||
if anchor in seen:
|
||||
rel = path.relative_to(ROOT)
|
||||
errors.append(f"{rel}:{lineno}: duplicate manual anchor '{anchor}', first seen at line {seen[anchor]}")
|
||||
else:
|
||||
seen[anchor] = lineno
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_linear_readme(path: Path, expected_anchors: list[str]) -> list[str]:
|
||||
rel = path.relative_to(ROOT)
|
||||
text = strip_fenced_code(path.read_text(encoding="utf-8", errors="ignore"))
|
||||
errors: list[str] = []
|
||||
|
||||
if "完整细粒度目录(点击展开/收起)" not in text:
|
||||
errors.append(f"{rel}: missing collapsible full fine-grained table of contents")
|
||||
|
||||
if "<details>" not in text or "</details>" not in text:
|
||||
errors.append(f"{rel}: missing details block for long-form navigation")
|
||||
|
||||
anchor_lines: dict[str, int] = {}
|
||||
for lineno, line in enumerate(text.splitlines(), start=1):
|
||||
for anchor in ANCHOR_PATTERN.findall(line):
|
||||
anchor_lines.setdefault(anchor, lineno)
|
||||
|
||||
previous_line = 0
|
||||
for anchor in expected_anchors:
|
||||
line = anchor_lines.get(anchor)
|
||||
if line is None:
|
||||
errors.append(f"{rel}: missing required main anchor '{anchor}'")
|
||||
continue
|
||||
if line <= previous_line:
|
||||
errors.append(f"{rel}:{line}: main anchor '{anchor}' is out of expected section order")
|
||||
previous_line = line
|
||||
|
||||
toc_boundary = min((anchor_lines.get(anchor, len(text.splitlines()) + 1) for anchor in expected_anchors), default=0)
|
||||
toc_text = "\n".join(text.splitlines()[:toc_boundary])
|
||||
for anchor in expected_anchors:
|
||||
if f"#{anchor}" not in toc_text:
|
||||
errors.append(f"{rel}: top navigation or fine-grained TOC does not link to '#{anchor}'")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
checked_files = 0
|
||||
|
||||
for markdown_file in sorted(ROOT.rglob("*.md")):
|
||||
if should_skip(markdown_file):
|
||||
continue
|
||||
checked_files += 1
|
||||
errors.extend(duplicate_manual_anchors(markdown_file))
|
||||
|
||||
for rel_path, expected_anchors in DOC_READMES.items():
|
||||
path = ROOT / rel_path
|
||||
if not path.exists():
|
||||
errors.append(f"{rel_path}: missing docs README")
|
||||
continue
|
||||
errors.extend(check_linear_readme(path, expected_anchors))
|
||||
|
||||
if errors:
|
||||
print("DOC_STRUCTURE_ERRORS")
|
||||
for error in errors:
|
||||
print(error)
|
||||
print(f"TOTAL={len(errors)}")
|
||||
return 1
|
||||
|
||||
print(f"OK docs structure checked: {checked_files} markdown files")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SKIP_PARTS = {".git", ".history"}
|
||||
SKIP_PARTS = {".git", ".history", "node_modules"}
|
||||
SKIP_PREFIXES = [
|
||||
Path(".github/wiki"),
|
||||
Path("tools/external"),
|
||||
|
||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SKIP_PARTS = {".git", ".history"}
|
||||
SKIP_PARTS = {".git", ".history", "node_modules"}
|
||||
SKIP_PREFIXES = [
|
||||
Path(".github/wiki"),
|
||||
Path("tools/external"),
|
||||
|
||||
Reference in New Issue
Block a user