{
"tool_response": {
"type": "text",
"file": {
"filePath": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/quality\/post_rules.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nPost-Hook Regeln (WARN) - Beliebig erweiterbar ohne Claude-Neustart.\n\nAlle W-Regeln aus dem Quality Gate Plan.\n\"\"\"\n\nimport re\nfrom pathlib import Path\nfrom collections import Counter\nfrom typing import List\n\n# =============================================================================\n# ALLOWLIST\n# =============================================================================\n\nGLOBAL_ALLOWLIST = [\n \"\/vendor\/\",\n \"\/tests\/\",\n \"\/Test\/\",\n]\n\nDTO_ALLOWLIST = [\n \"\/Infrastructure\/DTO\/\",\n \"\/DTO\/\",\n]\n\nMIGRATION_ALLOWLIST = [\n \"\/migrations\/\",\n \"\/Migration\/\",\n]\n\n# =============================================================================\n# COMMON NUMBERS - Erlaubte Magic Numbers\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 count_non_empty_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\n# =============================================================================\n# PRÜFUNG 1: SRP + KISS (Warnungen)\n# =============================================================================\n\ndef w1_1_class_size(file_path: str, content: str) -> List[str]:\n \"\"\"W1.1: Klassengröße.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n loc = count_non_empty_lines(content)\n warnings = []\n\n if loc > 300:\n warnings.append(f\"W1.1: Class has {loc} lines (max 300). Consider splitting.\")\n elif loc > 200:\n warnings.append(f\"W1.1: Class has {loc} lines (approaching limit of 300).\")\n\n return warnings\n\n\ndef w1_2_public_method_count(file_path: str, content: str) -> List[str]:\n \"\"\"W1.2: Anzahl public methods.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n public_methods = re.findall(r\"public\\s+function\\s+\\w+\", content)\n count = len(public_methods)\n\n if count > 10:\n return [f\"W1.2: Class has {count} public methods (max 10). Consider splitting.\"]\n\n return []\n\n\ndef w1_3_constructor_params(file_path: str, content: str) -> List[str]:\n \"\"\"W1.3: Constructor-Parameter.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n # Finde Constructor\n constructor_match = re.search(r\"function\\s+__construct\\s*\\(([^)]*)\\)\", content, re.DOTALL)\n if not constructor_match:\n return []\n\n params = constructor_match.group(1)\n # Zähle Parameter (durch Komma getrennt, aber nicht in Klammern)\n param_count = len([p for p in params.split(',') if p.strip()])\n\n if param_count > 5:\n return [f\"W1.3: Constructor has {param_count} parameters (max 5). Consider refactoring.\"]\n\n return []\n\n\ndef w1_4_dependency_count(file_path: str, content: str) -> List[str]:\n \"\"\"W1.4: Anzahl use-Statements.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n use_statements = re.findall(r\"^use\\s+\", content, re.MULTILINE)\n count = len(use_statements)\n\n if count > 10:\n return [f\"W1.4: Class has {count} dependencies (max 10). Consider reducing coupling.\"]\n\n return []\n\n\ndef w1_5_suspicious_names(file_path: str, content: str) -> List[str]:\n \"\"\"W1.5: Verdächtige Namen (Hint).\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n filename = Path(file_path).stem\n hints = []\n\n if \"Manager\" in filename:\n hints.append(f\"W1.5: Hint - '{filename}' contains 'Manager'. Verify single responsibility.\")\n\n # \"And\" als separates Wort im CamelCase\n if re.search(r\"[a-z]And[A-Z]\", filename):\n hints.append(f\"W1.5: Hint - '{filename}' contains 'And'. May indicate multiple responsibilities.\")\n\n return hints\n\n\n# =============================================================================\n# PRÜFUNG 2: MVC + CRUD (Warnungen)\n# =============================================================================\n\ndef w2_1_business_keywords_in_controller(file_path: str, content: str) -> List[str]:\n \"\"\"W2.1: Business-Keywords in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return []\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n keywords = [\n (r\"\\bcalculate\\w*\\s*\\(\", \"calculate\"),\n (r\"\\bcompute\\w*\\s*\\(\", \"compute\"),\n (r\"\\bvalidate\\w*\\s*\\(\", \"validate\"),\n (r\"\\bprocess\\w*\\s*\\(\", \"process\"),\n (r\"\\btransform\\w*\\s*\\(\", \"transform\"),\n (r\"\\bconvert\\w*\\s*\\(\", \"convert\"),\n ]\n\n found = []\n for pattern, name in keywords:\n if re.search(pattern, content, re.IGNORECASE):\n found.append(name)\n\n if found:\n return [f\"W2.1: Business keywords in Controller: {', '.join(found)}. Consider moving to Domain\/UseCase.\"]\n\n return []\n\n\ndef w2_2_private_methods_in_controller(file_path: str, content: str) -> List[str]:\n \"\"\"W2.2: Viele private Methoden in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return []\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n private_methods = re.findall(r\"private\\s+function\\s+\\w+\", content)\n count = len(private_methods)\n\n if count > 5:\n return [f\"W2.2: Controller has {count} private methods (max 5). Extract to Service.\"]\n\n return []\n\n\n# =============================================================================\n# PRÜFUNG 3: PSR + Types (Warnungen)\n# =============================================================================\n\ndef w3_1_potential_untyped_params(file_path: str, content: str) -> List[str]:\n \"\"\"W3.1: Potentiell untypisierte Parameter.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n # Heuristik: Suche nach $param direkt nach ( oder ,\n potential = re.findall(r\"function\\s+\\w+\\s*\\([^)]*[(,]\\s*\\$\", content)\n\n if potential:\n return [\"W3.1: Potential untyped parameters detected. Run PHPStan for verification.\"]\n\n return []\n\n\ndef w3_3_mixed_type(file_path: str, content: str) -> List[str]:\n \"\"\"W3.3: mixed Type verwendet.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n if re.search(r\":\\s*mixed\\b\", content):\n return [\"W3.3: 'mixed' type used. Consider more specific type.\"]\n\n return []\n\n\n# =============================================================================\n# PRÜFUNG 4: OOP (Warnungen)\n# =============================================================================\n\ndef w4_1_anemic_model(file_path: str, content: str) -> List[str]:\n \"\"\"W4.1: Anämisches Model (hohe Accessor-Ratio).\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + DTO_ALLOWLIST):\n return []\n\n getters = len(re.findall(r\"public\\s+function\\s+get[A-Z]\\w*\\s*\\(\", content))\n setters = len(re.findall(r\"public\\s+function\\s+set[A-Z]\\w*\\s*\\(\", content))\n all_public = len(re.findall(r\"public\\s+function\\s+\\w+\", content))\n\n if all_public > 4:\n accessor_ratio = (getters + setters) \/ all_public\n if accessor_ratio > 0.7:\n return [f\"W4.1: Potential anemic model: {int(accessor_ratio*100)}% accessors. Consider adding behavior.\"]\n\n return []\n\n\ndef w4_2_class_without_behavior(file_path: str, content: str) -> List[str]:\n \"\"\"W4.2: Klasse ohne Verhalten.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + DTO_ALLOWLIST):\n return []\n\n has_properties = bool(re.search(r\"(?:private|protected|public)\\s+.*\\$\\w+\", content))\n methods = re.findall(r\"(?:public|private|protected)\\s+function\\s+(\\w+)\", content)\n real_methods = [m for m in methods if not m.startswith((\"get\", \"set\", \"__\"))]\n\n if has_properties and len(real_methods) == 0:\n return [\"W4.2: Class has properties but no behavior methods.\"]\n\n return []\n\n\ndef w4_3_low_encapsulation(file_path: str, content: str) -> List[str]:\n \"\"\"W4.3: Niedrige Kapselung.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + DTO_ALLOWLIST):\n return []\n\n public_props = len(re.findall(r\"public\\s+(?!function|const).*\\$\", content))\n protected_props = len(re.findall(r\"protected\\s+(?!function|const).*\\$\", content))\n private_props = len(re.findall(r\"private\\s+(?!function|const).*\\$\", content))\n\n total = public_props + protected_props + private_props\n if total > 3:\n exposed_ratio = (public_props + protected_props) \/ total\n if exposed_ratio > 0.5:\n return [f\"W4.3: Low encapsulation: {int(exposed_ratio*100)}% exposed properties.\"]\n\n return []\n\n\ndef w4_4_high_static_ratio(file_path: str, content: str) -> List[str]:\n \"\"\"W4.4: Hohe Static-Ratio.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n static_methods = len(re.findall(r\"public\\s+static\\s+function\", content))\n all_methods = len(re.findall(r\"public\\s+function\", content))\n\n if all_methods > 0 and static_methods > 2:\n static_ratio = static_methods \/ all_methods\n if static_ratio > 0.5:\n return [f\"W4.4: High static method ratio: {static_methods}\/{all_methods}. Consider instance methods.\"]\n\n return []\n\n\n# =============================================================================\n# PRÜFUNG 5: DRY (Warnungen)\n# =============================================================================\n\ndef w5_1_repeated_strings(file_path: str, content: str) -> List[str]:\n \"\"\"W5.1: Wiederholte String-Literale.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + MIGRATION_ALLOWLIST):\n return []\n\n # Finde Strings > 20 Zeichen\n ... [TRUNCATED-b7b8062923829b73]",
"numLines": 468,
"startLine": 1,
"totalLines": 468
}
}
}