Protokoll #28690

ID28690
Zeitstempel2025-12-28 13:59:10.442948
Clientroot
IP145.224.72.140
Modellclaude-sonnet-4-20250514
Statuscompleted
Tokens3,597 (Input: 1,796, Output: 1,801)
Dauer1145890 ms
Request-Zeit2025-12-28 13:59:10.442948
Response-Zeit2025-12-28 14:18:16.333312

Request

{
    "event": "PreToolUse",
    "tool_name": "Write",
    "tool_input": {
        "file_path": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/quality\/phpmetrics_check.py",
        "content": "#!\/usr\/bin\/env python3\n\"\"\"\nPHPMetrics Integration für Cohesion\/Coupling-Analyse.\n\nLäuft PHPMetrics auf der Codebase und erzeugt Warnungen\nfür Klassen mit schlechten Metriken.\n\nPrinzip: Cohesion over Coupling (#13)\n\"\"\"\n\nimport json\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\n# =============================================================================\n# KONFIGURATION\n# =============================================================================\n\nPHPMETRICS_BIN = \"\/opt\/php-tools\/vendor\/bin\/phpmetrics\"\nSRC_PATH = \"\/var\/www\/dev.campus.systemische-tools.de\/src\"\nCACHE_FILE = \"\/tmp\/phpmetrics_cache.json\"\nCACHE_MAX_AGE_SECONDS = 3600  # 1 Stunde\n\n# Schwellwerte für Warnungen\nTHRESHOLDS = {\n    \"lcom\": 4,              # Lack of Cohesion - höher = schlechter\n    \"afferent_coupling\": 10, # Wie viele Klassen nutzen diese\n    \"efferent_coupling\": 15, # Wie viele Klassen nutzt diese\n    \"instability_low\": 0.2,  # Zu stabil (schwer zu ändern)\n    \"instability_high\": 0.8, # Zu instabil (zu viele Abhängigkeiten)\n    \"ccn_method_max\": 15,    # Maximale Komplexität pro Methode\n    \"wmc\": 50,               # Weighted Methods per Class\n}\n\n# Pfade die ignoriert werden\nSKIP_PATHS = [\n    \"\/vendor\/\",\n    \"\/tests\/\",\n    \"\/Test\/\",\n]\n\n\n# =============================================================================\n# HELPER\n# =============================================================================\n\ndef is_cache_valid() -> bool:\n    \"\"\"Prüft ob der Cache noch gültig ist.\"\"\"\n    cache = Path(CACHE_FILE)\n    if not cache.exists():\n        return False\n    import time\n    age = time.time() - cache.stat().st_mtime\n    return age < CACHE_MAX_AGE_SECONDS\n\n\ndef load_metrics() -> Dict:\n    \"\"\"Lädt Metriken aus Cache oder führt PHPMetrics aus.\"\"\"\n    if is_cache_valid():\n        with open(CACHE_FILE) as f:\n            return json.load(f)\n\n    # PHPMetrics ausführen\n    result = subprocess.run(\n        [PHPMETRICS_BIN, \"--report-json=\" + CACHE_FILE, SRC_PATH],\n        capture_output=True,\n        text=True,\n        timeout=300\n    )\n\n    if result.returncode != 0 and not Path(CACHE_FILE).exists():\n        return {}\n\n    with open(CACHE_FILE) as f:\n        return json.load(f)\n\n\ndef should_skip(class_name: str) -> bool:\n    \"\"\"Prüft ob die Klasse übersprungen werden soll.\"\"\"\n    return any(skip in class_name for skip in SKIP_PATHS)\n\n\n# =============================================================================\n# ANALYSE\n# =============================================================================\n\ndef analyze_class(name: str, data: Dict) -> List[str]:\n    \"\"\"Analysiert eine Klasse und gibt Warnungen zurück.\"\"\"\n    warnings = []\n\n    if not isinstance(data, dict):\n        return warnings\n\n    # LCOM - Lack of Cohesion of Methods\n    lcom = data.get(\"lcom\", 0)\n    if lcom > THRESHOLDS[\"lcom\"]:\n        warnings.append(\n            f\"W13.1: {name} has low cohesion (LCOM={lcom}). \"\n            \"Consider splitting into focused classes.\"\n        )\n\n    # Afferent Coupling (incoming dependencies)\n    afferent = data.get(\"afferentCoupling\", 0)\n    if afferent > THRESHOLDS[\"afferent_coupling\"]:\n        warnings.append(\n            f\"W13.2: {name} has high afferent coupling ({afferent} dependents). \"\n            \"Changes here affect many classes - consider interface.\"\n        )\n\n    # Efferent Coupling (outgoing dependencies)\n    efferent = data.get(\"efferentCoupling\", 0)\n    if efferent > THRESHOLDS[\"efferent_coupling\"]:\n        warnings.append(\n            f\"W13.3: {name} has high efferent coupling ({efferent} dependencies). \"\n            \"Consider dependency injection or facade pattern.\"\n        )\n\n    # Instability\n    instability = data.get(\"instability\", 0.5)\n    if instability < THRESHOLDS[\"instability_low\"] and afferent > 5:\n        warnings.append(\n            f\"W13.4: {name} is very stable (I={instability:.2f}) with many dependents. \"\n            \"Changes are risky - ensure good test coverage.\"\n        )\n\n    # Cyclomatic Complexity per Method\n    ccn_max = data.get(\"ccnMethodMax\", 0)\n    if ccn_max > THRESHOLDS[\"ccn_method_max\"]:\n        warnings.append(\n            f\"W13.5: {name} has high method complexity (CCN={ccn_max}). \"\n            \"Consider extracting methods or simplifying logic.\"\n        )\n\n    # Weighted Methods per Class\n    wmc = data.get(\"wmc\", 0)\n    if wmc > THRESHOLDS[\"wmc\"]:\n        warnings.append(\n            f\"W13.6: {name} has high overall complexity (WMC={wmc}). \"\n            \"Class is doing too much - consider splitting.\"\n        )\n\n    return warnings\n\n\ndef run_analysis() -> Tuple[List[str], Dict]:\n    \"\"\"Führt die vollständige Analyse durch.\"\"\"\n    metrics = load_metrics()\n    all_warnings = []\n    stats = {\n        \"classes_analyzed\": 0,\n        \"classes_with_warnings\": 0,\n        \"total_warnings\": 0,\n    }\n\n    for name, data in metrics.items():\n        if should_skip(name):\n            continue\n\n        if not isinstance(data, dict):\n            continue\n\n        stats[\"classes_analyzed\"] += 1\n        warnings = analyze_class(name, data)\n\n        if warnings:\n            stats[\"classes_with_warnings\"] += 1\n            stats[\"total_warnings\"] += len(warnings)\n            all_warnings.extend(warnings)\n\n    return all_warnings, stats\n\n\n# =============================================================================\n# MAIN\n# =============================================================================\n\ndef main():\n    \"\"\"Hauptfunktion für CLI-Aufruf.\"\"\"\n    import argparse\n\n    parser = argparse.ArgumentParser(description=\"PHPMetrics Cohesion Check\")\n    parser.add_argument(\"--refresh\", action=\"store_true\", help=\"Cache neu erstellen\")\n    parser.add_argument(\"--json\", action=\"store_true\", help=\"JSON-Ausgabe\")\n    parser.add_argument(\"--class\", dest=\"class_name\", help=\"Nur eine Klasse prüfen\")\n    args = parser.parse_args()\n\n    if args.refresh and Path(CACHE_FILE).exists():\n        Path(CACHE_FILE).unlink()\n\n    warnings, stats = run_analysis()\n\n    if args.json:\n        print(json.dumps({\n            \"warnings\": warnings,\n            \"stats\": stats\n        }, indent=2))\n    else:\n        print(f\"PHPMetrics Analysis: {stats['classes_analyzed']} classes\")\n        print(f\"Classes with warnings: {stats['classes_with_warnings']}\")\n        print(f\"Total warnings: {stats['total_warnings']}\")\n        print()\n\n        if warnings:\n            print(\"Warnings:\")\n            for w in warnings[:20]:  # Limit output\n                print(f\"  - {w}\")\n\n            if len(warnings) > 20:\n                print(f\"  ... and {len(warnings) - 20} more\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
    }
}

