← Files Marketing CouncilARCHIVED FILE

scripts/validate_documentation.py

8.2 KB · Oct 2, 2026 · 00:31 UTC

↓ Download file

#!/usr/bin/env python3
"""Validate documentation lifecycle metadata, registry coverage, and internal links."""
from __future__ import annotations

import argparse
import json
import re
from datetime import date
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
REGISTRY = ROOT / "docs" / "document-registry.json"
ALLOWED = {
    "ACTIVE",
    "ONGOING",
    "HISTORICAL_COMPLETED",
    "HISTORICAL_SUPERSEDED",
    "REFERENCE",
    "DEPRECATED",
}
ONGOING_FIELDS = (
    "Status",
    "Current state",
    "Completed work",
    "Remaining work",
    "Blockers",
    "Next verification step",
    "Source-of-truth references",
    "Last reviewed date",
)
LINK_RE = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
METADATA_RE = re.compile(r"<!--\s*(.*?)\s*-->", re.DOTALL)


def add(errors: list[str], condition: bool, message: str) -> None:
    if not condition:
        errors.append(message)


def valid_review_date(value: object) -> bool:
    if not isinstance(value, str):
        return False
    try:
        parsed = date.fromisoformat(value)
    except ValueError:
        return False
    return parsed.isoformat() == value


def parse_document_metadata(text: str) -> dict[str, str]:
    match = METADATA_RE.search(text)
    if not match:
        return {}
    metadata: dict[str, str] = {}
    for raw_line in match.group(1).splitlines():
        line = raw_line.strip()
        if not line or ":" not in line or line.startswith("-"):
            continue
        key, value = line.split(":", 1)
        metadata[key.strip()] = value.strip()
    return metadata


def review_date_errors(item: dict, metadata: dict[str, str]) -> list[str]:
    rel = item.get("path")
    registry_date = item.get("last_reviewed")
    document_date = metadata.get("last_reviewed")
    errors: list[str] = []
    if not valid_review_date(registry_date):
        errors.append(f"{rel}: registry last_reviewed must be a valid ISO date")
    if not valid_review_date(document_date):
        errors.append(f"{rel}: document last_reviewed must be a valid ISO date")
    if valid_review_date(registry_date) and valid_review_date(document_date) and registry_date != document_date:
        errors.append(
            f"{rel}: registry last_reviewed {registry_date} does not match document metadata {document_date}"
        )
    return errors


