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:
parent
08a9a787a7
commit
f434773fd0
4 changed files with 116 additions and 21 deletions
|
|
@ -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
64
tests/test_ui_boundary.py
Normal 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()
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue