2026-10-13 23:32:25 +02:00
|
|
|
#!/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)}"]
|
|
|
|
|
|
|
|
|
|
|
2026-10-19 10:38:49 +02:00
|
|
|
def third_party_problems(root: Path = ROOT, pin: str | None = None) -> list[str]:
|
|
|
|
|
pin = load_pin() if pin is None else pin
|
|
|
|
|
path = root / "THIRD_PARTY.md"
|
|
|
|
|
if not path.is_file():
|
|
|
|
|
return ["THIRD_PARTY.md is missing"]
|
|
|
|
|
text = path.read_text(encoding="utf-8")
|
|
|
|
|
missing = [mark for mark in ("Copyright (c) 2026 OnCo", "MIT License", pin) if mark not in text]
|
|
|
|
|
if not missing:
|
|
|
|
|
return []
|
|
|
|
|
return [f"THIRD_PARTY.md missing {', '.join(missing)}"]
|
|
|
|
|
|
|
|
|
|
|
2026-10-13 23:32:25 +02:00
|
|
|
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))
|
2026-10-19 10:38:49 +02:00
|
|
|
problems.extend(third_party_problems(root.parent if root == UI else root, pin))
|
2026-10-13 23:32:25 +02:00
|
|
|
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())
|