#!/usr/bin/env python3 """Require provenance headers on UI files adapted from the pinned OnCo commit.""" from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] UI = ROOT / "ui" PIN_FILE = ROOT / "research" / "baseline" / "ONCO_PIN" SOURCE_SUFFIXES = {".ts", ".tsx", ".css", ".mjs"} ADAPTED = ("adapted from OnCo", "reduced from OnCo") HEADER_MARKS = ("Copyright (c) 2026 OnCo", "MIT License", "ui/NOTICE") NOTICE_MARKS = ( "Copyright (c) 2026 OnCo", "MIT License", "Permission is hereby granted", "The above copyright notice and this permission notice shall be included", ) def load_pin(path: Path = PIN_FILE) -> str: for line in path.read_text(encoding="utf-8").splitlines(): if line.startswith("sha:"): return line.split(":", 1)[1].strip() raise ValueError(f"{path} has no sha") def is_adapted(text: str) -> bool: folded = text.casefold() return any(mark.casefold() in folded for mark in ADAPTED) def header_problems(text: str, label: str, pin: str) -> list[str]: if not is_adapted(text): return [] missing = [mark for mark in (*HEADER_MARKS, pin) if mark not in text] if not missing: return [] return [f"{label}: provenance header missing {', '.join(missing)}"] def notice_problems(text: str, pin: str) -> list[str]: missing = [mark for mark in (*NOTICE_MARKS, pin) if mark not in text] if not missing: return [] return [f"ui/NOTICE missing {', '.join(missing)}"] def footer_problems(text: str, pin: str) -> list[str]: missing = [mark for mark in ("MIT License", pin) if mark not in text] if not missing: return [] return [f"ui/components/SiteFooter.tsx missing {', '.join(missing)}"] def tree_problems(root: Path = UI, pin: str | None = None) -> list[str]: pin = load_pin() if pin is None else pin problems: list[str] = [] for path in sorted(root.rglob("*")): if not path.is_file() or "node_modules" in path.parts or ".next" in path.parts: continue if path.suffix not in SOURCE_SUFFIXES: continue problems.extend(header_problems(path.read_text(encoding="utf-8"), str(path.relative_to(ROOT)), pin)) notice = root / "NOTICE" if not notice.is_file(): problems.append("ui/NOTICE is missing") else: problems.extend(notice_problems(notice.read_text(encoding="utf-8"), pin)) footer = root / "components" / "SiteFooter.tsx" if not footer.is_file(): problems.append("ui/components/SiteFooter.tsx is missing") else: problems.extend(footer_problems(footer.read_text(encoding="utf-8"), pin)) return problems def main() -> int: problems = tree_problems() if problems: print("\n".join(problems)) return 1 print("ui provenance: adapted files name the OnCo pin, copyright, and NOTICE") return 0 if __name__ == "__main__": sys.exit(main())