#!/usr/bin/env python3 # ============================================================================= # Project : anomaly-atlas # File : tools/check_headers.py # Purpose : CI-style enforcement of the mandatory author header (CLAUDE.md §0.1) # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Data src : hfmarketdata.io (sole data source) # Created : 2026-08-12 # Modified : 2026-08-12 # Platform : macOS / Apple Silicon (arm64) # License : All rights reserved (research code) # ============================================================================= """Fail (exit 1) if any tracked source file lacks a conforming author header. Usage: python3 tools/check_headers.py # check all git-tracked files python3 tools/check_headers.py FILE... # check specific files Rules enforced (see CLAUDE.md §0.1): * Comment-style sources (.py .sh .zsh .yaml .yml .toml .cff .sql Makefile CMakeLists.txt .gitignore) must contain the '#'-style header block near the top, including the 'Data src' field. * C-family / TS / JS / CSS sources must contain the '//'-style header block ('/*'-style for .css) near the top. * Markdown documents must begin with YAML front matter declaring project/author/contact/data_source. * A shebang line may precede the header. Exemptions: generated results under results/, atlas payload JSON, LICENSE, package-lock.json, CLAUDE.md (the charter is the specification itself). """ from __future__ import annotations import subprocess import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent REQUIRED_FIELDS = ( "Project", "File", "Purpose", "Author", "Contact", "Data src", "Created", "Modified", "Platform", "License", ) AUTHOR = "Simon-Pierre Boucher" CONTACT = "contact@spboucher.ai" DATA_SOURCE = "hfmarketdata.io" HASH_EXTS = {".py", ".sh", ".zsh", ".bash", ".yaml", ".yml", ".toml", ".cff", ".sql"} SLASH_EXTS = { ".c", ".cc", ".cpp", ".h", ".hpp", ".metal", ".swift", ".m", ".mm", ".js", ".mjs", ".ts", ".tsx", ".css", } HASH_NAMES = {"Makefile", "CMakeLists.txt", ".gitignore"} EXEMPT_NAMES = {"LICENSE", "CLAUDE.md", "MEMORY.md", "package-lock.json"} EXEMPT_DIRS = {"results"} # How many leading lines to scan for the header block (allows shebang etc.). SCAN_LINES = 22 def tracked_files() -> list[Path]: out = subprocess.run( ["git", "ls-files"], cwd=REPO_ROOT, capture_output=True, text=True, check=True ).stdout return [REPO_ROOT / line for line in out.splitlines() if line.strip()] def is_exempt(path: Path) -> bool: rel = path.relative_to(REPO_ROOT) if rel.name in EXEMPT_NAMES: return True return bool(rel.parts and rel.parts[0] in EXEMPT_DIRS) def check_comment_header(lines: list[str], prefixes: tuple[str, ...]) -> list[str]: """Check for a comment-style header with all required fields near the top.""" head = "\n".join(lines[:SCAN_LINES]) errors = [] for field in REQUIRED_FIELDS: if not any(f"{p} {field}" in head or f"{p} {field}" in head for p in prefixes): errors.append(f"missing header field: {field}") if AUTHOR not in head: errors.append(f"missing author name '{AUTHOR}'") if CONTACT not in head: errors.append(f"missing contact '{CONTACT}'") if DATA_SOURCE not in head: errors.append(f"missing data source '{DATA_SOURCE}'") return errors def check_markdown_front_matter(lines: list[str]) -> list[str]: if not lines or lines[0].strip() != "---": return ["markdown file must start with YAML front matter (---)"] errors = [] try: end = next(i for i in range(1, min(len(lines), SCAN_LINES)) if lines[i].strip() == "---") except StopIteration: return ["unterminated YAML front matter"] block = "\n".join(lines[1:end]) for key in ( "project: anomaly-atlas", f"author: {AUTHOR}", f"contact: {CONTACT}", f"data_source: {DATA_SOURCE}", ): if key not in block: errors.append(f"front matter missing '{key}'") return errors def check_file(path: Path) -> list[str]: try: text = path.read_text(encoding="utf-8", errors="replace") except OSError as exc: return [f"unreadable: {exc}"] lines = text.splitlines() if lines and lines[0].startswith("#!"): lines = lines[1:] name, ext = path.name, path.suffix if ext in HASH_EXTS or name in HASH_NAMES: return check_comment_header(lines, ("#",)) if ext in SLASH_EXTS: return check_comment_header(lines, ("//", "*", "/*")) if ext == ".md": return check_markdown_front_matter(lines) return [] # other file types are not subject to the header rule def main(argv: list[str]) -> int: paths = [Path(p).resolve() for p in argv] if argv else tracked_files() failures: dict[str, list[str]] = {} checked = 0 for path in paths: if not path.is_file() or is_exempt(path): continue errors = check_file(path) if path.suffix in HASH_EXTS | SLASH_EXTS | {".md"} or path.name in HASH_NAMES: checked += 1 if errors: failures[str(path.relative_to(REPO_ROOT))] = errors if failures: print(f"HEADER CHECK FAILED — {len(failures)} non-conforming file(s):\n") for rel, errors in sorted(failures.items()): print(f" {rel}") for err in errors: print(f" - {err}") return 1 print(f"Header check passed ({checked} files checked).") return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))