Files
cleveragents-core/scripts/check-adr-compliance.py
brent.edwards 7ef5ebb695 feat: Add Q0: Pre-commit hooks setup.
This should automatically check for problems on build.
2026-02-10 16:04:17 +00:00

141 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""ADR compliance checker for CleverAgents.
Verifies that code follows the architectural decisions recorded in ADRs.
ADR-002: Asyncio Concurrency Model
- Async functions should use 'async def', not threading
- No direct thread usage in application layer
ADR-003: Dependency Injection Framework
- Services should receive dependencies via __init__
- No direct instantiation of infrastructure in application layer
ADR-007: Repository Pattern for Persistence
- Database access only through repository classes
- No direct SQLAlchemy session usage in services
Usage:
python scripts/check-adr-compliance.py
"""
import ast
import sys
from pathlib import Path
def check_adr002_async_model(src_dir: Path) -> list[str]:
"""Check ADR-002: Asyncio Concurrency Model.
Verifies no direct threading usage in application code.
"""
violations: list[str] = []
app_dir = src_dir / "application"
if not app_dir.exists():
return violations
for py_file in app_dir.rglob("*.py"):
content = py_file.read_text()
if "import threading" in content or "from threading import" in content:
violations.append(
f"ADR-002 violation: {py_file.relative_to(src_dir)} "
"imports threading directly. Use asyncio instead."
)
return violations
def check_adr003_dependency_injection(src_dir: Path) -> list[str]:
"""Check ADR-003: Dependency Injection Framework.
Verifies services use constructor injection.
"""
violations: list[str] = []
services_dir = src_dir / "application" / "services"
if not services_dir.exists():
return violations
for py_file in services_dir.rglob("*.py"):
if py_file.name.startswith("__"):
continue
try:
tree = ast.parse(py_file.read_text())
except SyntaxError:
continue
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
# Check if class has __init__ with parameters (DI)
has_init = False
for item in node.body:
if isinstance(item, ast.FunctionDef) and item.name == "__init__":
has_init = True
# Check for at least one parameter beyond self
if len(item.args.args) < 2:
violations.append(
f"ADR-003 note: {py_file.relative_to(src_dir)}:"
f"{node.lineno} class {node.name} has __init__ "
"with no injected dependencies"
)
if not has_init and node.body:
# Services without __init__ may be okay if they're utility classes
pass
return violations
def check_adr007_repository_pattern(src_dir: Path) -> list[str]:
"""Check ADR-007: Repository Pattern for Persistence.
Verifies no direct SQLAlchemy session usage in service layer.
"""
violations: list[str] = []
services_dir = src_dir / "application" / "services"
if not services_dir.exists():
return violations
for py_file in services_dir.rglob("*.py"):
content = py_file.read_text()
# Check for direct session imports
if "from sqlalchemy" in content:
violations.append(
f"ADR-007 violation: {py_file.relative_to(src_dir)} "
"imports SQLAlchemy directly. Use repository pattern instead."
)
if "session.query" in content or "session.execute" in content:
violations.append(
f"ADR-007 violation: {py_file.relative_to(src_dir)} "
"uses session directly. Access data through repositories."
)
return violations
def main() -> int:
"""Run all ADR compliance checks."""
src_dir = Path("src/cleveragents")
if not src_dir.exists():
print("Error: src/cleveragents directory not found")
return 1
all_violations: list[str] = []
print("Checking ADR-002: Asyncio Concurrency Model...")
all_violations.extend(check_adr002_async_model(src_dir))
print("Checking ADR-003: Dependency Injection Framework...")
all_violations.extend(check_adr003_dependency_injection(src_dir))
print("Checking ADR-007: Repository Pattern for Persistence...")
all_violations.extend(check_adr007_repository_pattern(src_dir))
if all_violations:
print(f"\nFound {len(all_violations)} ADR compliance issue(s):")
for violation in all_violations:
print(f" - {violation}")
return 1
print("\nAll ADR compliance checks passed.")
return 0
if __name__ == "__main__":
sys.exit(main())