Response

{
    "tool_response": {
        "type": "create",
        "filePath": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/quality\/phpmetrics_check.py",
        "content": "#!\/usr\/bin\/env python3\n\"\"\"\nPHPMetrics Integration für Cohesion\/Coupling-Analyse.\n\nLäuft PHPMetrics auf der Codebase und erzeugt Warnungen\nfür Klassen mit schlechten Metriken.\n\nPrinzip: Cohesion over Coupling (#13)\n\"\"\"\n\nimport json\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\n# =============================================================================\n# KONFIGURATION\n# =============================================================================\n\nPHPMETRICS_BIN = \"\/opt\/php-tools\/vendor\/bin\/phpmetrics\"\nSRC_PATH = \"\/var\/www\/dev.campus.systemische-tools.de\/src\"\nCACHE_FILE = \"\/tmp\/phpmetrics_cache.json\"\nCACHE_MAX_AGE_SECONDS = 3600  # 1 Stunde\n\n# Schwellwerte für Warnungen\nTHRESHOLDS = {\n    \"lcom\": 4,              # Lack of Cohesion - höher = schlechter\n    \"afferent_coupling\": 10, # Wie viele Klassen nutzen diese\n    \"efferent_coupling\": 15, # Wie viele Klassen nutzt diese\n    \"instability_low\": 0.2,  # Zu stabil (schwer zu ändern)\n    \"instability_high\": 0.8, # Zu instabil (zu viele Abhängigkeiten)\n    \"ccn_method_max\": 15,    # Maximale Komplexität pro Methode\n    \"wmc\": 50,               # Weighted Methods per Class\n}\n\n# Pfade die ignoriert werden\nSKIP_PATHS = [\n    \"\/vendor\/\",\n    \"\/tests\/\",\n    \"\/Test\/\",\n]\n\n\n# =============================================================================\n# HELPER\n# =============================================================================\n\ndef is_cache_valid() -> bool:\n    \"\"\"Prüft ob der Cache noch gültig ist.\"\"\"\n    cache = Path(CACHE_FILE)\n    if not cache.exists():\n        return False\n    import time\n    age = time.time() - cache.stat().st_mtime\n    return age < CACHE_MAX_AGE_SECONDS\n\n\ndef load_metrics() -> Dict:\n    \"\"\"Lädt Metriken aus Cache oder führt PHPMetrics aus.\"\"\"\n    if is_cache_valid():\n        with open(CACHE_FILE) as f:\n            return json.load(f)\n\n    # PHPMetrics ausführen\n    result = subprocess.run(\n        [PHPMETRICS_BIN, \"--report-json=\" + CACHE_FILE, SRC_PATH],\n        capture_output=True,\n        text=True,\n        timeout=300\n    )\n\n    if result.returncode != 0 and not Path(CACHE_FILE).exists():\n        return {}\n\n    with open(CACHE_FILE) as f:\n        return json.load(f)\n\n\ndef should_skip(class_name: str) -> bool:\n    \"\"\"Prüft ob die Klasse übersprungen werden soll.\"\"\"\n    return any(skip in class_name for skip in SKIP_PATHS)\n\n\n# =============================================================================\n# ANALYSE\n# =============================================================================\n\ndef analyze_class(name: str, data: Dict) -> List[str]:\n    \"\"\"Analysiert eine Klasse und gibt Warnungen zurück.\"\"\"\n    warnings = []\n\n    if not isinstance(data, dict):\n        return warnings\n\n    # LCOM - Lack of Cohesion of Methods\n    lcom = data.get(\"lcom\", 0)\n    if lcom > THRESHOLDS[\"lcom\"]:\n        warnings.append(\n            f\"W13.1: {name} has low cohesion (LCOM={lcom}). \"\n            \"Consider splitting into focused classes.\"\n        )\n\n    # Afferent Coupling (incoming dependencies)\n    afferent = data.get(\"afferentCoupling\", 0)\n    if afferent > THRESHOLDS[\"afferent_coupling\"]:\n        warnings.append(\n            f\"W13.2: {name} has high afferent coupling ({afferent} dependents). \"\n            \"Changes here affect many classes - consider interface.\"\n        )\n\n    # Efferent Coupling (outgoing dependencies)\n    efferent = data.get(\"efferentCoupling\", 0)\n    if efferent > THRESHOLDS[\"efferent_coupling\"]:\n        warnings.append(\n            f\"W13.3: {name} has high efferent coupling ({efferent} dependencies). \"\n            \"Consider dependency injection or facade pattern.\"\n        )\n\n    # Instability\n    instability = data.get(\"instability\", 0.5)\n    if instability < THRESHOLDS[\"instability_low\"] and afferent > 5:\n        warnings.append(\n            f\"W13.4: {name} is very stable (I={instability:.2f}) with many dependents. \"\n            \"Changes are risky - ensure good test coverage.\"\n        )\n\n    # Cyclomatic Complexity per Method\n    ccn_max = data.get(\"ccnMethodMax\", 0)\n    if ccn_max > THRESHOLDS[\"ccn_method_max\"]:\n        warnings.append(\n            f\"W13.5: {name} has high method complexity (CCN={ccn_max}). \"\n            \"Consider extracting methods or simplifying logic.\"\n        )\n\n    # Weighted Methods per Class\n    wmc = data.get(\"wmc\", 0)\n    if wmc > THRESHOLDS[\"wmc\"]:\n        warnings.append(\n            f\"W13.6: {name} has high overall complexity (WMC={wmc}). \"\n            \"Class is doing too much - consider splitting.\"\n        )\n\n    return warnings\n\n\ndef run_analysis() -> Tuple[List[str], Dict]:\n    \"\"\"Führt die vollständige Analyse durch.\"\"\"\n    metrics = load_metrics()\n    all_warnings = []\n    stats = {\n        \"classes_analyzed\": 0,\n        \"classes_with_warnings\": 0,\n        \"total_warnings\": 0,\n    }\n\n    for name, data in metrics.items():\n        if should_skip(name):\n            continue\n\n        if not isinstance(data, dict):\n            continue\n\n        stats[\"classes_analyzed\"] += 1\n        warnings = analyze_class(name, data)\n\n        if warnings:\n            stats[\"classes_with_warnings\"] += 1\n            stats[\"total_warnings\"] += len(warnings)\n            all_warnings.extend(warnings)\n\n    return all_warnings, stats\n\n\n# =============================================================================\n# MAIN\n# =============================================================================\n\ndef main():\n    \"\"\"Hauptfunktion für CLI-Aufruf.\"\"\"\n    import argparse\n\n    parser = argparse.ArgumentParser(description=\"PHPMetrics Cohesion Check\")\n    parser.add_argument(\"--refresh\", action=\"store_true\", help=\"Cache neu erstellen\")\n    parser.add_argument(\"--json\", action=\"store_true\", help=\"JSON-Ausgabe\")\n    parser.add_argument(\"--class\", dest=\"class_name\", help=\"Nur eine Klasse prüfen\")\n    args = parser.parse_args()\n\n    if args.refresh and Path(CACHE_FILE).exists():\n        Path(CACHE_FILE).unlink()\n\n    warnings, stats = run_analysis()\n\n    if args.json:\n        print(json.dumps({\n            \"warnings\": warnings,\n            \"stats\": stats\n        }, indent=2))\n    else:\n        print(f\"PHPMetrics Analysis: {stats['classes_analyzed']} classes\")\n        print(f\"Classes with warnings: {stats['classes_with_warnings']}\")\n        print(f\"Total warnings: {stats['total_warnings']}\")\n        print()\n\n        if warnings:\n            print(\"Warnings:\")\n            for w in warnings[:20]:  # Limit output\n                print(f\"  - {w}\")\n\n            if len(warnings) > 20:\n                print(f\"  ... and {len(warnings) - 20} more\")\n\n\nif __name__ == \"__main__\":\n    main()\n",
        "structuredPatch": [],
        "originalFile": null
    }
}
← Vorheriger Zur Liste Nächster →