{
"tool_response": {
"filePath": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/quality\/pre_rules.py",
"oldString": "#!\/usr\/bin\/env python3\n\"\"\"\nPre-Hook Regeln (BLOCK) - Beliebig erweiterbar ohne Claude-Neustart.\n\nAlle P-Regeln aus dem Quality Gate Plan.\n\"\"\"\n\nimport re\nfrom pathlib import Path\nfrom typing import Optional\n\n# =============================================================================\n# ALLOWLIST - Pfade die von allen Prüfungen ausgenommen sind\n# =============================================================================\n\nGLOBAL_ALLOWLIST = [\n \"\/vendor\/\",\n \"\/tests\/\",\n \"\/Test\/\",\n]\n\nFACTORY_ALLOWLIST = [\n \"\/Factory\/\",\n \"\/Bootstrap\/\",\n]\n\n# =============================================================================\n# COMMON NUMBERS - Erlaubte Magic Numbers (für W5.2, hier zur Referenz)\n# =============================================================================\n\nCOMMON_NUMBERS = {\n '0', '1', '2',\n '10', '100', '1000',\n '60', '24', '365',\n '30', '31', '28', '29',\n '12', '52', '7',\n '200', '201', '204',\n '301', '302', '304',\n '400', '401', '403', '404', '500',\n}\n\n# =============================================================================\n# HELPER FUNCTIONS\n# =============================================================================\n\ndef is_in_allowlist(file_path: str, allowlist: list) -> bool:\n \"\"\"Prüft ob Pfad in Allowlist ist.\"\"\"\n return any(allowed in file_path for allowed in allowlist)\n\n\ndef block(rule_id: str, message: str) -> dict:\n \"\"\"Erzeugt Block-Response.\"\"\"\n return {\n \"allowed\": False,\n \"message\": f\"QUALITY VIOLATION [{rule_id}]: {message}\"\n }\n\n\ndef allow() -> dict:\n \"\"\"Erzeugt Allow-Response.\"\"\"\n return {\"allowed\": True}\n\n\n# =============================================================================\n# PRÜFUNG 1: SRP + KISS\n# =============================================================================\n\ndef p1_1_responsibility_header(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P1.1: @responsibility Header erforderlich.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n header_pattern = r\"\/\/\\s*@responsibility:\\s*(.+)\"\n match = re.search(header_pattern, content)\n\n if not match:\n return block(\"P1.1\", \"Missing @responsibility header. Add: \/\/ @responsibility: <single-responsibility>\")\n\n responsibility_text = match.group(1).strip()\n\n # Prüfe auf Multi-Responsibility-Wörter\n multi_words = r\"\\b(und|sowie|außerdem|also|zusätzlich|and|also|additionally)\\b\"\n if re.search(multi_words, responsibility_text, re.IGNORECASE):\n return block(\"P1.1\", f\"@responsibility contains multi-responsibility indicator: '{responsibility_text}'\")\n\n return None\n\n\ndef p1_2_garbage_names(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P1.2: Müllhalden-Namen blockieren.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n filename = Path(file_path).stem.lower()\n forbidden = [\"helper\", \"utils\", \"common\", \"misc\", \"base\"]\n\n for term in forbidden:\n if term in filename:\n return block(\"P1.2\", f\"Forbidden name pattern: '{term}' indicates unclear responsibility\")\n\n return None\n\n\n# =============================================================================\n# PRÜFUNG 2: MVC + CRUD\n# =============================================================================\n\ndef p2_1_no_sql_in_controller(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P2.1: Keine SQL-Statements in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n sql_patterns = [\n r\"\\bSELECT\\s+.+\\s+FROM\\b\",\n r\"\\bINSERT\\s+INTO\\b\",\n r\"\\bUPDATE\\s+\\w+\\s+SET\\b\",\n r\"\\bDELETE\\s+FROM\\b\",\n ]\n\n for pattern in sql_patterns:\n if re.search(pattern, content, re.IGNORECASE):\n return block(\"P2.1\", \"SQL statement in Controller. Move to Repository.\")\n\n return None\n\n\ndef p2_2_no_transactions_in_controller(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P2.2: Keine Transaktionen in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n transaction_patterns = [\n r\"\\bbeginTransaction\\s*\\(\",\n r\"\\bcommit\\s*\\(\",\n r\"\\brollBack\\s*\\(\",\n r\"\\brollback\\s*\\(\",\n ]\n\n for pattern in transaction_patterns:\n if re.search(pattern, content):\n return block(\"P2.2\", \"Transaction control in Controller. Move to UseCase or Repository.\")\n\n return None\n\n\ndef p2_3_no_echo_in_controller(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P2.3: Kein echo\/print in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n output_patterns = [\n r\"\\becho\\s+\",\n r\"\\becho\\(\",\n r\"\\bprint\\s+\",\n r\"\\bprint\\(\",\n ]\n\n for pattern in output_patterns:\n if re.search(pattern, content):\n return block(\"P2.3\", \"Direct output (echo\/print) in Controller. Use Response object.\")\n\n return None\n\n\ndef p2_4_no_db_in_usecases(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P2.4: Keine DB-Artefakte in UseCases.\"\"\"\n if \"\/UseCases\/\" not in file_path and \"\/Application\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n db_patterns = [\n r\"\\bnew\\s+PDO\\b\",\n r\"\\bPDO::\",\n r\"\\bDatabaseFactory::\",\n r\"\\bSELECT\\s+.+\\s+FROM\\b\",\n r\"\\bINSERT\\s+INTO\\b\",\n r\"\\bUPDATE\\s+\\w+\\s+SET\\b\",\n r\"\\bDELETE\\s+FROM\\b\",\n ]\n\n for pattern in db_patterns:\n if re.search(pattern, content, re.IGNORECASE):\n return block(\"P2.4\", \"DB artifact in UseCase\/Application. Use Repository interface.\")\n\n return None\n\n\n# =============================================================================\n# PRÜFUNG 3: PSR + Types\n# =============================================================================\n\ndef p3_1_strict_types(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P3.1: strict_types erforderlich.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n strict_pattern = r\"declare\\s*\\(\\s*strict_types\\s*=\\s*1\\s*\\)\\s*;\"\n if not re.search(strict_pattern, content):\n return block(\"P3.1\", \"Missing declare(strict_types=1)\")\n\n return None\n\n\ndef p3_2_namespace_matches_path(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P3.2: Namespace muss Pfad entsprechen.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n # Namespace-Mapping\n path_to_namespace = {\n \"\/src\/Controller\/\": \"Controller\\\\\",\n \"\/src\/Domain\/\": \"Domain\\\\\",\n \"\/src\/UseCases\/\": \"UseCases\\\\\",\n \"\/src\/Application\/\": \"Application\\\\\",\n \"\/src\/Infrastructure\/\": \"Infrastructure\\\\\",\n \"\/src\/Framework\/\": \"Framework\\\\\",\n \"\/app\/\": \"App\\\\\",\n }\n\n namespace_match = re.search(r\"namespace\\s+([^;]+);\", content)\n if not namespace_match:\n return None # Kein Namespace = kein Check\n\n declared_namespace = namespace_match.group(1).strip()\n\n for path_prefix, ns_prefix in path_to_namespace.items():\n if path_prefix in file_path:\n if not declared_namespace.startswith(ns_prefix.rstrip(\"\\\\\")):\n return block(\"P3.2\", f\"Namespace '{declared_namespace}' does not match path. Expected: '{ns_prefix}...'\")\n break\n\n return None\n\n\ndef p3_3_classname_matches_filename(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P3.3: Klassenname muss Dateiname entsprechen.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n class_match = re.search(r\"(?:class|interface|trait|enum)\\s+(\\w+)\", content)\n if not class_match:\n return None # Keine Klasse = kein Check\n\n class_name = class_match.group(1)\n expected_name = Path(file_path).stem\n\n if class_name != expected_name:\n return block(\"P3.3\", f\"Class '{class_name}' does not match filename '{expected_name}'\")\n\n return None\n\n\ndef p3_4_public_method_return_type(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P3.4: Public Methods müssen Return-Type haben.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n # Finde public functions\n public_methods = re.findall(\n r\"public\\s+function\\s+(\\w+)\\s*\\([^)]*\\)\\s*(?::\\s*\\S+)?\",\n content\n )\n\n for method in public_methods:\n if method.startswith(\"__\"):\n continue # Magic methods überspringen\n\n # Prüfe ob Return-Type vorhanden\n pattern = rf\"public\\s+function\\s+{method}\\s*\\([^)]*\\)\\s*:\\s*\\S+\"\n if not re.search(pattern, content):\n return block(\"P3.4\", f\"Public method '{method}' missing return type\")\n\n return None\n\n\n# =============================================================================\n# PRÜFUNG 4: OOP\n# =============================================================================\n\ndef p4_1_no_public_properties_in_domain(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P4.1: Keine public Properties in Domain.\"\"\"\n if \"\/Domain\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n # public readonly ist erlaubt\n public_props = re.findall(\n r\"public\\s+(?!readonly|function|const|static\\s+function).*\\$\\w+\",\n content\n )\n\n if public_props:\n return block(\"P4.1\", \"Public property in Domain. Use getter methods or public readonly.\")\n\n return None\n\n\n# =============================================================================\n# PRÜFUNG 6: SOLID + DIP\n# =============================================================================\n\ndef p6_1_application_no_infrastructure_import(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P6.1: UseCases\/Application darf Infrastructure nicht importieren.\"\"\"\n if \"\/UseCases\/\" not in file_pat... [TRUNCATED-a52e2f1d0fd5bcc2]",
"newString": "#!\/usr\/bin\/env python3\n\"\"\"\nPre-Hook Regeln (BLOCK) - Modular und erweiterbar ohne Claude-Neustart.\n\nAlle P-Regeln aus dem Quality Gate Plan.\nImportiert Regeln aus spezialisierten Modulen.\n\"\"\"\n\nfrom . import pre_rules_guard\nfrom . import pre_rules_security\nfrom . import pre_rules_validation\n\n\n# =============================================================================\n# HELPER FUNCTIONS\n# =============================================================================\n\ndef allow() -> dict:\n \"\"\"Erzeugt Allow-Response.\"\"\"\n return {\"allowed\": True}\n\n\n# =============================================================================\n# MAIN CHECK FUNCTION\n# =============================================================================\n\n# Alle aktiven Regeln in Prüfungsreihenfolge\nRULES = [\n # Prüfung 1: SRP + KISS + Prüfung 4: OOP\n *pre_rules_guard.RULES,\n\n # Prüfung 2: MVC + CRUD + Prüfung 6: SOLID + DIP + H-Regeln\n *pre_rules_security.RULES,\n\n # Prüfung 3: PSR + Types\n *pre_rules_validation.RULES,\n]",
"originalFile": "#!\/usr\/bin\/env python3\n\"\"\"\nPre-Hook Regeln (BLOCK) - Beliebig erweiterbar ohne Claude-Neustart.\n\nAlle P-Regeln aus dem Quality Gate Plan.\n\"\"\"\n\nimport re\nfrom pathlib import Path\nfrom typing import Optional\n\n# =============================================================================\n# ALLOWLIST - Pfade die von allen Prüfungen ausgenommen sind\n# =============================================================================\n\nGLOBAL_ALLOWLIST = [\n \"\/vendor\/\",\n \"\/tests\/\",\n \"\/Test\/\",\n]\n\nFACTORY_ALLOWLIST = [\n \"\/Factory\/\",\n \"\/Bootstrap\/\",\n]\n\n# =============================================================================\n# COMMON NUMBERS - Erlaubte Magic Numbers (für W5.2, hier zur Referenz)\n# =============================================================================\n\nCOMMON_NUMBERS = {\n '0', '1', '2',\n '10', '100', '1000',\n '60', '24', '365',\n '30', '31', '28', '29',\n '12', '52', '7',\n '200', '201', '204',\n '301', '302', '304',\n '400', '401', '403', '404', '500',\n}\n\n# =============================================================================\n# HELPER FUNCTIONS\n# =============================================================================\n\ndef is_in_allowlist(file_path: str, allowlist: list) -> bool:\n \"\"\"Prüft ob Pfad in Allowlist ist.\"\"\"\n return any(allowed in file_path for allowed in allowlist)\n\n\ndef block(rule_id: str, message: str) -> dict:\n \"\"\"Erzeugt Block-Response.\"\"\"\n return {\n \"allowed\": False,\n \"message\": f\"QUALITY VIOLATION [{rule_id}]: {message}\"\n }\n\n\ndef allow() -> dict:\n \"\"\"Erzeugt Allow-Response.\"\"\"\n return {\"allowed\": True}\n\n\n# =============================================================================\n# PRÜFUNG 1: SRP + KISS\n# =============================================================================\n\ndef p1_1_responsibility_header(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P1.1: @responsibility Header erforderlich.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n header_pattern = r\"\/\/\\s*@responsibility:\\s*(.+)\"\n match = re.search(header_pattern, content)\n\n if not match:\n return block(\"P1.1\", \"Missing @responsibility header. Add: \/\/ @responsibility: <single-responsibility>\")\n\n responsibility_text = match.group(1).strip()\n\n # Prüfe auf Multi-Responsibility-Wörter\n multi_words = r\"\\b(und|sowie|außerdem|also|zusätzlich|and|also|additionally)\\b\"\n if re.search(multi_words, responsibility_text, re.IGNORECASE):\n return block(\"P1.1\", f\"@responsibility contains multi-responsibility indicator: '{responsibility_text}'\")\n\n return None\n\n\ndef p1_2_garbage_names(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P1.2: Müllhalden-Namen blockieren.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n filename = Path(file_path).stem.lower()\n forbidden = [\"helper\", \"utils\", \"common\", \"misc\", \"base\"]\n\n for term in forbidden:\n if term in filename:\n return block(\"P1.2\", f\"Forbidden name pattern: '{term}' indicates unclear responsibility\")\n\n return None\n\n\n# =============================================================================\n# PRÜFUNG 2: MVC + CRUD\n# =============================================================================\n\ndef p2_1_no_sql_in_controller(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P2.1: Keine SQL-Statements in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n sql_patterns = [\n r\"\\bSELECT\\s+.+\\s+FROM\\b\",\n r\"\\bINSERT\\s+INTO\\b\",\n r\"\\bUPDATE\\s+\\w+\\s+SET\\b\",\n r\"\\bDELETE\\s+FROM\\b\",\n ]\n\n for pattern in sql_patterns:\n if re.search(pattern, content, re.IGNORECASE):\n return block(\"P2.1\", \"SQL statement in Controller. Move to Repository.\")\n\n return None\n\n\ndef p2_2_no_transactions_in_controller(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P2.2: Keine Transaktionen in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n transaction_patterns = [\n r\"\\bbeginTransaction\\s*\\(\",\n r\"\\bcommit\\s*\\(\",\n r\"\\brollBack\\s*\\(\",\n r\"\\brollback\\s*\\(\",\n ]\n\n for pattern in transaction_patterns:\n if re.search(pattern, content):\n return block(\"P2.2\", \"Transaction control in Controller. Move to UseCase or Repository.\")\n\n return None\n\n\ndef p2_3_no_echo_in_controller(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P2.3: Kein echo\/print in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n output_patterns = [\n r\"\\becho\\s+\",\n r\"\\becho\\(\",\n r\"\\bprint\\s+\",\n r\"\\bprint\\(\",\n ]\n\n for pattern in output_patterns:\n if re.search(pattern, content):\n return block(\"P2.3\", \"Direct output (echo\/print) in Controller. Use Response object.\")\n\n return None\n\n\ndef p2_4_no_db_in_usecases(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P2.4: Keine DB-Artefakte in UseCases.\"\"\"\n if \"\/UseCases\/\" not in file_path and \"\/Application\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n db_patterns = [\n r\"\\bnew\\s+PDO\\b\",\n r\"\\bPDO::\",\n r\"\\bDatabaseFactory::\",\n r\"\\bSELECT\\s+.+\\s+FROM\\b\",\n r\"\\bINSERT\\s+INTO\\b\",\n r\"\\bUPDATE\\s+\\w+\\s+SET\\b\",\n r\"\\bDELETE\\s+FROM\\b\",\n ]\n\n for pattern in db_patterns:\n if re.search(pattern, content, re.IGNORECASE):\n return block(\"P2.4\", \"DB artifact in UseCase\/Application. Use Repository interface.\")\n\n return None\n\n\n# =============================================================================\n# PRÜFUNG 3: PSR + Types\n# =============================================================================\n\ndef p3_1_strict_types(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P3.1: strict_types erforderlich.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n strict_pattern = r\"declare\\s*\\(\\s*strict_types\\s*=\\s*1\\s*\\)\\s*;\"\n if not re.search(strict_pattern, content):\n return block(\"P3.1\", \"Missing declare(strict_types=1)\")\n\n return None\n\n\ndef p3_2_namespace_matches_path(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P3.2: Namespace muss Pfad entsprechen.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n # Namespace-Mapping\n path_to_namespace = {\n \"\/src\/Controller\/\": \"Controller\\\\\",\n \"\/src\/Domain\/\": \"Domain\\\\\",\n \"\/src\/UseCases\/\": \"UseCases\\\\\",\n \"\/src\/Application\/\": \"Application\\\\\",\n \"\/src\/Infrastructure\/\": \"Infrastructure\\\\\",\n \"\/src\/Framework\/\": \"Framework\\\\\",\n \"\/app\/\": \"App\\\\\",\n }\n\n namespace_match = re.search(r\"namespace\\s+([^;]+);\", content)\n if not namespace_match:\n return None # Kein Namespace = kein Check\n\n declared_namespace = namespace_match.group(1).strip()\n\n for path_prefix, ns_prefix in path_to_namespace.items():\n if path_prefix in file_path:\n if not declared_namespace.startswith(ns_prefix.rstrip(\"\\\\\")):\n return block(\"P3.2\", f\"Namespace '{declared_namespace}' does not match path. Expected: '{ns_prefix}...'\")\n break\n\n return None\n\n\ndef p3_3_classname_matches_filename(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P3.3: Klassenname muss Dateiname entsprechen.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n class_match = re.search(r\"(?:class|interface|trait|enum)\\s+(\\w+)\", content)\n if not class_match:\n return None # Keine Klasse = kein Check\n\n class_name = class_match.group(1)\n expected_name = Path(file_path).stem\n\n if class_name != expected_name:\n return block(\"P3.3\", f\"Class '{class_name}' does not match filename '{expected_name}'\")\n\n return None\n\n\ndef p3_4_public_method_return_type(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P3.4: Public Methods müssen Return-Type haben.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n # Finde public functions\n public_methods = re.findall(\n r\"public\\s+function\\s+(\\w+)\\s*\\([^)]*\\)\\s*(?::\\s*\\S+)?\",\n content\n )\n\n for method in public_methods:\n if method.startswith(\"__\"):\n continue # Magic methods überspringen\n\n # Prüfe ob Return-Type vorhanden\n pattern = rf\"public\\s+function\\s+{method}\\s*\\([^)]*\\)\\s*:\\s*\\S+\"\n if not re.search(pattern, content):\n return block(\"P3.4\", f\"Public method '{method}' missing return type\")\n\n return None\n\n\n# =============================================================================\n# PRÜFUNG 4: OOP\n# =============================================================================\n\ndef p4_1_no_public_properties_in_domain(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P4.1: Keine public Properties in Domain.\"\"\"\n if \"\/Domain\/\" not in file_path:\n return None\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return None\n\n # public readonly ist erlaubt\n public_props = re.findall(\n r\"public\\s+(?!readonly|function|const|static\\s+function).*\\$\\w+\",\n content\n )\n\n if public_props:\n return block(\"P4.1\", \"Public property in Domain. Use getter methods or public readonly.\")\n\n return None\n\n\n# =============================================================================\n# PRÜFUNG 6: SOLID + DIP\n# =============================================================================\n\ndef p6_1_application_no_infrastructure_import(file_path: str, content: str) -> Optional[dict]:\n \"\"\"P6.1: UseCases\/Application darf Infrastructure nicht importieren.\"\"\"\n if \"\/UseCases\/\" not in file_pat... [TRUNCATED-8b8fe0235d9fc756]",
"structuredPatch": [
{
"oldStart": 1,
"oldLines": 426,
"newStart": 1,
"newLines": 39,
"lines": [
" #!\/usr\/bin\/env python3",
" \"\"\"",
"-Pre-Hook Regeln (BLOCK) - Beliebig erweiterbar ohne Claude-Neustart.",
"+Pre-Hook Regeln (BLOCK) - Modular und erweiterbar ohne Claude-Neustart.",
" ",
" Alle P-Regeln aus dem Quality Gate Plan.",
"+Importiert Regeln aus spezialisierten Modulen.",
" \"\"\"",
" ",
"-import re",
"-from pathlib import Path",
"-from typing import Optional",
"+from . import pre_rules_guard",
"+from . import pre_rules_security",
"+from . import pre_rules_validation",
" ",
"-# =============================================================================",
"-# ALLOWLIST - Pfade die von allen Prüfungen ausgenommen sind",
"-# =============================================================================",
" ",
"-GLOBAL_ALLOWLIST = [",
"- \"\/vendor\/\",",
"- \"\/tests\/\",",
"- \"\/Test\/\",",
"-]",
"-",
"-FACTORY_ALLOWLIST = [",
"- \"\/Factory\/\",",
"- \"\/Bootstrap\/\",",
"-]",
"-",
" # =============================================================================",
"-# COMMON NUMBERS - Erlaubte Magic Numbers (für W5.2, hier zur Referenz)",
"-# =============================================================================",
"-",
"-COMMON_NUMBERS = {",
"- '0', '1', '2',",
"- '10', '100', '1000',",
"- '60', '24', '365',",
"- '30', '31', '28', '29',",
"- '12', '52', '7',",
"- '200', '201', '204',",
"- '301', '302', '304',",
"- '400', '401', '403', '404', '500',",
"-}",
"-",
"-# =============================================================================",
" # HELPER FUNCTIONS",
" # =============================================================================",
" ",
"-def is_in_allowlist(file_path: str, allowlist: list) -> bool:",
"- \"\"\"Prüft ob Pfad in Allowlist ist.\"\"\"",
"- return any(allowed in file_path for allowed in allowlist)",
"-",
"-",
"-def block(rule_id: str, message: str) -> dict:",
"- \"\"\"Erzeugt Block-Response.\"\"\"",
"- return {",
"- \"allowed\": False,",
"- \"message\": f\"QUALITY VIOLATION [{rule_id}]: {message}\"",
"- }",
"-",
"-",
" def allow() -> dict:",
" \"\"\"Erzeugt Allow-Response.\"\"\"",
" return {\"allowed\": True}",
" ",
" ",
" # =============================================================================",
"-# PRÜFUNG 1: SRP + KISS",
"-# =============================================================================",
"-",
"-def p1_1_responsibility_header(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P1.1: @responsibility Header erforderlich.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- header_pattern = r\"\/\/\\s*@responsibility:\\s*(.+)\"",
"- match = re.search(header_pattern, content)",
"-",
"- if not match:",
"- return block(\"P1.1\", \"Missing @responsibility header. Add: \/\/ @responsibility: <single-responsibility>\")",
"-",
"- responsibility_text = match.group(1).strip()",
"-",
"- # Prüfe auf Multi-Responsibility-Wörter",
"- multi_words = r\"\\b(und|sowie|außerdem|also|zusätzlich|and|also|additionally)\\b\"",
"- if re.search(multi_words, responsibility_text, re.IGNORECASE):",
"- return block(\"P1.1\", f\"@responsibility contains multi-responsibility indicator: '{responsibility_text}'\")",
"-",
"- return None",
"-",
"-",
"-def p1_2_garbage_names(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P1.2: Müllhalden-Namen blockieren.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- filename = Path(file_path).stem.lower()",
"- forbidden = [\"helper\", \"utils\", \"common\", \"misc\", \"base\"]",
"-",
"- for term in forbidden:",
"- if term in filename:",
"- return block(\"P1.2\", f\"Forbidden name pattern: '{term}' indicates unclear responsibility\")",
"-",
"- return None",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 2: MVC + CRUD",
"-# =============================================================================",
"-",
"-def p2_1_no_sql_in_controller(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P2.1: Keine SQL-Statements in Controller.\"\"\"",
"- if \"\/Controller\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- sql_patterns = [",
"- r\"\\bSELECT\\s+.+\\s+FROM\\b\",",
"- r\"\\bINSERT\\s+INTO\\b\",",
"- r\"\\bUPDATE\\s+\\w+\\s+SET\\b\",",
"- r\"\\bDELETE\\s+FROM\\b\",",
"- ]",
"-",
"- for pattern in sql_patterns:",
"- if re.search(pattern, content, re.IGNORECASE):",
"- return block(\"P2.1\", \"SQL statement in Controller. Move to Repository.\")",
"-",
"- return None",
"-",
"-",
"-def p2_2_no_transactions_in_controller(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P2.2: Keine Transaktionen in Controller.\"\"\"",
"- if \"\/Controller\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- transaction_patterns = [",
"- r\"\\bbeginTransaction\\s*\\(\",",
"- r\"\\bcommit\\s*\\(\",",
"- r\"\\brollBack\\s*\\(\",",
"- r\"\\brollback\\s*\\(\",",
"- ]",
"-",
"- for pattern in transaction_patterns:",
"- if re.search(pattern, content):",
"- return block(\"P2.2\", \"Transaction control in Controller. Move to UseCase or Repository.\")",
"-",
"- return None",
"-",
"-",
"-def p2_3_no_echo_in_controller(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P2.3: Kein echo\/print in Controller.\"\"\"",
"- if \"\/Controller\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- output_patterns = [",
"- r\"\\becho\\s+\",",
"- r\"\\becho\\(\",",
"- r\"\\bprint\\s+\",",
"- r\"\\bprint\\(\",",
"- ]",
"-",
"- for pattern in output_patterns:",
"- if re.search(pattern, content):",
"- return block(\"P2.3\", \"Direct output (echo\/print) in Controller. Use Response object.\")",
"-",
"- return None",
"-",
"-",
"-def p2_4_no_db_in_usecases(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P2.4: Keine DB-Artefakte in UseCases.\"\"\"",
"- if \"\/UseCases\/\" not in file_path and \"\/Application\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- db_patterns = [",
"- r\"\\bnew\\s+PDO\\b\",",
"- r\"\\bPDO::\",",
"- r\"\\bDatabaseFactory::\",",
"- r\"\\bSELECT\\s+.+\\s+FROM\\b\",",
"- r\"\\bINSERT\\s+INTO\\b\",",
"- r\"\\bUPDATE\\s+\\w+\\s+SET\\b\",",
"- r\"\\bDELETE\\s+FROM\\b\",",
"- ]",
"-",
"- for pattern in db_patterns:",
"- if re.search(pattern, content, re.IGNORECASE):",
"- return block(\"P2.4\", \"DB artifact in UseCase\/Application. Use Repository interface.\")",
"-",
"- return None",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 3: PSR + Types",
"-# =============================================================================",
"-",
"-def p3_1_strict_types(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P3.1: strict_types erforderlich.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- strict_pattern = r\"declare\\s*\\(\\s*strict_types\\s*=\\s*1\\s*\\)\\s*;\"",
"- if not re.search(strict_pattern, content):",
"- return block(\"P3.1\", \"Missing declare(strict_types=1)\")",
"-",
"- return None",
"-",
"-",
"-def p3_2_namespace_matches_path(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P3.2: Namespace muss Pfad entsprechen.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- # Namespace-Mapping",
"- path_to_namespace = {",
"- \"\/src\/Controller\/\": \"Controller\\\\\",",
"- \"\/src\/Domain\/\": \"Domain\\\\\",",
"- \"\/src\/UseCases\/\": \"UseCases\\\\\",",
"- \"\/src\/Application\/\": \"Application\\\\\",",
"- \"\/src\/Infrastructure\/\": \"Infrastructure\\\\\",",
"- \"\/src\/Framework\/\": \"Framework\\\\\",",
"- \"\/app\/\": \"App\\\\\",",
"- }",
"-",
"- namespace_match = re.search(r\"namespace\\s+([^;]+);\", content)",
"- if not namespace_match:",
"- return None # Kein Namespace = kein Check",
"-",
"- declared_namespace = namespace_match.group(1).strip()",
"-",
"- for path_prefix, ns_prefix in path_to_namespace.items():",
"- if path_prefix in file_path:",
"- if not declared_namespace.startswith(ns_prefix.rstrip(\"\\\\\")):",
"- return block(\"P3.2\", f\"Namespace '{declared_namespace}' does not match path. Expected: '{ns_prefix}...'\")",
"- break",
"-",
"- return None",
"-",
"-",
"-def p3_3_classname_matches_filename(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P3.3: Klassenname muss Dateiname entsprechen.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- class_match = re.search(r\"(?:class|interface|trait|enum)\\s+(\\w+)\", content)",
"- if not class_match:",
"- return None # Keine Klasse = kein Check",
"-",
"- class_name = class_match.group(1)",
"- expected_name = Path(file_path).stem",
"-",
"- if class_name != expected_name:",
"- return block(\"P3.3\", f\"Class '{class_name}' does not match filename '{expected_name}'\")",
"-",
"- return None",
"-",
"-",
"-def p3_4_public_method_return_type(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P3.4: Public Methods müssen Return-Type haben.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- # Finde public functions",
"- public_methods = re.findall(",
"- r\"public\\s+function\\s+(\\w+)\\s*\\([^)]*\\)\\s*(?::\\s*\\S+)?\",",
"- content",
"- )",
"-",
"- for method in public_methods:",
"- if method.startswith(\"__\"):",
"- continue # Magic methods überspringen",
"-",
"- # Prüfe ob Return-Type vorhanden",
"- pattern = rf\"public\\s+function\\s+{method}\\s*\\([^)]*\\)\\s*:\\s*\\S+\"",
"- if not re.search(pattern, content):",
"- return block(\"P3.4\", f\"Public method '{method}' missing return type\")",
"-",
"- return None",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 4: OOP",
"-# =============================================================================",
"-",
"-def p4_1_no_public_properties_in_domain(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P4.1: Keine public Properties in Domain.\"\"\"",
"- if \"\/Domain\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- # public readonly ist erlaubt",
"- public_props = re.findall(",
"- r\"public\\s+(?!readonly|function|const|static\\s+function).*\\$\\w+\",",
"- content",
"- )",
"-",
"- if public_props:",
"- return block(\"P4.1\", \"Public property in Domain. Use getter methods or public readonly.\")",
"-",
"- return None",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 6: SOLID + DIP",
"-# =============================================================================",
"-",
"-def p6_1_application_no_infrastructure_import(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P6.1: UseCases\/Application darf Infrastructure nicht importieren.\"\"\"",
"- if \"\/UseCases\/\" not in file_path and \"\/Application\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- if re.search(r\"use\\s+Infrastructure\\\\\", content):",
"- return block(\"P6.1\", \"Application layer must not import Infrastructure directly (DIP)\")",
"-",
"- return None",
"-",
"-",
"-def p6_2_domain_no_application_import(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"P6.2: Domain darf Application-Layer nicht importieren.\"\"\"",
"- if \"\/Domain\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- if re.search(r\"use\\s+(UseCases|Application)\\\\\", content):",
"- return block(\"P6.2\", \"Domain must not import Application layer (layer violation)\")",
"-",
"- return None",
"-",
"-",
"-# =============================================================================",
"-# LEGACY H-REGELN (aus architecture_guard.py)",
"-# =============================================================================",
"-",
"-def h2_domain_no_infrastructure(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"H2: Domain ohne Infrastructure.\"\"\"",
"- if \"\/Domain\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- if re.search(r\"use\\s+Infrastructure\\\\\", content):",
"- return block(\"H2\", \"Domain must not use Infrastructure\")",
"-",
"- return None",
"-",
"-",
"-def h3_database_factory_only_in_factory(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"H3: DatabaseFactory nur in Factory.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + FACTORY_ALLOWLIST):",
"- return None",
"-",
"- if re.search(r\"DatabaseFactory::\", content):",
"- return block(\"H3\", \"DatabaseFactory only allowed in Factory classes\")",
"-",
"- return None",
"-",
"-",
"-def h4_no_new_repository_in_controller(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"H4: Kein new Repository in Controller.\"\"\"",
"- if \"\/Controller\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- if re.search(r\"new\\s+\\w+Repository\\s*\\(\", content):",
"- return block(\"H4\", \"new Repository in Controller not allowed. Use DI.\")",
"-",
"- return None",
"-",
"-",
"-def h5_no_new_infrastructure_in_controller(file_path: str, content: str) -> Optional[dict]:",
"- \"\"\"H5: Kein new Infrastructure in Controller.\"\"\"",
"- if \"\/Controller\/\" not in file_path:",
"- return None",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return None",
"-",
"- if re.search(r\"new\\s+Infrastructure\\\\\", content):",
"- return block(\"H5\", \"new Infrastructure in Controller not allowed. Use DI.\")",
"-",
"- return None",
"-",
"-",
"-# =============================================================================",
" # MAIN CHECK FUNCTION",
" # =============================================================================",
" ",
" # Alle aktiven Regeln in Prüfungsreihenfolge",
" RULES = [",
"- # Prüfung 1: SRP + KISS",
"- p1_1_responsibility_header,",
"- p1_2_garbage_names,",
"+ # Prüfung 1: SRP + KISS + Prüfung 4: OOP",
"+ *pre_rules_guard.RULES,",
" ",
"- # Prüfung 2: MVC + CRUD",
"- p2_1_no_sql_in_controller,",
"- p2_2_no_transactions_in_controller,",
"- p2_3_no_echo_in_controller,",
"- p2_4_no_db_in_usecases,",
"+ # Prüfung 2: MVC + CRUD + Prüfung 6: SOLID + DIP + H-Regeln",
"+ *pre_rules_security.RULES,",
" ",
" # Prüfung 3: PSR + Types",
"- p3_1_strict_types,",
"- p3_2_namespace_matches_path,",
"- p3_3_classname_matches_filename,",
"- p3_4_public_method_return_type,",
"-",
"- # Prüfung 4: OOP",
"- p4_1_no_public_properties_in_domain,",
"-",
"- # Prüfung 6: SOLID + DIP",
"- p6_1_application_no_infrastructure_import,",
"- p6_2_domain_no_application_import,",
"-",
"- # Legacy H-Regeln",
"- h2_domain_no_infrastructure,",
"- h3_database_factory_only_in_factory,",
"- h4_no_new_repository_in_controller,",
"- h5_no_new_infrastructure_in_controller,",
"+ *pre_rules_validation.RULES,",
" ]",
" ",
" "
]
}
],
"userModified": false,
"replaceAll": false
}
}