"""Docs-consistency checker used by CI (.github/workflows/docs.yml) and a pytest wrapper (tests/test_docs.py). Checks, over a curated set of public docs: 1. Every relative markdown link points to a file that exists. 2. Every `make ` referenced in those docs exists in the Makefile. 3. env.example documents the environment variables the getting-started / Hugging Face docs tell people to set. Exits non-zero and prints every problem if anything fails. Pure stdlib; runs offline at $0. """ from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent # Public docs we hold to the link/target contract. Deliberately curated — not # every file under docs/ (some are private research notes). DOCS = [ "README.md", "RUNNING.md", "docs/getting-started/no-podman.md", "docs/getting-started/podman.md", "docs/getting-started/server.md", "docs/getting-started/huggingface-space.md", "docs/getting-started/troubleshooting.md", "docs/evaluation.md", "docs/benchmarks.md", "examples/demo_corpus/README.md", ] # Env vars the HF/getting-started docs promise exist; env.example must list them. REQUIRED_ENV_VARS = [ "AURALYNQ_HF_SPACE", "AURALYNQ_DEMO_MODE", "AURALYNQ_ALLOW_UPLOADS", "AURALYNQ_LLM__PROVIDER", "AURALYNQ_VECTOR__BACKEND", "AURALYNQ_EMBEDDING__PROVIDER", "AURALYNQ_SERVE__API_KEY", ] _LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") _MAKE = re.compile(r"\bmake\s+([a-zA-Z][a-zA-Z0-9_-]*)") _FENCE = re.compile(r"```.*?```", re.S) _INLINE_CODE = re.compile(r"`([^`\n]+)`") def _code_spans(text: str) -> str: """Concatenate only the code portions of a markdown doc (fenced blocks + inline code), so `make sure` in prose isn't mistaken for `make `.""" parts = _FENCE.findall(text) parts += _INLINE_CODE.findall(text) return "\n".join(parts) def _make_targets() -> set[str]: text = (ROOT / "Makefile").read_text(encoding="utf-8") # Match ".PHONY: name" and "name:" target definitions. targets: set[str] = set() for m in re.finditer(r"^\.PHONY:\s*(.+)$", text, re.M): targets.update(m.group(1).split()) for m in re.finditer(r"^([a-zA-Z][a-zA-Z0-9_-]*)\s*:", text, re.M): targets.add(m.group(1)) return targets def check_links(errors: list[str]) -> None: for rel in DOCS: doc = ROOT / rel if not doc.exists(): errors.append(f"{rel}: listed in check_docs but the file is missing") continue base = doc.parent for link in _LINK.findall(doc.read_text(encoding="utf-8")): target = link.split("#", 1)[0].strip() if not target or target.startswith(("http://", "https://", "mailto:")): continue resolved = (base / target).resolve() if not resolved.exists(): errors.append(f"{rel}: broken relative link -> {target}") def check_make_targets(errors: list[str]) -> None: targets = _make_targets() # Env-style make invocations we intentionally skip (they're commands users # run, not always literal targets, e.g. "make stack-up" IS a target though). for rel in DOCS: doc = ROOT / rel if not doc.exists(): continue code = _code_spans(doc.read_text(encoding="utf-8")) for tgt in _MAKE.findall(code): if tgt not in targets: errors.append(f"{rel}: references `make {tgt}` but no such Makefile target") def check_env_example(errors: list[str]) -> None: env = ROOT / ".env.example" hf_env = ROOT / "deploy/huggingface/env.example" corpus = "" for p in (env, hf_env): if p.exists(): corpus += p.read_text(encoding="utf-8") if not corpus: errors.append(".env.example and deploy/huggingface/env.example both missing") return for var in REQUIRED_ENV_VARS: if var not in corpus: errors.append(f"env example files do not document {var}") def main() -> int: errors: list[str] = [] check_links(errors) check_make_targets(errors) check_env_example(errors) if errors: print("✗ docs check FAILED:") for e in sorted(set(errors)): print(f" - {e}") return 1 print("✓ docs check passed") return 0 if __name__ == "__main__": sys.exit(main())