Code / tools/check_headers.py
tools/check_headers.py
178 lines
#!/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:]))