system-prompts-and-models-o.../scripts/check_docs_links.py
Sami Assiri b4531f0a4c feat(tier1): docs-governance CI, evidence gate, closure artifacts, trust/execution docs
- Replace repo-preflight with docs-governance workflow and check_docs_links.py
- Class B bundle: require correlation_id for external_*; AuditMetadata trace fields
- Root-safe TIER1 §2; optional .githooks pre-push for main
- Add RELEASE_READINESS_MATRIX_AR, SOURCE_OF_TRUTH_INDEX, operational severity, external index
- ExecWeeklyGovernanceContract; expand trust-fabric, execution-fabric, ADR-0001, ws5, Saudi overlays
- Wire MASTER TOC, enterprise-readiness, completion-program, architecture_brief paths

Made-with: Cursor
2026-04-16 16:46:36 +03:00

82 lines
2.4 KiB
Python

#!/usr/bin/env python3
"""Verify relative markdown links from repo root (Tier-1 docs governance CI)."""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
# Markdown files to scan (repo-grounded governance spine)
GLOBS = [
"docs/**/*.md",
"MASTER_OPERATING_PROMPT.md",
"AGENTS.md",
"CLAUDE.md",
"Execution_Matrix.md",
"Execution_Matrix_v2.md",
"Architecture_Pack.md",
]
def collect_files() -> list[Path]:
out: list[Path] = []
for g in GLOBS:
if "**" in g:
out.extend(p for p in ROOT.glob(g) if p.is_file())
else:
p = ROOT / g
if p.is_file():
out.append(p)
return sorted(set(out))
def resolve_link(source: Path, raw_target: str) -> Path | None:
target = raw_target.split("#", 1)[0].strip()
if not target or target.startswith(("http://", "https://", "mailto:", "vscode:", "file:")):
return None
if target.startswith("/"):
return (ROOT / target.lstrip("/")).resolve()
base = source.parent
return (base / target).resolve()
def main() -> int:
errors: list[str] = []
for md in collect_files():
try:
text = md.read_text(encoding="utf-8")
except OSError as e:
errors.append(f"{md.relative_to(ROOT)}: read error {e}")
continue
for m in LINK_RE.finditer(text):
raw = m.group(1).strip()
if "`" in raw or raw.startswith("{{"):
continue
resolved = resolve_link(md, raw)
if resolved is None:
continue
try:
resolved.relative_to(ROOT)
except ValueError:
errors.append(f"{md.relative_to(ROOT)}: escape path {raw}")
continue
if not resolved.exists():
errors.append(f"{md.relative_to(ROOT)}: broken link -> {raw} (resolved {resolved.relative_to(ROOT)})")
if errors:
print("Docs link check FAILED:\n", file=sys.stderr)
for e in errors[:80]:
print(f" {e}", file=sys.stderr)
if len(errors) > 80:
print(f" ... and {len(errors) - 80} more", file=sys.stderr)
return 1
print(f"Docs link check OK ({len(collect_files())} files scanned)")
return 0
if __name__ == "__main__":
sys.exit(main())