def validate() -> dict:
    errors: list[str] = []
    if not REGISTRY.is_file():
        return {"ok": False, "document_count": 0, "errors": ["docs/document-registry.json missing"]}

    try:
        payload = json.loads(REGISTRY.read_text(encoding="utf-8"))
    except Exception as exc:
        return {"ok": False, "document_count": 0, "errors": [f"registry unreadable: {exc}"]}

    add(errors, payload.get("version") == 1, "registry version must be 1")
    add(errors, valid_review_date(payload.get("last_reviewed")), "registry last_reviewed must be a valid ISO date")
    documents = payload.get("documents", [])
    paths = [item.get("path") for item in documents]
    add(errors, len(paths) == len(set(paths)), "registry contains duplicate document paths")

    actual = {
        path.relative_to(ROOT).as_posix()
        for path in (ROOT / "docs").rglob("*.md")
    }
    add(errors, set(paths) == actual, f"registry/docs markdown mismatch: registry={sorted(paths)} actual={sorted(actual)}")

    for item in documents:
        rel = item.get("path")
        status = item.get("status")
        source = item.get("source_of_truth")
        add(errors, status in ALLOWED, f"{rel}: invalid status {status!r}")
        add(errors, isinstance(source, bool), f"{rel}: source_of_truth must be boolean")
        if not isinstance(rel, str):
            continue
        path = ROOT / rel
        add(errors, path.is_file(), f"{rel}: document missing")
        if not path.is_file():
            continue
        text = path.read_text(encoding="utf-8")
        metadata = parse_document_metadata(text)
        errors.extend(review_date_errors(item, metadata))
        add(errors, f"doc_status: {status}" in text, f"{rel}: doc_status metadata mismatch")
        add(errors, f"source_of_truth: {str(source).lower()}" in text, f"{rel}: source_of_truth metadata mismatch")

        if isinstance(status, str) and status.startswith("HISTORICAL_"):
            add(errors, source is False, f"{rel}: historical document cannot be source of truth")
            add(errors, "Current source of truth" in text, f"{rel}: historical document must identify current source of truth")

        if status == "HISTORICAL_SUPERSEDED":
            replacements = item.get("superseded_by", [])
            add(errors, bool(replacements), f"{rel}: superseded document requires superseded_by")
            add(errors, "Do not execute this" in text, f"{rel}: superseded document requires execution warning")
            for replacement in replacements:
                add(errors, (ROOT / replacement).is_file(), f"{rel}: superseded_by target missing: {replacement}")

        if status == "ONGOING":
            for field in ONGOING_FIELDS:
                add(errors, field in text, f"{rel}: ongoing document missing {field}")

        for raw in LINK_RE.findall(text):
            target = raw.split("#", 1)[0]
            if not target or "://" in target or target.startswith("#"):
                continue
            resolved = (path.parent / target).resolve()
            add(errors, resolved.is_file() or resolved.is_dir(), f"{rel}: broken relative link {raw}")

    index = ROOT / "docs" / "README.md"
    if index.is_file():
        text = index.read_text(encoding="utf-8")
        for heading in (
            "Start here",
            "Current product documentation",
            "Ongoing work",
            "Architecture and contracts",
            "Release and distribution",
            "Historical specs",
            "Historical implementation plans",
        ):
            add(errors, heading in text, f"docs/README.md missing section: {heading}")

    manifest = json.loads((ROOT / ".codex-plugin" / "plugin.json").read_text(encoding="utf-8"))
    version = manifest.get("version")
    release_doc = ROOT / "docs" / "OPENAI_RELEASE.md"
    add(errors, isinstance(version, str) and bool(version), "plugin version missing")
    if release_doc.is_file() and isinstance(version, str):
        release_text = release_doc.read_text(encoding="utf-8")
        add(errors, version in release_text, f"docs/OPENAI_RELEASE.md must reference current version {version}")

    submission = ROOT / "submission" / "README.md"
    if submission.is_file():
        submission_text = submission.read_text(encoding="utf-8")
        submission_metadata = parse_document_metadata(submission_text)
        add(errors, "doc_status: ONGOING" in submission_text, "submission/README.md must be ONGOING")
        add(errors, "source_of_truth: false" in submission_text, "submission/README.md cannot be source of truth")
        add(
            errors,
            valid_review_date(submission_metadata.get("last_reviewed")),
            "submission/README.md last_reviewed must be a valid ISO date",
        )
        for field in ONGOING_FIELDS:
            add(errors, field in submission_text, f"submission/README.md missing {field}")

    valid_item_dates = [
        item.get("last_reviewed")
        for item in documents
        if valid_review_date(item.get("last_reviewed"))
    ]
    if valid_item_dates and valid_review_date(payload.get("last_reviewed")):
        add(
            errors,
            payload.get("last_reviewed") == max(valid_item_dates),
            "registry top-level last_reviewed must match the latest managed document review date",
        )

    return {
        "ok": not errors,
        "document_count": len(documents),
        "current_release": version,
        "errors": errors,
    }


def main() -> None:
    parser = argparse.ArgumentParser(description="Validate Marketing Council documentation lifecycle.")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    report = validate()
    if args.json:
        print(json.dumps(report, indent=2, sort_keys=True))
    else:
        print("documentation lifecycle: " + ("PASS" if report["ok"] else "FAIL"))
        for error in report["errors"]:
            print(f"error: {error}")
    raise SystemExit(0 if report["ok"] else 1)


if __name__ == "__main__":
    main()

SHA-256: 5a75aace81f221f96a5dc7356bc2743154ddee47ce20f46a4bd5ec0d940dba6f