scripts.doc_lint

Reference › scripts › doc_lint

The doc-truth lint: it catches hand-written documentation claims that drift from the facts. tests/scripts/test_doc_lint.py imports it; a gate or a human runs it.

Invocation: main takes any number of files or directories (default docs/ and README.md), --warn-only (report findings but exit 0) and --incus-json FILE (read the Incus ground truth from a file instead of running incus list). If the ground truth is unavailable, meaning no live Incus and no JSON file, it fails with exit 2 even under --warn-only.

lint scans the Markdown files (iter_markdown_files) with three checks. check_stale_infra flags stale infrastructure references. check_test_counts compares test counts quoted in the documentation with the actual count read from a run-tests JSON file (load_actual_test_count). check_container_ips compares IP addresses quoted in the documentation with the container addresses known to Incus (load_incus_ips). A line can be marked exempt (_is_exempt). Each failure is a Finding, and the summary line counts failures, exempt lines and files.

At a glance

Source scripts/doc_lint.py
Lines of code 337
Classes 2
Functions 9
Imports (internal) none
Imported by none
Tests that import it tests/scripts/test_doc_lint.py

Class diagram

Class diagram scripts.doc_lint

Dependency diagram

No internal imports and no importers.

API

Constants

Name Type Value
_STALE_PATTERNS tuple[tuple[str, re.Pattern[str]], ...] (('10.222.9.x IP', re.compile('\\b10\\.222\\.9\\.\\d+\\b')), ('swiplserver', re…

Classes

Finding (dataclass)

Decorators: dataclass · Line 96

Attribute Type Default
path Path
line_no int
check str
detail str
Method Signature Summary
format format() -> str

LintReport (dataclass)

Decorators: dataclass · Line 107

Attribute Type Default
findings list[Finding]
exempt_count int
file_count int
ground_truth_available bool

Functions

Function Signature Summary
iter_markdown_files iter_markdown_files(paths: list[Path]) -> list[Path] Expand CLI path args (files or directories) into a sorted, de-duplicated list of .md files.
_is_exempt _is_exempt(lines: list[str], idx: int) -> bool idx is the 0-based index of the line under test.
check_stale_infra check_stale_infra(path: Path, lines: list[str]) -> tuple[list[Finding], int] Returns (findings, exempt_count) — exempt_count is the number of matches that WOULD have been findings but were exempted, for the summary line.
load_actual_test_count load_actual_test_count(run_tests_json: Path) -> int Real test total = passed + failed, read from run-tests-latest.json. (The file's only count keys are aborted/passed/failed — there is no total key, so the real count a doc claim means…
check_test_counts check_test_counts(path: Path, lines: list[str], actual: int) -> list[Finding]
load_incus_ips load_incus_ips(incus_json_path: Path | None) -> set[str] | None Return the set of live IPv4 addresses known to incus ground truth, or None if ground truth could not be obtained from either source (caller must then exit 2 — never silently pass).
check_container_ips check_container_ips(path: Path, lines: list[str], known_ips: set[str]) -> list[Finding]
lint lint(paths: list[Path], incus_json_path: Path | None) -> LintReport
main main(argv: list[str] | None = None) -> int

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/doc_lint.py

#!/usr/bin/env python3
"""Doc-truth lint — catches hand-written documentation claims drifting from
ground reality (the project's corrections-register defect-#6 class:
hand-written claims drifting from reality; see ``plans/tooling-wiring.md``).

MVP scope — exactly three checks, each comparing a doc claim to a live
ground-truth source, never to another document:

1. STALE-INFRA PHRASES — ``docs/**/*.md`` + ``README.md`` scanned for
   retired-infrastructure references: IPs matching ``10.222.9.\\d+``, the
   phrase ``swiplserver`` or ``MQI`` presented as the CURRENT bridge, and
   ``gatekeeper-neo4j`` presented as live. A line (or its containing
   markdown table row — table rows are one physical line) that also
   contains "stale", "was", "superseded", "destroyed", "delta",
   "historical", or "originally" is EXEMPT — reconciliation tables
   legitimately quote old values. An inline ``<!-- doc-lint: allow -->``
   comment on the same or the immediately preceding line is also an
   exemption.

2. TEST-COUNT CLAIMS — ``\\b([0-9]{1,3}(?:,[0-9]{3})+|[0-9]{4,})\\s+tests\\b``
   claims are compared against the real total (``passed`` + ``failed``)
   read from ``reports/run-tests-latest.json``. A claim differing from
   reality by more than 10% is a FAIL naming file:line, claimed, actual.

3. CONTAINER/IP CLAIMS — every ``10.53.1.\\d+`` mention is checked against
   live ``incus list --format json`` output (or ``--incus-json PATH`` for
   tests/CI). If neither ground truth is reachable the run exits 2 with
   "ground truth unavailable" — it never silently passes.

Output is one line per finding: ``FAIL <file>:<line> <check>: <detail>``,
followed by a summary line ``doc-lint: N FAIL, M exempt, K files``.

Usage:
    python scripts/doc_lint.py [--warn-only] [--incus-json PATH] [paths...]

Exit codes: 0 clean, 1 findings (unless ``--warn-only``), 2 ground-truth
unavailable (unconditional — ``--warn-only`` does not suppress this).
"""

from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
RUN_TESTS_LATEST_JSON = REPO_ROOT / "reports" / "run-tests-latest.json"
DEFAULT_PATHS = [Path("docs"), Path("README.md")]

# --- check 1: stale-infra phrases -------------------------------------------------

_STALE_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
    ("10.222.9.x IP", re.compile(r"\b10\.222\.9\.\d+\b")),
    ("swiplserver", re.compile(r"swiplserver", re.IGNORECASE)),
    ("MQI", re.compile(r"\bMQI\b")),
    ("gatekeeper-neo4j", re.compile(r"gatekeeper-neo4j", re.IGNORECASE)),
)

_EXEMPTION_KEYWORDS = (
    "stale",
    "was",
    "superseded",
    "destroyed",
    "delta",
    "historical",
    "originally",
)
_EXEMPTION_RE = re.compile(
    r"\b(" + "|".join(_EXEMPTION_KEYWORDS) + r")\b", re.IGNORECASE
)
_INLINE_ALLOW_RE = re.compile(r"<!--\s*doc-lint:\s*allow\s*-->")

# --- check 2: test-count claims ---------------------------------------------------

_TEST_COUNT_RE = re.compile(r"\b([0-9]{1,3}(?:,[0-9]{3})+|[0-9]{4,})\s+tests\b")
_TEST_COUNT_TOLERANCE = 0.10

# --- check 3: container/IP claims -------------------------------------------------

# (?!/\d) skips CIDR network references like 10.53.1.0/24 — those describe
# the subnet, not a claimed container host (false positive found 2026-08-02).
_CONTAINER_IP_RE = re.compile(r"\b10\.53\.1\.\d+\b(?!/\d)")

# Strikethrough (~~...~~) marks a RETIRED/DELETED container row by convention in
# INFRASTRUCTURE.md. A struck IP is explicitly no-longer-live, so it must NOT be
# checked against incus ground truth — that is exactly the record it documents
# (false positive found 2026-08-03: 7 retired Neo4j/Memgraph rows).
_STRIKETHROUGH_RE = re.compile(r"~~.*?~~")


@dataclass
class Finding:
    path: Path
    line_no: int
    check: str
    detail: str

    def format(self) -> str:
        return f"FAIL {self.path}:{self.line_no} {self.check}: {self.detail}"


@dataclass
class LintReport:
    findings: list[Finding]
    exempt_count: int
    file_count: int
    ground_truth_available: bool


def iter_markdown_files(paths: list[Path]) -> list[Path]:
    """Expand CLI path args (files or directories) into a sorted, de-duplicated
    list of ``.md`` files. Directories are scanned recursively; missing paths
    are silently skipped (a default ``docs/`` in a doc-less checkout is not a
    lint error)."""
    files: set[Path] = set()
    for p in paths:
        if p.is_dir():
            files.update(p.rglob("*.md"))
        elif p.is_file():
            files.add(p)
    return sorted(files)


def _is_exempt(lines: list[str], idx: int) -> bool:
    """``idx`` is the 0-based index of the line under test. Exempt if the
    line itself carries an exemption keyword or the inline allow comment, or
    if the immediately preceding line carries the inline allow comment."""
    line = lines[idx]
    if _EXEMPTION_RE.search(line) or _INLINE_ALLOW_RE.search(line):
        return True
    if idx > 0 and _INLINE_ALLOW_RE.search(lines[idx - 1]):
        return True
    return False


def check_stale_infra(path: Path, lines: list[str]) -> tuple[list[Finding], int]:
    """Returns (findings, exempt_count) — exempt_count is the number of
    matches that WOULD have been findings but were exempted, for the summary
    line."""
    findings: list[Finding] = []
    exempt_count = 0
    for i, line in enumerate(lines, start=1):
        matches = [
            (label, m.group(0))
            for label, rx in _STALE_PATTERNS
            if (m := rx.search(line)) is not None
        ]
        if not matches:
            continue
        if _is_exempt(lines, i - 1):
            exempt_count += len(matches)
            continue
        findings.extend(
            Finding(path, i, "STALE-INFRA", f"retired {label} reference {text!r}")
            for label, text in matches
        )
    return findings, exempt_count


def load_actual_test_count(run_tests_json: Path) -> int:
    """Real test total = passed + failed, read from run-tests-latest.json.
    (The file's only count keys are ``aborted``/``passed``/``failed`` — there
    is no ``total`` key, so the real count a doc claim means is the sum of
    the two outcome counts.)"""
    data = json.loads(run_tests_json.read_text(encoding="utf-8"))
    return int(data.get("passed", 0)) + int(data.get("failed", 0))


def check_test_counts(path: Path, lines: list[str], actual: int) -> list[Finding]:
    findings: list[Finding] = []
    for i, line in enumerate(lines, start=1):
        for m in _TEST_COUNT_RE.finditer(line):
            claimed = int(m.group(1).replace(",", ""))
            if actual == 0:
                differs = claimed != 0
            else:
                differs = abs(claimed - actual) / actual > _TEST_COUNT_TOLERANCE
            if differs:
                findings.append(
                    Finding(
                        path,
                        i,
                        "TEST-COUNT",
                        f"claimed {claimed:,} tests, actual {actual:,}",
                    )
                )
    return findings


def load_incus_ips(incus_json_path: Path | None) -> set[str] | None:
    """Return the set of live IPv4 addresses known to incus ground truth, or
    None if ground truth could not be obtained from either source (caller
    must then exit 2 — never silently pass)."""
    if incus_json_path is not None:
        try:
            data = json.loads(incus_json_path.read_text(encoding="utf-8"))
        except OSError:
            return None
        except json.JSONDecodeError:
            return None
    else:
        try:
            result = subprocess.run(
                ["incus", "list", "--format", "json"],
                capture_output=True,
                text=True,
                timeout=30,
                check=False,
            )
        except OSError:
            return None
        except subprocess.TimeoutExpired:
            return None
        if result.returncode != 0:
            return None
        try:
            data = json.loads(result.stdout)
        except json.JSONDecodeError:
            return None

    ips: set[str] = set()
    for container in data:
        state = container.get("state") or {}
        network = state.get("network") or {}
        for iface in network.values():
            for addr in iface.get("addresses") or []:
                if addr.get("family") == "inet" and addr.get("address"):
                    ips.add(addr["address"])
    return ips


def check_container_ips(
    path: Path, lines: list[str], known_ips: set[str]
) -> list[Finding]:
    findings: list[Finding] = []
    for i, line in enumerate(lines, start=1):
        # Drop struck-through spans first: a ~~retired~~ container's IP is not a
        # live-ground-truth claim (line numbers are unaffected — we scan the
        # residue of the same line).
        scan_line = _STRIKETHROUGH_RE.sub("", line)
        for m in _CONTAINER_IP_RE.finditer(scan_line):
            ip = m.group(0)
            if ip not in known_ips:
                findings.append(
                    Finding(
                        path,
                        i,
                        "CONTAINER-IP",
                        f"{ip} not found in incus ground truth",
                    )
                )
    return findings


def lint(paths: list[Path], incus_json_path: Path | None) -> LintReport:
    md_files = iter_markdown_files(paths)

    known_ips = load_incus_ips(incus_json_path)
    if known_ips is None:
        return LintReport(
            findings=[],
            exempt_count=0,
            file_count=len(md_files),
            ground_truth_available=False,
        )

    actual_tests = load_actual_test_count(RUN_TESTS_LATEST_JSON)

    findings: list[Finding] = []
    exempt_count = 0
    for f in md_files:
        lines = f.read_text(encoding="utf-8", errors="replace").splitlines()
        stale_findings, file_exempt = check_stale_infra(f, lines)
        findings.extend(stale_findings)
        exempt_count += file_exempt
        findings.extend(check_test_counts(f, lines, actual_tests))
        findings.extend(check_container_ips(f, lines, known_ips))

    return LintReport(
        findings=findings,
        exempt_count=exempt_count,
        file_count=len(md_files),
        ground_truth_available=True,
    )


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "paths",
        nargs="*",
        type=Path,
        default=[],
        help="Files/directories to scan (default: docs/ README.md)",
    )
    parser.add_argument(
        "--warn-only",
        action="store_true",
        help="Report findings but exit 0 (does not affect the ground-truth-unavailable exit 2)",
    )
    parser.add_argument(
        "--incus-json",
        type=Path,
        default=None,
        help="Read incus ground truth from this JSON file instead of running `incus list`",
    )
    args = parser.parse_args(argv)

    paths = args.paths if args.paths else DEFAULT_PATHS
    report = lint(paths, args.incus_json)

    if not report.ground_truth_available:
        print(
            "FAIL doc-lint: ground truth unavailable "
            "(no live `incus` and no --incus-json provided)",
            file=sys.stderr,
        )
        return 2

    for finding in report.findings:
        print(finding.format())
    print(
        f"doc-lint: {len(report.findings)} FAIL, "
        f"{report.exempt_count} exempt, {report.file_count} files"
    )

    if report.findings and not args.warn_only:
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())