#!/usr/bin/env python3 """Enforce Core ownership of repeated WebUI layout and dialog anatomy.""" from __future__ import annotations import pathlib import re import sys META_ROOT = pathlib.Path(__file__).resolve().parents[2] REPOS_ROOT = META_ROOT.parent CORE_INDEX = pathlib.Path("govoplan-core/webui/src/index.ts") DIALOG_WIDTH_EXCEPTIONS = META_ROOT / "tools/checks/shared-webui-dialog-width-exceptions.txt" RAW_ELEMENT = re.compile( r'<(?Pdiv|span|header|section|form)\b(?P[^>]*?)' r'\bclassName="(?P[^"]+)"', re.DOTALL, ) LEGACY_LAYOUT_TOKENS = { "form-grid", "admin-form-grid", "dashboard-grid", "settings-grid", "admin-dialog", "admin-dialog-wide", "admin-details-grid", "detail-list", "metric-grid", } CENTRAL_COMPONENTS = { "ActionToolbar": pathlib.Path( "govoplan-core/webui/src/components/ActionToolbar.tsx" ), "ToolbarGroup": pathlib.Path( "govoplan-core/webui/src/components/ActionToolbar.tsx" ), "ToolbarSpacer": pathlib.Path( "govoplan-core/webui/src/components/ActionToolbar.tsx" ), "ContentGrid": pathlib.Path( "govoplan-core/webui/src/components/ContentGrid.tsx" ), "FormGrid": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"), "FormLayout": pathlib.Path( "govoplan-core/webui/src/components/ContentGrid.tsx" ), "GridItem": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"), "FormSection": pathlib.Path( "govoplan-core/webui/src/components/FormSection.tsx" ), "DialogActions": pathlib.Path( "govoplan-core/webui/src/components/DialogAnatomy.tsx" ), "DialogForm": pathlib.Path( "govoplan-core/webui/src/components/DialogAnatomy.tsx" ), "DialogSection": pathlib.Path( "govoplan-core/webui/src/components/DialogAnatomy.tsx" ), "DescriptionList": pathlib.Path( "govoplan-core/webui/src/components/DescriptionList.tsx" ), "DescriptionItem": pathlib.Path( "govoplan-core/webui/src/components/DescriptionList.tsx" ), "MetricGrid": pathlib.Path( "govoplan-core/webui/src/components/MetricGrid.tsx" ), } REQUIRED_CONSUMERS = { "ActionToolbar": ( pathlib.Path("govoplan-core/webui/src/components/WysiwygEditor.tsx"), pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarPage.tsx"), pathlib.Path("govoplan-files/webui/src/features/files/FilesPage.tsx"), pathlib.Path("govoplan-forms/webui/src/features/forms/FormsPage.tsx"), pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"), ), "ContentGrid": ( pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"), pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"), pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSettingsPanel.tsx"), ), "FormGrid": ( pathlib.Path("govoplan-core/webui/src/components/mail/MailServerSettingsPanel.tsx"), pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"), pathlib.Path("govoplan-forms/webui/src/features/forms/FormDefinitionDialog.tsx"), pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxAdminPanel.tsx"), ), "FormSection": ( pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"), pathlib.Path("govoplan-quick-access/webui/src/features/settings/QuickAccessSettingsPanel.tsx"), ), "DialogForm": ( pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"), pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"), pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"), ), "DialogSection": ( pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"), pathlib.Path("govoplan-files/webui/src/features/files/components/FileShareDialog.tsx"), pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"), ), "DescriptionList": ( pathlib.Path("govoplan-access/webui/src/features/admin/UsersPanel.tsx"), pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx"), pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"), pathlib.Path("govoplan-policy/webui/src/features/policy/ViewPoliciesPanel.tsx"), ), "MetricGrid": ( pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"), pathlib.Path("govoplan-admin/webui/src/features/admin/ModuleManagementPanel.tsx"), pathlib.Path("govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx"), pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSummaryWidget.tsx"), ), } LEGACY_CSS_SELECTOR = re.compile( r"(? list[pathlib.Path]: paths: list[pathlib.Path] = [] for repository in sorted(REPOS_ROOT.glob("govoplan*")): source_root = repository / "webui" / "src" if source_root.is_dir(): paths.extend(sorted(source_root.rglob("*.tsx"))) return paths def css_paths() -> list[pathlib.Path]: paths: list[pathlib.Path] = [] for repository in sorted(REPOS_ROOT.glob("govoplan*")): source_root = repository / "webui" / "src" if source_root.is_dir(): paths.extend(sorted(source_root.rglob("*.css"))) return paths def relative(path: pathlib.Path) -> pathlib.Path: return path.relative_to(REPOS_ROOT) def raw_reason(classes: str) -> str | None: tokens = classes.split() legacy = sorted(set(tokens) & LEGACY_LAYOUT_TOKENS) if legacy: return f"legacy shared layout class {', '.join(legacy)}" toolbars = [ token for token in tokens if token == "admin-toolbar-row" or token.endswith("-toolbar") ] if toolbars: return f"raw toolbar class {', '.join(toolbars)}" dialog_forms = [token for token in tokens if token.endswith("dialog-form")] if dialog_forms: return f"raw dialog form class {', '.join(dialog_forms)}" return None def normalized_css_selector(selector: str) -> str: return " ".join(selector.split()) def dialog_width_exceptions(styles: dict[pathlib.Path, str]) -> dict[str, str]: exceptions: dict[str, str] = {} central_dialog_styles = pathlib.Path("govoplan-core/webui/src/styles/dialogs.css") for path, content in styles.items(): if path == central_dialog_styles: continue without_comments = CSS_COMMENT.sub("", content) for match in CSS_BLOCK.finditer(without_comments): selector = normalized_css_selector(match.group(1)) declarations = match.group(2) if not DIALOG_WIDTH.search(declarations): continue dialog_classes = DIALOG_CLASS.findall(selector) if not dialog_classes: continue if all(name.lower().endswith(DIALOG_INTERNAL_SUFFIXES) for name in dialog_classes): continue signature = f"{path}|{selector}" exceptions[signature] = declarations return exceptions def exception_baseline() -> set[str]: if not DIALOG_WIDTH_EXCEPTIONS.exists(): return set() return { line.strip() for line in DIALOG_WIDTH_EXCEPTIONS.read_text(encoding="utf-8").splitlines() if line.strip() and not line.lstrip().startswith("#") } def main() -> int: sources = source_paths() source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources} styles = css_paths() style_text = {relative(path): path.read_text(encoding="utf-8") for path in styles} errors: list[str] = [] for path, content in source_text.items(): for match in RAW_ELEMENT.finditer(content): reason = raw_reason(match.group("classes")) if reason is None: continue line = content.count("\n", 0, match.start()) + 1 errors.append( f"Raw {match.group('tag')} repeats shared anatomy ({reason}): {path}:{line}" ) for path, content in style_text.items(): legacy_match = LEGACY_CSS_SELECTOR.search(CSS_COMMENT.sub("", content)) if legacy_match: line = content.count("\n", 0, legacy_match.start()) + 1 errors.append(f"Legacy shared layout selector is not allowed: {path}:{line}") dialog_exceptions = dialog_width_exceptions(style_text) baseline = exception_baseline() for signature in sorted(set(dialog_exceptions) - baseline): errors.append(f"Unreviewed local dialog width; use Dialog size or register a justified exception: {signature}") for signature in sorted(baseline - set(dialog_exceptions)): errors.append(f"Stale dialog width exception can be removed: {signature}") for signature, declarations in sorted(dialog_exceptions.items()): width_values = " ".join(DIALOG_WIDTH_VALUE.findall(declarations)) standard = STANDARD_DIALOG_WIDTH.search(width_values) if standard: errors.append(f"Local dialog width duplicates Core size {standard.group(0)}: {signature}") for name, owner in CENTRAL_COMPONENTS.items(): definition = re.compile( rf"\b(?:function|class)\s+{name}\b|\bconst\s+{name}\s*=" ) for path, content in source_text.items(): if path != owner and definition.search(content): errors.append(f"Module-local {name} definition is not allowed: {path}") usage_counts: dict[str, int] = {} for name in CENTRAL_COMPONENTS: usage = re.compile(rf"<{name}\b") usage_counts[name] = sum(bool(usage.search(content)) for content in source_text.values()) for name, consumers in REQUIRED_CONSUMERS.items(): for path in consumers: absolute = REPOS_ROOT / path if not absolute.exists(): continue if f"<{name}" not in absolute.read_text(encoding="utf-8"): errors.append(f"Required shared {name} consumer regressed: {path}") core_index_path = REPOS_ROOT / CORE_INDEX if core_index_path.exists(): core_index = core_index_path.read_text(encoding="utf-8") for name in CENTRAL_COMPONENTS: if not re.search(rf"\b{name}\b", core_index): errors.append(f"Core must export {name} from @govoplan/core-webui.") dialog_path = REPOS_ROOT / "govoplan-core/webui/src/components/Dialog.tsx" if dialog_path.exists(): dialog_text = dialog_path.read_text(encoding="utf-8") if "