#!/usr/bin/env python3 # ============================================================================= # Projet : AIR — Accounting Intermediate Representation # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : check_headers.py # Description : CI gate — fails if any project file is missing the author header. # ============================================================================= """Verify that every project file carries the mandatory author header. Usage: python3 scripts/check_headers.py [root] Exit code 0 if all files pass, 1 otherwise (with a list of offenders). """ from __future__ import annotations import json import sys from pathlib import Path AUTHOR = "Simon-Pierre Boucher" CONTACT = "contact@spboucher.ai" # Extensions checked for a comment header containing author + contact. COMMENT_EXTS = { ".py", ".ts", ".js", ".rs", ".go", ".c", ".h", ".zig", ".md", ".yaml", ".yml", ".toml", ".sql", ".sh", } SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".pytest_cache", ".mypy_cache", ".hypothesis", ".claude", "dist", "build"} SKIP_FILES = {".gitignore", "LICENSE", "py.typed"} HEADER_WINDOW = 15 # header must appear within the first N lines def has_header(path: Path) -> bool: if path.suffix == ".json": try: data = json.loads(path.read_text(encoding="utf-8")) except (json.JSONDecodeError, UnicodeDecodeError): return False return isinstance(data, dict) and CONTACT in str(data.get("_author", "")) try: head = "".join( path.read_text(encoding="utf-8").splitlines(keepends=True)[:HEADER_WINDOW] ) except UnicodeDecodeError: return True # binary-ish file, not subject to header rule return AUTHOR in head and CONTACT in head def main() -> int: root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parent.parent offenders: list[Path] = [] for path in sorted(root.rglob("*")): if not path.is_file(): continue if any(part in SKIP_DIRS for part in path.parts): continue if path.name in SKIP_FILES: continue if path.suffix not in COMMENT_EXTS and path.suffix != ".json": continue if not has_header(path): offenders.append(path.relative_to(root)) if offenders: print("Missing author header (Simon-Pierre Boucher / contact@spboucher.ai):") for p in offenders: print(f" - {p}") return 1 print("check_headers: OK — all files carry the author header.") return 0 if __name__ == "__main__": raise SystemExit(main())