spb/qwhpi Public
QHPI — Quebec Housing Price Index: quality-adjusted, hierarchically pooled housing price indexes.
Python 63.9%
TypeScript 25.4%
CSS 5.5%
TeX 3.5%
SQL 0.8%
Makefile 0.5%
Dockerfile 0.5%
1#!/usr/bin/env python32# =============================================================================3# QWHPI — Quebec Weekly Housing Price Index4# Author : Simon-Pierre Boucher5# Contact : contact@spboucher.ai6# File : scripts/check_headers.py7# Purpose : Pre-commit/CI check that every tracked source file carries the8# mandatory QWHPI author header (fails with non-zero exit if not).9# =============================================================================10"""Verify that all source files begin with the mandatory QWHPI author header.1112Usage:13 python3 scripts/check_headers.py [paths...]1415With no arguments, scans the whole repository. Exits 1 and lists offending16files if any tracked source file lacks the header.17"""1819from __future__ import annotations2021import sys22from pathlib import Path2324REPO_ROOT = Path(__file__).resolve().parents[1]2526# Extensions that must carry the header.27CHECKED_EXTENSIONS = {28 ".py", ".ts", ".tsx", ".js", ".jsx", ".css", ".sql", ".sh",29 ".yaml", ".yml", ".toml", ".tex",30}31CHECKED_FILENAMES = {"Dockerfile", "Makefile"}3233# Tool-generated files that are overwritten by their generator (a header34# added here would not survive the next build).35GENERATED_FILES = {"next-env.d.ts"}3637# Directories never scanned.38SKIP_DIRS = {39 ".git", "node_modules", ".venv", "venv", "__pycache__", ".next",40 "dist", "build", ".pytest_cache", ".mypy_cache", ".ruff_cache",41 "data", "outputs", ".claude",42}4344REQUIRED_TOKENS = (45 "QWHPI",46 "Simon-Pierre Boucher",47 "contact@spboucher.ai",48)4950# How many leading bytes to inspect for the header block.51HEAD_BYTES = 2048525354def has_header(path: Path) -> bool:55 try:56 head = path.read_text(encoding="utf-8", errors="replace")[:HEAD_BYTES]57 except OSError:58 return False59 return all(token in head for token in REQUIRED_TOKENS)606162def iter_candidates(roots: list[Path]):63 for root in roots:64 if root.is_file():65 yield root66 continue67 for path in sorted(root.rglob("*")):68 if any(part in SKIP_DIRS for part in path.parts):69 continue70 if not path.is_file():71 continue72 if path.name in GENERATED_FILES:73 continue74 if path.suffix in CHECKED_EXTENSIONS or path.name in CHECKED_FILENAMES:75 yield path767778def main(argv: list[str]) -> int:79 roots = [Path(a).resolve() for a in argv[1:]] or [REPO_ROOT]80 missing = [p for p in iter_candidates(roots) if not has_header(p)]81 if missing:82 print("Files missing the mandatory QWHPI author header:")83 for p in missing:84 print(f" {p.relative_to(REPO_ROOT)}")85 return 186 print("All checked files carry the QWHPI author header.")87 return 0888990if __name__ == "__main__":91 raise SystemExit(main(sys.argv))92