Reject oncology, cancer, drug, and target imports in the UI.

CONTENT-WP-0002-T07 checks every import statement and the navigation labels of both fixtures.

Assistant: grok
Assistant-Session: 01a1346c-fdff-7741-b1af-b7283661554b
This commit is contained in:
tegwick 2026-10-13 23:27:32 +02:00
parent 08a9a787a7
commit f434773fd0
4 changed files with 116 additions and 21 deletions

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Reject oncology vocabulary on UI import lines, and require the MIT notice."""
"""Reject oncology, cancer, drug, and target imports in the generic UI."""
from __future__ import annotations
@ -9,33 +9,51 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
UI = ROOT / "ui"
IMPORT_LINE = re.compile(r"""^\s*(import|export)\b.*\bfrom\b|^\s*import\s*\(""")
FORBIDDEN = re.compile(r"oncology|cancers?|drugs?|targets?", re.IGNORECASE)
SOURCE_SUFFIXES = {".ts", ".tsx", ".css", ".mjs"}
IMPORT_STATEMENT = re.compile(
r"""(?:^|\n)\s*import\b[\s\S]*?\bfrom\s+['"][^'"]+['"]"""
r"""|(?:^|\n)\s*import\s+['"][^'"]+['"]"""
r"""|import\s*\(\s*['"][^'"]+['"]\s*\)"""
r"""|require\s*\(\s*['"][^'"]+['"]\s*\)"""
r"""|@import\s+['"][^'"]+['"]""",
)
# The token may start an identifier such as CancerIcon or targetThumb.
FORBIDDEN = re.compile(r"\b(?:oncology|cancers?|drugs?|targets?)", re.IGNORECASE)
def import_lines(path: Path) -> list[str]:
hits: list[str] = []
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
if IMPORT_LINE.search(line) and FORBIDDEN.search(line):
hits.append(f"{path.relative_to(ROOT)}:{number}: {line.strip()}")
return hits
def import_problems(text: str, label: str) -> list[str]:
problems: list[str] = []
for match in IMPORT_STATEMENT.finditer(text):
statement = " ".join(match.group(0).split())
if FORBIDDEN.search(statement):
problems.append(f"{label}: {statement}")
return problems
def tree_problems(root: Path = UI) -> list[str]:
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(import_problems(path.read_text(encoding="utf-8"), str(path.relative_to(ROOT))))
notice = root / "NOTICE"
if not notice.is_file():
problems.append("ui/NOTICE is missing")
else:
text = notice.read_text(encoding="utf-8")
if "Copyright (c) 2026 OnCo" not in text or "MIT License" not in text:
problems.append("ui/NOTICE is missing the OnCo MIT copyright and permission notice")
return problems
def main() -> int:
problems: list[str] = []
for path in sorted(UI.rglob("*")):
if not path.is_file() or "node_modules" in path.parts or ".next" in path.parts:
continue
if path.suffix in SOURCE_SUFFIXES:
problems.extend(import_lines(path))
notice = (UI / "NOTICE").read_text(encoding="utf-8")
if "Copyright (c) 2026 OnCo" not in notice or "MIT License" not in notice:
problems.append("ui/NOTICE is missing the OnCo MIT copyright and permission notice")
problems = tree_problems()
if problems:
print("\n".join(problems))
return 1
print("ui boundary: no oncology imports; NOTICE retains the OnCo MIT text")
print("ui boundary: no oncology, cancer, drug, or target imports; NOTICE retains the OnCo MIT text")
return 0

64
tests/test_ui_boundary.py Normal file
View file

@ -0,0 +1,64 @@
"""The generic UI must not import oncology, cancer, drug, or target modules."""
from __future__ import annotations
import importlib.util
import json
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EXAMPLES = ROOT / "examples" / "dataspaces"
FORBIDDEN = r"\b(?:oncology|cancers?|drugs?|targets?)"
spec = importlib.util.spec_from_file_location("check_ui_boundary", ROOT / "scripts" / "check_ui_boundary.py")
boundary = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(boundary)
class UiBoundary(unittest.TestCase):
def test_rejects_domain_imports(self) -> None:
samples = [
'import { CancerIcon } from "@/components/icons";',
'import {\n drug\n} from "@/lib/rows";',
'const rows = import("./targets/list");',
'@import "oncology.css";',
]
for sample in samples:
with self.subTest(sample=sample):
self.assertTrue(boundary.import_problems(sample, "sample"))
def test_allows_generic_imports_and_event_target(self) -> None:
sample = """
import { findKind } from "@/lib/catalog";
const node = event.target;
// cancer stays out of the module graph
"""
self.assertEqual(boundary.import_problems(sample, "sample"), [])
def test_shipped_ui_has_no_domain_imports(self) -> None:
self.assertEqual(boundary.tree_problems(), [])
def test_navigation_labels_are_not_domain_terms(self) -> None:
import re
forbidden = re.compile(FORBIDDEN, re.IGNORECASE)
names = sorted(path.name for path in EXAMPLES.iterdir() if (path / "snapshot.json").is_file())
self.assertGreaterEqual(len(names), 2)
for name in names:
snapshot = json.loads((EXAMPLES / name / "snapshot.json").read_text(encoding="utf-8"))
manifest = json.loads((EXAMPLES / name / "manifest.json").read_text(encoding="utf-8"))
labels = [snapshot["dataspace"]["title"]]
labels.extend(kind["id"] for kind in snapshot["kinds"])
labels.extend(kind["label"] for kind in snapshot["kinds"])
labels.extend(kind["plural"] for kind in snapshot["kinds"])
for section in manifest.get("navigation", []):
labels.append(section["label"])
for label in labels:
with self.subTest(dataspace=name, label=label):
self.assertIsNone(forbidden.search(label))
if __name__ == "__main__":
unittest.main()

View file

@ -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 T06 have landed under `ui/`. T06 checks that browse, search, filter, sort, and detail work for both example dataspaces. T07 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 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`.
## Work packages

View file

@ -106,4 +106,17 @@ Checks on 2026-10-13, after T06:
- `npm run typecheck`, `scripts/check_ui_boundary.py`, fixture validation, and the contract and ingest tests passed.
- `next dev` on `127.0.0.1:3400` returned HTTP 200 for `/systems-thinking/`, `/software-architecture/`, `/systems-thinking/concept/feedback/`, and `/software-architecture/concept/coupling/`. Each page contained the other field's name or the record's sources. The server was stopped. Search and sort on those pages were exercised in the UI test, because the filter runs in the browser after the HTML is sent.
Not done: the formal import gate, the provenance gate, or the keyboard and small-screen smoke. A facet key other than `tags` still has no values in these fixtures.
Not done after T06: the formal import gate, the provenance gate, or the keyboard and small-screen smoke. A facet key other than `tags` still has no values in these fixtures.
## T07 — domain import gate
`scripts/check_ui_boundary.py` reads every import, `require`, dynamic `import()`, and CSS `@import` under `ui/`. It fails when the statement names oncology, cancer, drug, or target, including a name such as `CancerIcon`. A property such as `event.target` is not an import and stays allowed. The kind labels and navigation labels in both example fixtures are checked the same way.
`tests/test_ui_boundary.py` rejects sample imports for each forbidden token and accepts the shipped UI.
Checks on 2026-10-13, after T07:
- `python3 -m unittest tests.test_ui_boundary tests.test_contract tests.test_ingest` passed (15 tests).
- `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.