#!/usr/bin/env python3 from __future__ import annotations import ast from dataclasses import dataclass from pathlib import Path ROOT = Path(__file__).resolve().parents[1] REPOS = { "core": ROOT / "src" / "govoplan_core", "access": ROOT.parent / "govoplan-access" / "src" / "govoplan_access", "admin": ROOT.parent / "govoplan-admin" / "src" / "govoplan_admin", "tenancy": ROOT.parent / "govoplan-tenancy" / "src" / "govoplan_tenancy", "organizations": ROOT.parent / "govoplan-organizations" / "src" / "govoplan_organizations", "identity": ROOT.parent / "govoplan-identity" / "src" / "govoplan_identity", "policy": ROOT.parent / "govoplan-policy" / "src" / "govoplan_policy", "audit": ROOT.parent / "govoplan-audit" / "src" / "govoplan_audit", "dashboard": ROOT.parent / "govoplan-dashboard" / "src" / "govoplan_dashboard", "files": ROOT.parent / "govoplan-files" / "src" / "govoplan_files", "mail": ROOT.parent / "govoplan-mail" / "src" / "govoplan_mail", "campaign": ROOT.parent / "govoplan-campaign" / "src" / "govoplan_campaign", } PREFIXES = { "core": "govoplan_core", "access": "govoplan_access", "admin": "govoplan_admin", "tenancy": "govoplan_tenancy", "organizations": "govoplan_organizations", "identity": "govoplan_identity", "policy": "govoplan_policy", "audit": "govoplan_audit", "dashboard": "govoplan_dashboard", "files": "govoplan_files", "mail": "govoplan_mail", "campaign": "govoplan_campaign", } FEATURE_OWNERS = ("files", "mail", "campaign") PLATFORM_OWNERS = ("admin", "tenancy", "organizations", "identity", "policy", "audit", "dashboard") PUBLIC_ACCESS_IMPORTS = ("govoplan_access.auth",) @dataclass(frozen=True) class AllowlistedImport: owner: str relative_path: str imported_owner: str reason: str # Transitional exceptions. This should remain empty; add entries only with a # dated removal plan and a capability/API contract issue. ALLOWLIST: tuple[AllowlistedImport, ...] = () ALLOWLIST_KEYS = {(item.owner, item.relative_path, item.imported_owner) for item in ALLOWLIST} @dataclass(frozen=True) class Violation: owner: str path: Path lineno: int imported: str imported_owner: str def _import_owner(module_name: str) -> str | None: for owner, prefix in PREFIXES.items(): if module_name == prefix or module_name.startswith(f"{prefix}."): return owner return None def _imports(path: Path) -> list[tuple[int, str]]: tree = ast.parse(path.read_text(), filename=str(path)) result: list[tuple[int, str]] = [] for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: result.append((node.lineno, alias.name)) elif isinstance(node, ast.ImportFrom) and node.module: result.append((node.lineno, node.module)) return result def _relative(owner: str, path: Path) -> str: return path.relative_to(REPOS[owner]).as_posix() def _allowed(owner: str, path: Path, imported_owner: str) -> bool: return (owner, _relative(owner, path), imported_owner) in ALLOWLIST_KEYS def _is_public_access_import(imported: str) -> bool: return any(imported == item or imported.startswith(f"{item}.") for item in PUBLIC_ACCESS_IMPORTS) def _violations() -> list[Violation]: violations: list[Violation] = [] for owner, root in REPOS.items(): if not root.exists(): continue for path in root.rglob("*.py"): if "__pycache__" in path.parts: continue for lineno, imported in _imports(path): imported_owner = _import_owner(imported) if imported_owner is None or imported_owner == owner: continue if imported_owner == "access" and _is_public_access_import(imported): continue if owner == "core" and imported_owner in FEATURE_OWNERS: if not _allowed(owner, path, imported_owner): violations.append(Violation(owner, path, lineno, imported, imported_owner)) elif owner == "access" and imported_owner in FEATURE_OWNERS: violations.append(Violation(owner, path, lineno, imported, imported_owner)) elif owner in PLATFORM_OWNERS and imported_owner == "access": if not _allowed(owner, path, imported_owner): violations.append(Violation(owner, path, lineno, imported, imported_owner)) elif owner in FEATURE_OWNERS and imported_owner in FEATURE_OWNERS: if not _allowed(owner, path, imported_owner): violations.append(Violation(owner, path, lineno, imported, imported_owner)) elif owner in FEATURE_OWNERS and imported_owner == "access": violations.append(Violation(owner, path, lineno, imported, imported_owner)) return violations def main() -> int: violations = _violations() if not violations: print(f"Dependency boundary check passed ({len(ALLOWLIST)} transitional exceptions tracked)") return 0 print("Dependency boundary violations:") for item in violations: print( f"- {item.path}:{item.lineno}: {item.owner} imports {item.imported} " f"({item.imported_owner}); use kernel capability/API/event contracts instead" ) print("\nIf this is unavoidable transitional debt, add a reasoned ALLOWLIST entry.") return 1 if __name__ == "__main__": raise SystemExit(main())