diff --git a/scripts/check_ui_provenance.py b/scripts/check_ui_provenance.py new file mode 100644 index 0000000..56d2b0f --- /dev/null +++ b/scripts/check_ui_provenance.py @@ -0,0 +1,90 @@ +#!/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()) diff --git a/tests/test_ui_provenance.py b/tests/test_ui_provenance.py new file mode 100644 index 0000000..3ac5c46 --- /dev/null +++ b/tests/test_ui_provenance.py @@ -0,0 +1,64 @@ +"""Adapted UI files keep the OnCo pin, copyright, and NOTICE.""" + +from __future__ import annotations + +import importlib.util +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +spec = importlib.util.spec_from_file_location("check_ui_provenance", ROOT / "scripts" / "check_ui_provenance.py") +provenance = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(provenance) + +PIN = "b594d7433bf4e408b44215e25e5304696d6f840e" + + +class UiProvenance(unittest.TestCase): + def test_pin_matches_the_baseline_file(self) -> None: + self.assertEqual(provenance.load_pin(), PIN) + + def test_rejects_an_adapted_file_without_the_pin(self) -> None: + sample = """ + /** + * Adapted from OnCo SkipLink. + * Copyright (c) 2026 OnCo. MIT License. See ui/NOTICE. + */ + """ + problems = provenance.header_problems(sample, "sample", PIN) + self.assertEqual(len(problems), 1) + self.assertIn(PIN, problems[0]) + + def test_accepts_a_complete_header(self) -> None: + sample = f""" + /* + * Reduced from OnCo src/components/filters/ResultsTable.tsx + * https://github.com/judegomila/OnCo @ {PIN} + * Copyright (c) 2026 OnCo. MIT License. See ui/NOTICE. + */ + """ + self.assertEqual(provenance.header_problems(sample, "sample", PIN), []) + + def test_ignores_a_file_that_was_not_copied(self) -> None: + sample = """ + /** + * Generic record page. + * OnCo EntityDetail was not copied. + */ + """ + self.assertEqual(provenance.header_problems(sample, "sample", PIN), []) + + def test_notice_requires_the_permission_text(self) -> None: + short = f"Copyright (c) 2026 OnCo\nMIT License\n{PIN}\n" + problems = provenance.notice_problems(short, PIN) + self.assertTrue(problems) + self.assertIn("Permission is hereby granted", problems[0]) + + def test_shipped_ui_keeps_headers_and_notices(self) -> None: + self.assertEqual(provenance.tree_problems(), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/components/Hero.tsx b/ui/components/Hero.tsx index 2862a24..bc292b0 100644 --- a/ui/components/Hero.tsx +++ b/ui/components/Hero.tsx @@ -1,5 +1,6 @@ /** * Hero structure adapted from OnCo home: section.hero, kicker, display heading, lede, action. + * https://github.com/judegomila/OnCo @ b594d7433bf4e408b44215e25e5304696d6f840e * Copyright (c) 2026 OnCo for the adapted pattern. MIT License. See ui/NOTICE. * The heading and lede are Content Explorer copy. */ diff --git a/ui/components/PageHeader.tsx b/ui/components/PageHeader.tsx index 1abd540..1358d69 100644 --- a/ui/components/PageHeader.tsx +++ b/ui/components/PageHeader.tsx @@ -1,5 +1,6 @@ /** * Page heading adapted from OnCo PageHeader: kicker, title, lede. + * https://github.com/judegomila/OnCo @ b594d7433bf4e408b44215e25e5304696d6f840e * Copyright (c) 2026 OnCo for the adapted pattern. MIT License. See ui/NOTICE. * Garden art and domain logos are not included. */ diff --git a/ui/components/SiteHeader.tsx b/ui/components/SiteHeader.tsx index 695328b..b769c7b 100644 --- a/ui/components/SiteHeader.tsx +++ b/ui/components/SiteHeader.tsx @@ -5,6 +5,7 @@ import type { DataspaceNav } from "@/lib/catalog"; /** * Sticky header shell adapted from OnCo SiteHeader. + * https://github.com/judegomila/OnCo @ b594d7433bf4e408b44215e25e5304696d6f840e * Copyright (c) 2026 OnCo. MIT License. See ui/NOTICE. * The OnCo mark and command palette are not included. * Kind links come from the snapshot registry passed in by the layout. diff --git a/ui/components/SkipLink.tsx b/ui/components/SkipLink.tsx index 182fd53..3283d51 100644 --- a/ui/components/SkipLink.tsx +++ b/ui/components/SkipLink.tsx @@ -1,4 +1,9 @@ -/** First Tab jumps past the header to
. Adapted from OnCo SkipLink; copy is Content Explorer's. */ +/** + * First Tab jumps past the header to
. + * Adapted from OnCo SkipLink. The link copy is Content Explorer's. + * https://github.com/judegomila/OnCo @ b594d7433bf4e408b44215e25e5304696d6f840e + * Copyright (c) 2026 OnCo. MIT License. See ui/NOTICE. + */ export function SkipLink() { return ( diff --git a/workplans/CONTENT-WP-0002-ui-extraction.md b/workplans/CONTENT-WP-0002-ui-extraction.md index 79785b2..f3131a5 100644 --- a/workplans/CONTENT-WP-0002-ui-extraction.md +++ b/workplans/CONTENT-WP-0002-ui-extraction.md @@ -16,7 +16,7 @@ state_hub_workstream_id: "55c1d92f-fe1a-552a-998d-2d6905fb6b99" # UI extraction and experience skeleton -Deliver the strongest OnCo-style interaction patterns on generic data. CONTENT-WP-0000 and CONTENT-WP-0001 are finished, so this plan is eligible. T01 through T07 have landed under `ui/`. T07 rejects an import that names oncology, cancer, drug, or target. T08 is next. See `workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md`. +Deliver the strongest OnCo-style interaction patterns on generic data. CONTENT-WP-0000 and CONTENT-WP-0001 are finished, so this plan is eligible. T01 through T08 have landed under `ui/`. T08 requires each adapted file to name the OnCo pin, the copyright, and `ui/NOTICE`. T09 is next. See `workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md`. ## Work packages @@ -89,7 +89,7 @@ state_hub_task_id: "bd6b4dd8-e897-5c08-9e07-a0246def0faf" ```task id: CONTENT-WP-0002-T08 -status: todo +status: done priority: high state_hub_task_id: "16172ffd-23e0-5c4f-8cac-2991e589fcdb" ``` diff --git a/workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md b/workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md index 3d30462..3e44ae5 100644 --- a/workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md +++ b/workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md @@ -120,3 +120,16 @@ Checks on 2026-10-13, after T07: - `python3 scripts/check_ui_boundary.py` and `python3 scripts/validate_fixtures.py` passed. Not done: the provenance gate, or the keyboard and small-screen smoke. A facet key other than `tags` still has no values in these fixtures. + +## T08 — provenance headers and notices + +A file under `ui/` that says it was adapted or reduced from OnCo must name the pin in `research/baseline/ONCO_PIN`, `Copyright (c) 2026 OnCo`, `MIT License`, and `ui/NOTICE`. `ui/NOTICE` keeps the MIT permission text and that same pin. The footer keeps the MIT license and the pin in the page text. A file that says OnCo was not copied is not treated as an adapted copy. + +SkipLink, SiteHeader, Hero, and PageHeader already named OnCo and the license. They did not name the pin. Those four headers now include `b594d7433bf4e408b44215e25e5304696d6f840e`. ResultsTable, globals.css, browser-row, and table-view already had the full header. + +Checks on 2026-10-13, after T08: + +- `python3 -m unittest tests.test_ui_provenance tests.test_ui_boundary tests.test_contract tests.test_ingest` passed (21 tests). +- `python3 scripts/check_ui_provenance.py`, `python3 scripts/check_ui_boundary.py`, and `python3 scripts/validate_fixtures.py` passed. + +Not done: the keyboard and small-screen smoke. A facet key other than `tags` still has no values in these fixtures.