scripts/check-doc-links.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""Validate public documentation and its bilingual test checklist."""
import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
PUBLIC_DOCS = REPO / "public-docs"
# A page is capped so it stays readable in one sitting, not to make authors
# compress sentences: a contents table, one-fact lines, and a diagram all cost
# lines that a reader gets back.
MAX_LINES = 300
# API is the contract page; the complete test checklists also need room for
# every case and the commands needed to run them.
LINE_LIMIT_EXEMPT = frozenset({
"reference/api.md",
"development/testing.md",
"development/testing.ko.md",
})
KOREAN_ALLOWED = frozenset({"development/testing.ko.md"})
LINK_RE = re.compile(r"\]\(\s*<?([^)>\s]+)>?(?:\s+\"[^\"]*\")?\s*\)")
PUBLIC_PATH_RE = re.compile(r"public-docs/[\w./-]+\.(?:md|py|sh)")
LEGACY_PATH_RE = re.compile(r"(?<!public-)docs/[\w./-]+\.(?:md|py|sh)")
KOREAN_RE = re.compile(r"[가-힣]")
SKIP_PREFIXES = ("http://", "https://", "mailto:", "#", "data:", "tel:")
IGNORED_PARTS = {"node_modules", "target", ".git", "__pycache__"}
SOURCE_GLOBS = [
"firecrab-api/src/**/*.rs",
"firecrab-api-types/src/**/*.rs",
"firecrab-helper-protocol/src/**/*.rs",
"firecrab-helper/src/**/*.rs",
"firecrab-frontend/src/**/*.ts",
"firecrab-frontend/src/**/*.tsx",
"firecrab-e2e/types/**/*.ts",
"scripts/**/*.sh",
"scripts/**/*.py",
"install.sh",
"README*.md",
".github/workflows/*.yml",
]
def is_ignored(path: Path) -> bool:
return any(part in IGNORED_PARTS for part in path.parts)
def check_public_docs() -> list[str]:
problems: list[str] = []
root = PUBLIC_DOCS.resolve()
for doc in sorted(PUBLIC_DOCS.rglob("*.md")):
relative = doc.relative_to(REPO)
public_path = doc.relative_to(PUBLIC_DOCS).as_posix()
if doc.is_symlink():
target = doc.resolve()
if not target.is_file():
problems.append(f"{relative}: broken symbolic link")
elif not target.is_relative_to(root):
problems.append(f"{relative}: symbolic link leaves public-docs")
continue
text = doc.read_text(encoding="utf-8")
lines = text.splitlines()
if public_path not in LINE_LIMIT_EXEMPT and len(lines) > MAX_LINES:
problems.append(f"{relative}: {len(lines)} lines exceeds {MAX_LINES}")
for line_number, line in enumerate(lines, start=1):
if public_path not in KOREAN_ALLOWED and KOREAN_RE.search(line):
problems.append(
f"{relative}:{line_number}: Korean text is only allowed in "
"development/testing.ko.md"
)
in_fenced_code = False
for line_number, line in enumerate(lines, start=1):
if re.match(r"^\s*(`{3,}|~{3,})", line):
in_fenced_code = not in_fenced_code
continue
if in_fenced_code:
continue
for target in LINK_RE.findall(line):
if target.startswith(SKIP_PREFIXES):
continue
path_part = target.split("#", 1)[0]
if not path_part:
continue
if not (doc.parent / path_part).resolve().exists():
problems.append(f"{relative}:{line_number}: broken link {target}")
return problems
def check_source_references() -> list[str]:
problems: list[str] = []
seen: set[Path] = set()
for pattern in SOURCE_GLOBS:
for path in sorted(REPO.glob(pattern)):
if is_ignored(path) or path in seen:
continue
seen.add(path)
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
for line_number, line in enumerate(text.splitlines(), start=1):
for reference in LEGACY_PATH_RE.findall(line):
relative = path.relative_to(REPO)
problems.append(f"{relative}:{line_number}: private docs reference {reference}")
for reference in PUBLIC_PATH_RE.findall(line):
if not (REPO / reference).exists():
relative = path.relative_to(REPO)
problems.append(f"{relative}:{line_number}: broken reference {reference}")
return problems
def main() -> int:
problems = check_public_docs() + check_source_references()
if problems:
print(f"Public documentation check failed with {len(problems)} problem(s).")
for problem in problems:
print(f" {problem}")
return 1
print(
"Public documentation links and language rules pass; files are at most "
f"{MAX_LINES} lines except {', '.join(sorted(LINE_LIMIT_EXEMPT))}."
)
return 0
if __name__ == "__main__":
sys.exit(main())