File size: 4,993 Bytes
31226fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
135
136
137
138
139
140
141
"""Validate documentation links and canonical baseline claims."""

from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from urllib.parse import unquote

REPO_ROOT = Path(__file__).resolve().parents[1]
DOCS_ROOT = REPO_ROOT / "docs"
ROADMAP_DOCS = (
    DOCS_ROOT / "08-roadmap" / "TODO.md",
    DOCS_ROOT / "08-roadmap" / "ROADMAP.md",
)
MARKDOWN_LINK = re.compile(r"(?<!!)\[[^\]]+\]\(([^)]+)\)")
BASELINE_PATTERN = re.compile(
    r"\*\*Baseline:\*\*\s+`?(?:brain5d-core|mhrn-core)\s+([^`\s]+)`?"
)
COLLECTED_PATTERN = re.compile(r"(\d+)\s+tests collected")


def markdown_files() -> tuple[Path, ...]:
    """Return tracked documentation files in stable order."""
    return tuple(sorted(DOCS_ROOT.rglob("*.md")))


def check_markdown_links(files: tuple[Path, ...], root: Path = REPO_ROOT) -> list[str]:
    """Return one error for every missing local Markdown link target."""
    root = root.resolve()
    errors: list[str] = []
    for source in files:
        source = source.resolve()
        text = source.read_text(encoding="utf-8")
        for match in MARKDOWN_LINK.finditer(text):
            target = match.group(1).strip().strip("<>")
            target = target.split("#", 1)[0].strip()
            if not target or target.startswith(
                ("http://", "https://", "mailto:", "data:")
            ):
                continue
            candidate = (source.parent / unquote(target)).resolve()
            try:
                candidate.relative_to(root)
            except ValueError:
                errors.append(f"{source}: link escapes repository: {target}")
                continue
            if not candidate.exists():
                errors.append(f"{source}: missing link target: {target}")
    return errors


def check_baseline_version() -> list[str]:
    """Ensure the canonical roadmap baseline matches ``pyproject.toml``."""
    pyproject = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")
    version_match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject, re.MULTILINE)
    if version_match is None:
        return ["pyproject.toml: canonical project version is missing"]
    expected = version_match.group(1)
    errors: list[str] = []
    for path in ROADMAP_DOCS:
        text = path.read_text(encoding="utf-8")
        match = BASELINE_PATTERN.search(text)
        if match is None:
            errors.append(f"{path.relative_to(REPO_ROOT)}: baseline claim is missing")
        elif match.group(1) != expected:
            errors.append(
                f"{path.relative_to(REPO_ROOT)}: baseline {match.group(1)} "
                f"does not match {expected}"
            )
    return errors


def check_collected_test_count() -> list[str]:
    """Reject test-collection regressions relative to the canonical baseline.

    ``tests/test_baseline.json`` records a verified historical run. Adding new
    tests must not make documentation consistency fail merely because that
    immutable snapshot is older than the current tree. A lower current count,
    however, remains a regression and fails the gate.
    """
    result = subprocess.run(
        [sys.executable, "-m", "pytest", "--collect-only", "-q"],
        cwd=REPO_ROOT,
        capture_output=True,
        text=True,
        check=False,
    )
    output = f"{result.stdout}\n{result.stderr}"
    actual_match = COLLECTED_PATTERN.search(output)
    if actual_match is None:
        return ["pytest collection did not report a collected test count"]

    if result.returncode != 0:
        return ["pytest collection failed; a partial count is not a valid baseline"]
    baseline_path = REPO_ROOT / "tests" / "test_baseline.json"
    try:
        baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
        declared = int(baseline["full_collection"]["collected"])
    except (OSError, ValueError, KeyError, TypeError):
        return ["tests/test_baseline.json: canonical collected-test snapshot missing"]

    actual = int(actual_match.group(1))
    if actual < declared:
        return [
            "tests/test_baseline.json: test collection regressed "
            f"from baseline {declared} to {actual}"
        ]

    return []


def main() -> int:
    """Run documentation checks and return a shell-friendly status code."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--check-tests",
        action="store_true",
        help="also ensure pytest collection has not regressed below the baseline",
    )
    args = parser.parse_args()

    errors = check_markdown_links(markdown_files())
    errors.extend(check_baseline_version())
    if args.check_tests:
        errors.extend(check_collected_test_count())

    if errors:
        for error in errors:
            print(f"ERROR: {error}")
        return 1
    print("Documentation consistency checks passed.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())