content-explorer/scripts/check_ui_provenance.py

91 lines
2.9 KiB
Python
Raw Normal View History

#!/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())