#!/usr/bin/env python3 # file: scripts/audit_markdown_headers.py # version: 1 """Validate path and positive revision in selected Markdown file headers.""" from __future__ import annotations import argparse import pathlib import re import sys _IGNORED_DIRECTORIES = frozenset({".git", ".idea", ".venv", "__pycache__", "dist", "node_modules", "target"}) _VERSION = re.compile(r"") def _selected_files(raw_paths: list[str], root: pathlib.Path) -> tuple[list[pathlib.Path], list[str]]: files: set[pathlib.Path] = set() errors: list[str] = [] for raw_path in raw_paths: path = pathlib.Path(raw_path) if path.is_dir() or path.is_file(): if not path.resolve().is_relative_to(root): errors.append(f"Markdown header audit: path outside project root: {path}") continue if path.is_dir(): candidates = ( candidate for candidate in path.rglob("*.md") if candidate.is_file() and not any(part in _IGNORED_DIRECTORIES for part in candidate.parts) ) elif path.is_file() and path.suffix.lower() == ".md": candidates = iter((path,)) else: errors.append(f"Markdown header audit: unsupported or missing path: {path}") continue for candidate in candidates: canonical = candidate.resolve() if not canonical.is_relative_to(root): errors.append(f"Markdown header audit: path outside project root: {candidate}") else: files.add(canonical) return sorted(files), errors def _audit_file(path: pathlib.Path, root: pathlib.Path) -> list[str]: relative = path.relative_to(root).as_posix() lines = path.read_text(encoding="utf-8").splitlines() errors: list[str] = [] expected_file = f"" if not lines or lines[0] != expected_file: errors.append(f"{relative}:1: expected `{expected_file}`") if len(lines) < 2 or _VERSION.fullmatch(lines[1]) is None: errors.append(f"{relative}:2: expected positive ``") return errors def main() -> int: """Run the read-only Markdown header audit from the project root.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("paths", nargs="+", help="Markdown files or directories to audit") args = parser.parse_args() root = pathlib.Path.cwd().resolve() files, selection_errors = _selected_files(args.paths, root) if selection_errors or not files: for error in selection_errors or ["Markdown header audit: no Markdown files selected"]: print(error, file=sys.stderr) return 2 errors: list[str] = [] for path in files: try: errors.extend(_audit_file(path, root)) except (OSError, UnicodeError) as error: print(f"Markdown header audit: cannot read {path}: {error}", file=sys.stderr) return 2 for error in errors: print(error, file=sys.stderr) if errors: print(f"Markdown header audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr) return 1 print(f"Markdown header audit: clean ({len(files)} file(s))") return 0 if __name__ == "__main__": raise SystemExit(main())