Protokoll #6565
| ID | 6565 |
|---|---|
| Zeitstempel | 2025-12-23 06:17:58.727146 |
| Client | root |
| IP | Warning: Undefined array key "request_ip" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 13 Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 13 |
| Modell | claude-sonnet-4-20250514 |
| Status | completed |
| Tokens | 6,509 (Input: 0, Output: 0) |
| Dauer | 97 ms |
| Request-Zeit | Warning: Undefined array key "request_timestamp" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 18 |
| Response-Zeit | - |
Warning: Undefined array key "error_message" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 22
Request
{
"event": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/architecture_guard.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nArchitecture Guard - Pre-Hook (Blocking)\n\nEnforces hard_constraints from architecture-gate-contract v1.1\nBlocks file creation on violation. No exceptions.\n\nRules:\n H1: strict_types_required (all PHP files)\n H2: domain_no_infrastructure (Domain layer)\n H3: db_factory_only (only in Factory classes)\n H4: no_new_repository_in_controller (Controller layer)\n H5: no_new_infrastructure_in_controller (Controller layer)\n\nTrigger: PreToolUse (Write) on *.php\n\"\"\"\n\nimport json\nimport re\nimport sys\n\n# Hard rules from architecture-gate-contract v1.1\nHARD_RULES = [\n {\n \"id\": \"H1\",\n \"name\": \"strict_types_required\",\n \"pattern\": r\"declare\\s*\\(\\s*strict_types\\s*=\\s*1\\s*\\)\",\n \"must_match\": True,\n \"applies_to\": \"all\",\n \"message\": \"Missing declare(strict_types=1). Add at top of file after <?php\"\n },\n {\n \"id\": \"H2\",\n \"name\": \"domain_no_infrastructure\",\n \"pattern\": r\"use\\s+Infrastructure\\\\\",\n \"must_match\": False,\n \"applies_to\": \"\/Domain\/\",\n \"message\": \"Domain must not use Infrastructure. Violates DIP.\"\n },\n {\n \"id\": \"H3\",\n \"name\": \"db_factory_only\",\n \"pattern\": r\"DatabaseFactory::\",\n \"must_match\": False,\n \"applies_to_not\": \"\/Factory\/\",\n \"message\": \"DatabaseFactory only allowed in Factory classes. Use DI.\"\n },\n {\n \"id\": \"H4\",\n \"name\": \"no_new_repository_in_controller\",\n \"pattern\": r\"new\\s+\\w+Repository\\s*\\(\",\n \"must_match\": False,\n \"applies_to\": \"\/Controller\/\",\n \"message\": \"new Repository in Controller not allowed. Use DI via constructor.\"\n },\n {\n \"id\": \"H5\",\n \"name\": \"no_new_infrastructure_in_controller\",\n \"pattern\": r\"new\\s+Infrastructure\\\\\",\n \"must_match\": False,\n \"applies_to\": \"\/Controller\/\",\n \"message\": \"new Infrastructure in Controller not allowed. Use DI via constructor.\"\n }\n]\n\n# Paths exempt from all rules\nALLOWED_PATHS = [\n \"\/Factory\/\",\n \"\/Bootstrap\/\",\n \"\/tests\/\",\n \"\/Test\/\",\n \"\/vendor\/\",\n]\n\n\ndef is_allowed_path(file_path: str) -> bool:\n \"\"\"Check if file is in allowlist.\"\"\"\n for allowed in ALLOWED_PATHS:\n if allowed in file_path:\n return True\n return False\n\n\ndef rule_applies(rule: dict, file_path: str) -> bool:\n \"\"\"Check if rule applies to this file path.\"\"\"\n if \"applies_to\" in rule:\n if rule[\"applies_to\"] == \"all\":\n return True\n return rule[\"applies_to\"] in file_path\n\n if \"applies_to_not\" in rule:\n return rule[\"applies_to_not\"] not in file_path\n\n return True\n\n\ndef check_rule(rule: dict, content: str) -> bool:\n \"\"\"\n Check if content violates the rule.\n\n Returns True if VIOLATED, False if OK.\n \"\"\"\n match = re.search(rule[\"pattern\"], content)\n\n if rule[\"must_match\"]:\n # Pattern MUST be present\n return match is None # Violated if NOT found\n else:\n # Pattern must NOT be present\n return match is not None # Violated if found\n\n\ndef check_all_rules(file_path: str, content: str) -> dict:\n \"\"\"\n Check all rules against file.\n\n Returns:\n {\"allowed\": True} if all pass\n {\"allowed\": False, \"message\": \"...\"} on first violation\n \"\"\"\n # Skip non-PHP files\n if not file_path.endswith(\".php\"):\n return {\"allowed\": True}\n\n # Skip allowlisted paths\n if is_allowed_path(file_path):\n return {\"allowed\": True}\n\n for rule in HARD_RULES:\n if not rule_applies(rule, file_path):\n continue\n\n if check_rule(rule, content):\n return {\n \"allowed\": False,\n \"message\": f\"ARCHITECTURE VIOLATION [{rule['id']}]: {rule['message']}\"\n }\n\n return {\"allowed\": True}\n\n\ndef format_output(result: dict) -> str:\n \"\"\"Format output as JSON for Claude Code hook protocol.\"\"\"\n return json.dumps(result)\n\n\ndef main():\n try:\n input_data = json.load(sys.stdin)\n except json.JSONDecodeError:\n # Invalid input, allow by default\n print(json.dumps({\"allowed\": True}))\n sys.exit(0)\n\n tool_name = input_data.get(\"tool_name\", \"\")\n\n # Only check Write operations\n if tool_name != \"Write\":\n print(json.dumps({\"allowed\": True}))\n sys.exit(0)\n\n tool_input = input_data.get(\"tool_input\", {})\n file_path = tool_input.get(\"file_path\", \"\")\n content = tool_input.get(\"content\", \"\")\n\n result = check_all_rules(file_path, content)\n\n print(format_output(result))\n\n if result[\"allowed\"]:\n sys.exit(0)\n else:\n sys.exit(0) # Exit 0 with allowed:false triggers block\n\n\nif __name__ == \"__main__\":\n main()\n"
}
}
Response
{
"tool_response": {
"type": "update",
"filePath": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/architecture_guard.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nArchitecture Guard - Pre-Hook (Blocking)\n\nEnforces hard_constraints from architecture-gate-contract v1.1\nBlocks file creation on violation. No exceptions.\n\nRules:\n H1: strict_types_required (all PHP files)\n H2: domain_no_infrastructure (Domain layer)\n H3: db_factory_only (only in Factory classes)\n H4: no_new_repository_in_controller (Controller layer)\n H5: no_new_infrastructure_in_controller (Controller layer)\n\nTrigger: PreToolUse (Write) on *.php\n\"\"\"\n\nimport json\nimport re\nimport sys\n\n# Hard rules from architecture-gate-contract v1.1\nHARD_RULES = [\n {\n \"id\": \"H1\",\n \"name\": \"strict_types_required\",\n \"pattern\": r\"declare\\s*\\(\\s*strict_types\\s*=\\s*1\\s*\\)\",\n \"must_match\": True,\n \"applies_to\": \"all\",\n \"message\": \"Missing declare(strict_types=1). Add at top of file after <?php\"\n },\n {\n \"id\": \"H2\",\n \"name\": \"domain_no_infrastructure\",\n \"pattern\": r\"use\\s+Infrastructure\\\\\",\n \"must_match\": False,\n \"applies_to\": \"\/Domain\/\",\n \"message\": \"Domain must not use Infrastructure. Violates DIP.\"\n },\n {\n \"id\": \"H3\",\n \"name\": \"db_factory_only\",\n \"pattern\": r\"DatabaseFactory::\",\n \"must_match\": False,\n \"applies_to_not\": \"\/Factory\/\",\n \"message\": \"DatabaseFactory only allowed in Factory classes. Use DI.\"\n },\n {\n \"id\": \"H4\",\n \"name\": \"no_new_repository_in_controller\",\n \"pattern\": r\"new\\s+\\w+Repository\\s*\\(\",\n \"must_match\": False,\n \"applies_to\": \"\/Controller\/\",\n \"message\": \"new Repository in Controller not allowed. Use DI via constructor.\"\n },\n {\n \"id\": \"H5\",\n \"name\": \"no_new_infrastructure_in_controller\",\n \"pattern\": r\"new\\s+Infrastructure\\\\\",\n \"must_match\": False,\n \"applies_to\": \"\/Controller\/\",\n \"message\": \"new Infrastructure in Controller not allowed. Use DI via constructor.\"\n }\n]\n\n# Paths exempt from all rules\nALLOWED_PATHS = [\n \"\/Factory\/\",\n \"\/Bootstrap\/\",\n \"\/tests\/\",\n \"\/Test\/\",\n \"\/vendor\/\",\n]\n\n\ndef is_allowed_path(file_path: str) -> bool:\n \"\"\"Check if file is in allowlist.\"\"\"\n for allowed in ALLOWED_PATHS:\n if allowed in file_path:\n return True\n return False\n\n\ndef rule_applies(rule: dict, file_path: str) -> bool:\n \"\"\"Check if rule applies to this file path.\"\"\"\n if \"applies_to\" in rule:\n if rule[\"applies_to\"] == \"all\":\n return True\n return rule[\"applies_to\"] in file_path\n\n if \"applies_to_not\" in rule:\n return rule[\"applies_to_not\"] not in file_path\n\n return True\n\n\ndef check_rule(rule: dict, content: str) -> bool:\n \"\"\"\n Check if content violates the rule.\n\n Returns True if VIOLATED, False if OK.\n \"\"\"\n match = re.search(rule[\"pattern\"], content)\n\n if rule[\"must_match\"]:\n # Pattern MUST be present\n return match is None # Violated if NOT found\n else:\n # Pattern must NOT be present\n return match is not None # Violated if found\n\n\ndef check_all_rules(file_path: str, content: str) -> dict:\n \"\"\"\n Check all rules against file.\n\n Returns:\n {\"allowed\": True} if all pass\n {\"allowed\": False, \"message\": \"...\"} on first violation\n \"\"\"\n # Skip non-PHP files\n if not file_path.endswith(\".php\"):\n return {\"allowed\": True}\n\n # Skip allowlisted paths\n if is_allowed_path(file_path):\n return {\"allowed\": True}\n\n for rule in HARD_RULES:\n if not rule_applies(rule, file_path):\n continue\n\n if check_rule(rule, content):\n return {\n \"allowed\": False,\n \"message\": f\"ARCHITECTURE VIOLATION [{rule['id']}]: {rule['message']}\"\n }\n\n return {\"allowed\": True}\n\n\ndef format_output(result: dict) -> str:\n \"\"\"Format output as JSON for Claude Code hook protocol.\"\"\"\n return json.dumps(result)\n\n\ndef main():\n try:\n input_data = json.load(sys.stdin)\n except json.JSONDecodeError:\n # Invalid input, allow by default\n print(json.dumps({\"allowed\": True}))\n sys.exit(0)\n\n tool_name = input_data.get(\"tool_name\", \"\")\n\n # Only check Write operations\n if tool_name != \"Write\":\n print(json.dumps({\"allowed\": True}))\n sys.exit(0)\n\n tool_input = input_data.get(\"tool_input\", {})\n file_path = tool_input.get(\"file_path\", \"\")\n content = tool_input.get(\"content\", \"\")\n\n result = check_all_rules(file_path, content)\n\n print(format_output(result))\n\n if result[\"allowed\"]:\n sys.exit(0)\n else:\n sys.exit(0) # Exit 0 with allowed:false triggers block\n\n\nif __name__ == \"__main__\":\n main()\n",
"structuredPatch": [
{
"oldStart": 1,
"oldLines": 167,
"newStart": 1,
"newLines": 177,
"lines": [
" #!\/usr\/bin\/env python3",
" \"\"\"",
"-Architecture Guard Hook",
"+Architecture Guard - Pre-Hook (Blocking)",
" ",
"-Blockiert Architektur-Verletzungen BEVOR Code geschrieben wird.",
"-- Controller > 500 LOC: Block",
"-- Controller > 400 LOC: Warnung",
"-- PDO\/SQL in Controller: Block",
"+Enforces hard_constraints from architecture-gate-contract v1.1",
"+Blocks file creation on violation. No exceptions.",
" ",
"-Trigger: PreToolUse (Write, Edit)",
"+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",
" ",
"-# Patterns für direkten DB-Zugriff in Controllern",
"-PDO_PATTERNS = [",
"- r'\\$this->db\\s*=', # $this->db = ...",
"- r'\\$this->db->', # $this->db->query() etc.",
"- r'new\\s+\\\\?PDO\\(', # new PDO( oder new \\PDO(",
"- r'->query\\s*\\(', # ->query(",
"- r'->prepare\\s*\\(', # ->prepare(",
"- r'->exec\\s*\\(', # ->exec(",
"- r'DatabaseFactory::', # Direkter Factory-Zugriff",
"- r'getDatabase\\(\\)', # getDatabase() Methode",
"+# 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.\"",
"+ }",
" ]",
" ",
"-LOC_WARNING_THRESHOLD = 400",
"-LOC_BLOCK_THRESHOLD = 500",
"+# Paths exempt from all rules",
"+ALLOWED_PATHS = [",
"+ \"\/Factory\/\",",
"+ \"\/Bootstrap\/\",",
"+ \"\/tests\/\",",
"+ \"\/Test\/\",",
"+ \"\/vendor\/\",",
"+]",
" ",
" ",
"-def count_lines(content: str) -> int:",
"- \"\"\"Zählt nicht-leere Zeilen.\"\"\"",
"- return len([line for line in content.split('\\n') if line.strip()])",
"+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 is_controller_file(file_path: str) -> bool:",
"- \"\"\"Prüft ob es sich um einen Controller handelt.\"\"\"",
"- return '\/Controller\/' in file_path and file_path.endswith('.php')",
"+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",
" ",
"-def check_pdo_patterns(content: str) -> list[str]:",
"- \"\"\"Findet PDO\/SQL-Patterns im Content.\"\"\"",
"- found = []",
"- for pattern in PDO_PATTERNS:",
"- if re.search(pattern, content):",
"- found.append(pattern)",
"- return found",
"+ return True",
" ",
" ",
"-def check_controller(file_path: str, content: str) -> dict:",
"+def check_rule(rule: dict, content: str) -> bool:",
" \"\"\"",
"- Prüft Controller auf Architektur-Verletzungen.",
"+ Check if content violates the rule.",
" ",
"- Returns:",
"- dict mit keys: block, warn, reason",
"+ Returns True if VIOLATED, False if OK.",
" \"\"\"",
"- if not is_controller_file(file_path):",
"- return {'block': False, 'warn': False, 'reason': ''}",
"+ match = re.search(rule[\"pattern\"], content)",
" ",
"- result = {'block': False, 'warn': False, 'reason': ''}",
"- messages = []",
"+ 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",
" ",
"- # LOC-Prüfung",
"- loc = count_lines(content)",
" ",
"- if loc > LOC_BLOCK_THRESHOLD:",
"- result['block'] = True",
"- messages.append(f\"Controller hat {loc} LOC (max {LOC_BLOCK_THRESHOLD})\")",
"- elif loc > LOC_WARNING_THRESHOLD:",
"- result['warn'] = True",
"- messages.append(f\"Controller nähert sich LOC-Limit: {loc}\/{LOC_BLOCK_THRESHOLD}\")",
"+def check_all_rules(file_path: str, content: str) -> dict:",
"+ \"\"\"",
"+ Check all rules against file.",
" ",
"- # PDO-Pattern-Prüfung",
"- pdo_matches = check_pdo_patterns(content)",
"+ 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}",
" ",
"- if pdo_matches:",
"- result['block'] = True",
"- messages.append(f\"Controller enthält direkten DB-Zugriff: {', '.join(pdo_matches[:3])}\")",
"+ # Skip allowlisted paths",
"+ if is_allowed_path(file_path):",
"+ return {\"allowed\": True}",
" ",
"- if messages:",
"- result['reason'] = '\\n'.join(messages)",
"+ for rule in HARD_RULES:",
"+ if not rule_applies(rule, file_path):",
"+ continue",
" ",
"- return result",
"+ if check_rule(rule, content):",
"+ return {",
"+ \"allowed\": False,",
"+ \"message\": f\"ARCHITECTURE VIOLATION [{rule['id']}]: {rule['message']}\"",
"+ }",
" ",
"+ return {\"allowed\": True}",
" ",
"-def format_block_message(reason: str) -> str:",
"- \"\"\"Formatiert Blocknachricht mit Lösungshinweisen.\"\"\"",
"- return f\"\"\"ARCHITEKTUR-VERLETZUNG BLOCKIERT!",
" ",
"-{reason}",
"+def format_output(result: dict) -> str:",
"+ \"\"\"Format output as JSON for Claude Code hook protocol.\"\"\"",
"+ return json.dumps(result)",
" ",
"-Lösungen:",
"-1. LOC zu hoch: Service\/Repository extrahieren",
"-2. PDO in Controller:",
"- - Nutze Repository-Pattern",
"- - Erstelle Infrastructure\/Persistence\/*Repository.php",
"- - Controller ruft nur Repository-Methoden auf",
" ",
"-Beispiel:",
"- \/\/ FALSCH (im Controller):",
"- $this->db->query(\"SELECT * FROM users\")",
"-",
"- \/\/ RICHTIG:",
"- $this->userRepository->findAll()",
"-",
"-Architektur-Regeln: \/var\/www\/docs\/contracts\/code-quality-standards.yaml\"\"\"",
"-",
"-",
"-def format_warning_message(reason: str) -> str:",
"- \"\"\"Formatiert Warnungsnachricht.\"\"\"",
"- return f\"\"\"ARCHITEKTUR-WARNUNG:",
"-",
"-{reason}",
"-",
"-Empfehlung: Erwäge Refactoring bevor das Limit erreicht wird.\"\"\"",
"-",
"-",
" 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', '')",
"+ tool_name = input_data.get(\"tool_name\", \"\")",
" ",
"- # Nur Write und Edit prüfen",
"- if tool_name not in ('Write', 'Edit'):",
"+ # 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', '')",
"+ tool_input = input_data.get(\"tool_input\", {})",
"+ file_path = tool_input.get(\"file_path\", \"\")",
"+ content = tool_input.get(\"content\", \"\")",
" ",
"- # Content ermitteln",
"- if tool_name == 'Write':",
"- content = tool_input.get('content', '')",
"- else: # Edit",
"- # Bei Edit: neuen String prüfen (vereinfacht)",
"- content = tool_input.get('new_string', '')",
"- # Edit-Prüfung nur auf PDO-Patterns, nicht LOC",
"- if is_controller_file(file_path):",
"- pdo_matches = check_pdo_patterns(content)",
"- if pdo_matches:",
"- print(format_block_message(",
"- f\"Edit fügt direkten DB-Zugriff hinzu: {', '.join(pdo_matches[:3])}\"",
"- ), file=sys.stderr)",
"- sys.exit(2)",
"- sys.exit(0)",
"+ result = check_all_rules(file_path, content)",
" ",
"- # Vollständige Prüfung nur bei Write",
"- result = check_controller(file_path, content)",
"+ print(format_output(result))",
" ",
"- if result['block']:",
"- print(format_block_message(result['reason']), file=sys.stderr)",
"- sys.exit(2)",
"-",
"- if result['warn']:",
"- # Warnung ausgeben aber durchlassen",
"- print(format_warning_message(result['reason']), file=sys.stderr)",
"+ if result[\"allowed\"]:",
" sys.exit(0)",
"+ else:",
"+ sys.exit(0) # Exit 0 with allowed:false triggers block",
" ",
"- sys.exit(0)",
" ",
"-",
"-if __name__ == '__main__':",
"+if __name__ == \"__main__\":",
" main()"
]
}
],
"originalFile": "#!\/usr\/bin\/env python3\n\"\"\"\nArchitecture Guard Hook\n\nBlockiert Architektur-Verletzungen BEVOR Code geschrieben wird.\n- Controller > 500 LOC: Block\n- Controller > 400 LOC: Warnung\n- PDO\/SQL in Controller: Block\n\nTrigger: PreToolUse (Write, Edit)\n\"\"\"\n\nimport json\nimport re\nimport sys\n\n# Patterns für direkten DB-Zugriff in Controllern\nPDO_PATTERNS = [\n r'\\$this->db\\s*=', # $this->db = ...\n r'\\$this->db->', # $this->db->query() etc.\n r'new\\s+\\\\?PDO\\(', # new PDO( oder new \\PDO(\n r'->query\\s*\\(', # ->query(\n r'->prepare\\s*\\(', # ->prepare(\n r'->exec\\s*\\(', # ->exec(\n r'DatabaseFactory::', # Direkter Factory-Zugriff\n r'getDatabase\\(\\)', # getDatabase() Methode\n]\n\nLOC_WARNING_THRESHOLD = 400\nLOC_BLOCK_THRESHOLD = 500\n\n\ndef count_lines(content: str) -> int:\n \"\"\"Zählt nicht-leere Zeilen.\"\"\"\n return len([line for line in content.split('\\n') if line.strip()])\n\n\ndef is_controller_file(file_path: str) -> bool:\n \"\"\"Prüft ob es sich um einen Controller handelt.\"\"\"\n return '\/Controller\/' in file_path and file_path.endswith('.php')\n\n\ndef check_pdo_patterns(content: str) -> list[str]:\n \"\"\"Findet PDO\/SQL-Patterns im Content.\"\"\"\n found = []\n for pattern in PDO_PATTERNS:\n if re.search(pattern, content):\n found.append(pattern)\n return found\n\n\ndef check_controller(file_path: str, content: str) -> dict:\n \"\"\"\n Prüft Controller auf Architektur-Verletzungen.\n\n Returns:\n dict mit keys: block, warn, reason\n \"\"\"\n if not is_controller_file(file_path):\n return {'block': False, 'warn': False, 'reason': ''}\n\n result = {'block': False, 'warn': False, 'reason': ''}\n messages = []\n\n # LOC-Prüfung\n loc = count_lines(content)\n\n if loc > LOC_BLOCK_THRESHOLD:\n result['block'] = True\n messages.append(f\"Controller hat {loc} LOC (max {LOC_BLOCK_THRESHOLD})\")\n elif loc > LOC_WARNING_THRESHOLD:\n result['warn'] = True\n messages.append(f\"Controller nähert sich LOC-Limit: {loc}\/{LOC_BLOCK_THRESHOLD}\")\n\n # PDO-Pattern-Prüfung\n pdo_matches = check_pdo_patterns(content)\n\n if pdo_matches:\n result['block'] = True\n messages.append(f\"Controller enthält direkten DB-Zugriff: {', '.join(pdo_matches[:3])}\")\n\n if messages:\n result['reason'] = '\\n'.join(messages)\n\n return result\n\n\ndef format_block_message(reason: str) -> str:\n \"\"\"Formatiert Blocknachricht mit Lösungshinweisen.\"\"\"\n return f\"\"\"ARCHITEKTUR-VERLETZUNG BLOCKIERT!\n\n{reason}\n\nLösungen:\n1. LOC zu hoch: Service\/Repository extrahieren\n2. PDO in Controller:\n - Nutze Repository-Pattern\n - Erstelle Infrastructure\/Persistence\/*Repository.php\n - Controller ruft nur Repository-Methoden auf\n\nBeispiel:\n \/\/ FALSCH (im Controller):\n $this->db->query(\"SELECT * FROM users\")\n\n \/\/ RICHTIG:\n $this->userRepository->findAll()\n\nArchitektur-Regeln: \/var\/www\/docs\/contracts\/code-quality-standards.yaml\"\"\"\n\n\ndef format_warning_message(reason: str) -> str:\n \"\"\"Formatiert Warnungsnachricht.\"\"\"\n return f\"\"\"ARCHITEKTUR-WARNUNG:\n\n{reason}\n\nEmpfehlung: Erwäge Refactoring bevor das Limit erreicht wird.\"\"\"\n\n\ndef main():\n try:\n input_data = json.load(sys.stdin)\n except json.JSONDecodeError:\n sys.exit(0)\n\n tool_name = input_data.get('tool_name', '')\n\n # Nur Write und Edit prüfen\n if tool_name not in ('Write', 'Edit'):\n sys.exit(0)\n\n tool_input = input_data.get('tool_input', {})\n file_path = tool_input.get('file_path', '')\n\n # Content ermitteln\n if tool_name == 'Write':\n content = tool_input.get('content', '')\n else: # Edit\n # Bei Edit: neuen String prüfen (vereinfacht)\n content = tool_input.get('new_string', '')\n # Edit-Prüfung nur auf PDO-Patterns, nicht LOC\n if is_controller_file(file_path):\n pdo_matches = check_pdo_patterns(content)\n if pdo_matches:\n print(format_block_message(\n f\"Edit fügt direkten DB-Zugriff hinzu: {', '.join(pdo_matches[:3])}\"\n ), file=sys.stderr)\n sys.exit(2)\n sys.exit(0)\n\n # Vollständige Prüfung nur bei Write\n result = check_controller(file_path, content)\n\n if result['block']:\n print(format_block_message(result['reason']), file=sys.stderr)\n sys.exit(2)\n\n if result['warn']:\n # Warnung ausgeben aber durchlassen\n print(format_warning_message(result['reason']), file=sys.stderr)\n sys.exit(0)\n\n sys.exit(0)\n\n\nif __name__ == '__main__':\n main()\n"
}
}