#!/usr/bin/env python3 """Pre-commit hook to enforce that infrastructure packages do not import from web/. Packages like settings/, utilities/, security/, and config/ should not depend on web/ — this breaks CLI, MCP, and programmatic API usage. """ import re import sys # Infrastructure packages that must not import from web/ PROTECTED_PACKAGES = { "settings", "utilities", "security", "config", } # Existing violations that are allowlisted (file path substring -> allowed import patterns) ALLOWLIST = { "settings/manager.py": [ "web.models.settings", "web.themes", "web.services.socket_service", ], "utilities/log_utils.py": [ "web.services.socket_service", ], "security/rate_limiter.py": [ "web.server_config", ], } # Match import statements that reference .web. or from ..web WEB_IMPORT_PATTERN = re.compile( r"^\s*(?:from\s+(?:\.+)?(?:local_deep_research\.)?web\b|" r"import\s+(?:local_deep_research\.)?web\b)" ) def get_package(filepath): """Extract the package name from file path.""" parts = filepath.replace("\\", "/").split("/") try: ldr_idx = parts.index("local_deep_research") except ValueError: return None if ldr_idx + 1 < len(parts) and parts[ldr_idx + 1] in PROTECTED_PACKAGES: return parts[ldr_idx + 1] return None def is_allowlisted(filepath, line): """Check if a specific import is in the allowlist.""" for path_substr, allowed_imports in ALLOWLIST.items(): if path_substr in filepath: for allowed in allowed_imports: if allowed in line: return True return False def is_type_checking_block(lines, lineno): """Simple heuristic: check if line is inside a TYPE_CHECKING block.""" for i in range(lineno - 1, max(0, lineno - 20), -1): stripped = lines[i].strip() if stripped.startswith("if TYPE_CHECKING"): return True if ( stripped and not stripped.startswith("#") and not stripped.startswith("from") and not stripped.startswith("import") ): break return False def check_file(filepath): errors = [] try: with open(filepath, encoding="utf-8") as f: lines = f.readlines() except (OSError, UnicodeDecodeError): return errors pkg = get_package(filepath) if not pkg: return errors for lineno, line in enumerate(lines): if WEB_IMPORT_PATTERN.search(line): if is_allowlisted(filepath, line): continue if is_type_checking_block(lines, lineno): continue errors.append((lineno + 1, line.rstrip())) return errors def main(): exit_code = 0 for filepath in sys.argv[1:]: if not filepath.endswith(".py"): continue errors = check_file(filepath) for lineno, line in errors: pkg = get_package(filepath) print( f"{filepath}:{lineno}: {pkg}/ must not import from web/ — " f"this breaks CLI/MCP/API usage" ) print(f" {line.strip()}") exit_code = 1 if exit_code: print( "\nInfrastructure packages (settings/, utilities/, security/, config/) " "should not depend on the web layer." ) print( "Move shared types to a common module or use dependency injection." ) return exit_code if __name__ == "__main__": sys.exit(main())