File size: 4,424 Bytes
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""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 <target>` 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 <target>`."""
    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())