Files
ITWorx_MCP_Hub-Public/scripts/check_boundaries.py
T

128 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Check the repository dependency direction without importing application code."""
from __future__ import annotations
import argparse
import ast
import re
import sys
from dataclasses import dataclass
from pathlib import Path
SOURCE_ROOTS = {"apps", "services", "connectors", "packages"}
SOURCE_SUFFIXES = {".py", ".pyi", ".js", ".jsx", ".ts", ".tsx"}
IGNORED_PARTS = {"__pycache__", ".venv", "node_modules", "dist"}
@dataclass(frozen=True)
class Violation:
path: Path
line: int
message: str
def format(self, root: Path) -> str:
return f"{self.path.relative_to(root).as_posix()}:{self.line}: {self.message}"
def _owner(path: Path, root: Path) -> tuple[str, str] | None:
relative = path.relative_to(root)
if len(relative.parts) < 2 or relative.parts[0] not in SOURCE_ROOTS:
return None
return relative.parts[0], relative.parts[1].replace("-", "_")
def _target(module: str) -> tuple[str, str] | None:
normalized = module.replace("-", "_")
parts = normalized.split(".")
if len(parts) < 2 or parts[0] not in SOURCE_ROOTS:
return None
return parts[0], parts[1]
def _allowed(owner: tuple[str, str], target: tuple[str, str]) -> bool:
owner_root, owner_name = owner
target_root, target_name = target
if owner_root == "packages":
if owner_name == "hub_contracts":
return target_root == "packages" and target_name == "hub_contracts"
if owner_name in {"hub_core", "hub_policy", "connector_kit"}:
return target_root == "packages" and target_name in {owner_name, "hub_contracts"}
return target_root == "packages"
if owner_root == "connectors":
return target_root == "packages"
if owner_root == "services":
if owner_name == "runtime":
return target_root in {"packages", "services", "connectors"}
return target_root == "packages" or target == ("services", owner_name)
if owner_root == "apps":
return target_root == "packages" or target == ("apps", owner_name)
return True
def _python_imports(path: Path) -> list[tuple[int, str]]:
try:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
except (OSError, SyntaxError):
return []
imports: list[tuple[int, str]] = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imports.extend((node.lineno, alias.name) for alias in node.names)
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
imports.append((node.lineno, node.module))
return imports
def _js_imports(path: Path) -> list[tuple[int, str]]:
pattern = re.compile(r"(?:from\s+|import\s*\(?\s*|require\(\s*)['\"]([^'\"]+)")
return [
(line_no, module)
for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1)
for module in pattern.findall(line)
]
def find_violations(root: Path, paths: list[Path] | None = None) -> list[Violation]:
candidates = paths or [
path
for source_root in SOURCE_ROOTS
for path in (root / source_root).rglob("*")
if path.is_file()
and path.suffix in SOURCE_SUFFIXES
and not (set(path.parts) & IGNORED_PARTS)
]
violations: list[Violation] = []
for path in candidates:
owner = _owner(path, root)
if owner is None:
continue
imports = _python_imports(path) if path.suffix in {".py", ".pyi"} else _js_imports(path)
for line, module in imports:
target = _target(module)
if target and not _allowed(owner, target):
violations.append(
Violation(
path, line, f"forbidden dependency {module!r} from {owner[0]}/{owner[1]}"
)
)
return violations
def main() -> int:
parser = argparse.ArgumentParser(description="Check ITWorx MCP Hub import boundaries.")
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
args = parser.parse_args()
root = args.root.resolve()
violations = find_violations(root)
if violations:
for violation in violations:
print(violation.format(root), file=sys.stderr)
return 1
print("Import-boundary check: PASS")
return 0
if __name__ == "__main__":
raise SystemExit(main())