pre-agent-version
This commit is contained in:
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""check-manual-sync.py — enforce AGENTS.md §3.4.
|
||||
|
||||
Every public, architecturally-significant class (declared in an
|
||||
`include/` directory) must have a matching manual section tagged
|
||||
`\\label{cls:ClassName}` in the LaTeX manual sources.
|
||||
|
||||
Exit code 0 if every public class is documented in the manual, 1
|
||||
otherwise (printing the missing ones). Intended for CI, next to the
|
||||
Doxygen check.
|
||||
|
||||
Usage:
|
||||
tools/check-manual-sync.py [--src components] [--manual docs/manual]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# A public class declaration: `class Name {` / `class Name :` / `class Name`
|
||||
# but NOT a forward declaration `class Name;` and NOT `enum class`.
|
||||
CLASS_RE = re.compile(
|
||||
r"^\s*(?:template\s*<[^>]*>\s*)?class\s+([A-Z]\w*)\s*(?:final\s*)?(?:[:{]|$)"
|
||||
)
|
||||
# A pure forward declaration: `class Name;` (optionally templated).
|
||||
FORWARD_RE = re.compile(r"^\s*(?:template\s*<[^>]*>\s*)?class\s+\w+\s*;\s*$")
|
||||
LABEL_RE = re.compile(r"\\label\{cls:(\w+)\}")
|
||||
|
||||
|
||||
def public_classes(src_root: Path) -> dict[str, Path]:
|
||||
"""Collect class names declared under any include/ directory."""
|
||||
found: dict[str, Path] = {}
|
||||
for header in src_root.rglob("*.hpp"):
|
||||
if "include" not in header.parts:
|
||||
continue
|
||||
for line in header.read_text(encoding="utf-8").splitlines():
|
||||
if FORWARD_RE.match(line): # forward declaration
|
||||
continue
|
||||
if "enum class" in line:
|
||||
continue
|
||||
m = CLASS_RE.match(line)
|
||||
if m:
|
||||
found.setdefault(m.group(1), header)
|
||||
return found
|
||||
|
||||
|
||||
def documented_classes(manual_root: Path) -> set[str]:
|
||||
"""Collect class names that have a \\label{cls:...} in the manual."""
|
||||
labelled: set[str] = set()
|
||||
for tex in manual_root.rglob("*.tex"):
|
||||
labelled.update(LABEL_RE.findall(tex.read_text(encoding="utf-8")))
|
||||
return labelled
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--src", default="components",
|
||||
help="root of the source tree (default: components)")
|
||||
ap.add_argument("--manual", default="docs/manual",
|
||||
help="root of the manual sources (default: docs/manual)")
|
||||
args = ap.parse_args()
|
||||
|
||||
src_root = Path(args.src)
|
||||
manual_root = Path(args.manual)
|
||||
if not src_root.exists():
|
||||
print(f"source root not found: {src_root}", file=sys.stderr)
|
||||
return 2
|
||||
if not manual_root.exists():
|
||||
print(f"manual root not found: {manual_root}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
classes = public_classes(src_root)
|
||||
documented = documented_classes(manual_root)
|
||||
|
||||
missing = {name: path for name, path in classes.items()
|
||||
if name not in documented}
|
||||
|
||||
if missing:
|
||||
print("Manual out of sync — missing \\label{cls:...} sections:")
|
||||
for name, path in sorted(missing.items()):
|
||||
print(f" - {name} (declared in {path})")
|
||||
print(f"\n{len(missing)} public class(es) undocumented in the manual.")
|
||||
return 1
|
||||
|
||||
print(f"Manual in sync: {len(classes)} public class(es) documented.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user