architecture_guard.py
- Pfad:
/var/www/tools/ki-protokoll/claude-hook/architecture_guard.py - Namespace: claude-hook
- Zeilen: 178 | Größe: 4,707 Bytes
- Geändert: 2025-12-23 06:17:58 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 100
- Dependencies: 100 (25%)
- LOC: 100 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Dependencies 3
- use json
- use re
- use sys
Funktionen 6
-
is_allowed_path()Zeile 76 -
rule_applies()Zeile 84 -
check_rule()Zeile 97 -
check_all_rules()Zeile 113 -
format_output()Zeile 142 -
main()Zeile 147
Code
#!/usr/bin/env python3
"""
Architecture Guard - Pre-Hook (Blocking)
Enforces hard_constraints from architecture-gate-contract v1.1
Blocks file creation on violation. No exceptions.
Rules:
H1: strict_types_required (all PHP files)
H2: domain_no_infrastructure (Domain layer)
H3: db_factory_only (only in Factory classes)
H4: no_new_repository_in_controller (Controller layer)
H5: no_new_infrastructure_in_controller (Controller layer)
Trigger: PreToolUse (Write) on *.php
"""
import json
import re
import sys
# Hard rules from architecture-gate-contract v1.1
HARD_RULES = [
{
"id": "H1",
"name": "strict_types_required",
"pattern": r"declare\s*\(\s*strict_types\s*=\s*1\s*\)",
"must_match": True,
"applies_to": "all",
"message": "Missing declare(strict_types=1). Add at top of file after <?php"
},
{
"id": "H2",
"name": "domain_no_infrastructure",
"pattern": r"use\s+Infrastructure\\",
"must_match": False,
"applies_to": "/Domain/",
"message": "Domain must not use Infrastructure. Violates DIP."
},
{
"id": "H3",
"name": "db_factory_only",
"pattern": r"DatabaseFactory::",
"must_match": False,
"applies_to_not": "/Factory/",
"message": "DatabaseFactory only allowed in Factory classes. Use DI."
},
{
"id": "H4",
"name": "no_new_repository_in_controller",
"pattern": r"new\s+\w+Repository\s*\(",
"must_match": False,
"applies_to": "/Controller/",
"message": "new Repository in Controller not allowed. Use DI via constructor."
},
{
"id": "H5",
"name": "no_new_infrastructure_in_controller",
"pattern": r"new\s+Infrastructure\\",
"must_match": False,
"applies_to": "/Controller/",
"message": "new Infrastructure in Controller not allowed. Use DI via constructor."
}
]
# Paths exempt from all rules
ALLOWED_PATHS = [
"/Factory/",
"/Bootstrap/",
"/tests/",
"/Test/",
"/vendor/",
]
def is_allowed_path(file_path: str) -> bool:
"""Check if file is in allowlist."""
for allowed in ALLOWED_PATHS:
if allowed in file_path:
return True
return False
def rule_applies(rule: dict, file_path: str) -> bool:
"""Check if rule applies to this file path."""
if "applies_to" in rule:
if rule["applies_to"] == "all":
return True
return rule["applies_to"] in file_path
if "applies_to_not" in rule:
return rule["applies_to_not"] not in file_path
return True
def check_rule(rule: dict, content: str) -> bool:
"""
Check if content violates the rule.
Returns True if VIOLATED, False if OK.
"""
match = re.search(rule["pattern"], content)
if rule["must_match"]:
# Pattern MUST be present
return match is None # Violated if NOT found
else:
# Pattern must NOT be present
return match is not None # Violated if found
def check_all_rules(file_path: str, content: str) -> dict:
"""
Check all rules against file.
Returns:
{"allowed": True} if all pass
{"allowed": False, "message": "..."} on first violation
"""
# Skip non-PHP files
if not file_path.endswith(".php"):
return {"allowed": True}
# Skip allowlisted paths
if is_allowed_path(file_path):
return {"allowed": True}
for rule in HARD_RULES:
if not rule_applies(rule, file_path):
continue
if check_rule(rule, content):
return {
"allowed": False,
"message": f"ARCHITECTURE VIOLATION [{rule['id']}]: {rule['message']}"
}
return {"allowed": True}
def format_output(result: dict) -> str:
"""Format output as JSON for Claude Code hook protocol."""
return json.dumps(result)
def main():
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
# Invalid input, allow by default
print(json.dumps({"allowed": True}))
sys.exit(0)
tool_name = input_data.get("tool_name", "")
# Only check Write operations
if tool_name != "Write":
print(json.dumps({"allowed": True}))
sys.exit(0)
tool_input = input_data.get("tool_input", {})
file_path = tool_input.get("file_path", "")
content = tool_input.get("content", "")
result = check_all_rules(file_path, content)
print(format_output(result))
if result["allowed"]:
sys.exit(0)
else:
sys.exit(0) # Exit 0 with allowed:false triggers block
if __name__ == "__main__":
main()