#!/usr/bin/env python3 # ============================================================================= # QWHPI — Quebec Weekly Housing Price Index # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # File : scripts/check_headers.py # Purpose : Pre-commit/CI check that every tracked source file carries the # mandatory QWHPI author header (fails with non-zero exit if not). # ============================================================================= """Verify that all source files begin with the mandatory QWHPI author header. Usage: python3 scripts/check_headers.py [paths...] With no arguments, scans the whole repository. Exits 1 and lists offending files if any tracked source file lacks the header. """ from __future__ import annotations import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] # Extensions that must carry the header. CHECKED_EXTENSIONS = { ".py", ".ts", ".tsx", ".js", ".jsx", ".css", ".sql", ".sh", ".yaml", ".yml", ".toml", ".tex", } CHECKED_FILENAMES = {"Dockerfile", "Makefile"} # Tool-generated files that are overwritten by their generator (a header # added here would not survive the next build). GENERATED_FILES = {"next-env.d.ts"} # Directories never scanned. SKIP_DIRS = { ".git", "node_modules", ".venv", "venv", "__pycache__", ".next", "dist", "build", ".pytest_cache", ".mypy_cache", ".ruff_cache", "data", "outputs", ".claude", } REQUIRED_TOKENS = ( "QWHPI", "Simon-Pierre Boucher", "contact@spboucher.ai", ) # How many leading bytes to inspect for the header block. HEAD_BYTES = 2048 def has_header(path: Path) -> bool: try: head = path.read_text(encoding="utf-8", errors="replace")[:HEAD_BYTES] except OSError: return False return all(token in head for token in REQUIRED_TOKENS) def iter_candidates(roots: list[Path]): for root in roots: if root.is_file(): yield root continue for path in sorted(root.rglob("*")): if any(part in SKIP_DIRS for part in path.parts): continue if not path.is_file(): continue if path.name in GENERATED_FILES: continue if path.suffix in CHECKED_EXTENSIONS or path.name in CHECKED_FILENAMES: yield path def main(argv: list[str]) -> int: roots = [Path(a).resolve() for a in argv[1:]] or [REPO_ROOT] missing = [p for p in iter_candidates(roots) if not has_header(p)] if missing: print("Files missing the mandatory QWHPI author header:") for p in missing: print(f" {p.relative_to(REPO_ROOT)}") return 1 print("All checked files carry the QWHPI author header.") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))