0.0.3-alpha.5

This commit is contained in:
2026-09-24 10:57:10 +02:00
parent 8ad4987d38
commit 584a0d1586
7 changed files with 321 additions and 13 deletions

View File

@@ -0,0 +1,96 @@
#!/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"<!-- version: [1-9][0-9]* -->")
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"<!-- file: {relative} -->"
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 `<!-- version: N -->`")
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())