{
"tool_response": {
"type": "text",
"file": {
"filePath": "\/var\/www\/mcp-servers\/mcp_contracts\/validators\/contract_validator.py",
"content": "\"\"\"Contract Validator - YAML-Validierung und Scope-Prüfung\"\"\"\nimport os\nimport re\nimport sys\nimport glob as globlib\nfrom typing import Optional, Dict, Any, List\n\nimport yaml\n\nsys.path.insert(0, \"\/var\/www\/mcp-servers\/mcp_contracts\")\nfrom domain.contracts import ContractValidationResult\n\n\nclass ContractValidator:\n \"\"\"Validiert Contracts gegen ihren definierten Scope\"\"\"\n\n BASE_PATH = \"\/var\/www\/dev.campus.systemische-tools.de\"\n\n def __init__(self, contract_data: Dict[str, Any]):\n self.contract_data = contract_data\n self.contract_name = contract_data.get(\"contract\", {}).get(\"name\", \"unknown\")\n\n def validate(self, target_path: Optional[str] = None) -> ContractValidationResult:\n \"\"\"Führt vollständige Validierung durch\"\"\"\n result = ContractValidationResult(\n contract=self.contract_name,\n outcome=\"passed\",\n critical=0,\n major=0,\n minor=0,\n findings=[],\n )\n\n # Scope ermitteln\n check_paths = self._get_check_paths(target_path)\n\n if not check_paths:\n result.findings.append({\n \"type\": \"info\",\n \"message\": \"No paths to validate\",\n })\n return result\n\n # Dateien validieren\n for check_path in check_paths:\n if not os.path.exists(check_path):\n result.critical += 1\n result.findings.append({\n \"type\": \"critical\",\n \"factor\": \"path_existence\",\n \"message\": f\"Path does not exist: {check_path}\",\n })\n continue\n\n if os.path.isdir(check_path):\n self._validate_directory(check_path, result)\n else:\n self._validate_file(check_path, result)\n\n # Outcome bestimmen\n result.outcome = self._determine_outcome(result)\n\n return result\n\n def _get_check_paths(self, target_path: Optional[str] = None) -> List[str]:\n \"\"\"Ermittelt zu prüfende Pfade aus Scope (unterstützt alle Formate)\"\"\"\n if target_path:\n return [target_path]\n\n check_paths = []\n contract = self.contract_data.get(\"contract\", {})\n scope = contract.get(\"scope\", {})\n\n # Neues Standard-Format: scope.paths\n paths_list = scope.get(\"paths\", [])\n\n # Legacy-Formate als Fallback\n if not paths_list:\n paths_list = scope.get(\"includes\", []) # Legacy: scope.includes\n if not paths_list:\n paths_list = scope.get(\"applies_to_paths\", []) # Legacy\n if not paths_list:\n # Legacy: applicability.scope\n applicability = self.contract_data.get(\"applicability\", {})\n paths_list = applicability.get(\"scope\", [])\n if isinstance(paths_list, str):\n paths_list = [paths_list]\n\n excludes = scope.get(\"excludes\", [])\n\n for pattern in paths_list:\n # Glob-Pattern expandieren\n full_pattern = os.path.join(self.BASE_PATH, pattern.lstrip(\"\/\"))\n matched = globlib.glob(full_pattern, recursive=True)\n\n if matched:\n for path in matched:\n # Excludes prüfen\n excluded = False\n for excl in excludes:\n excl_pattern = os.path.join(self.BASE_PATH, excl.lstrip(\"\/\"))\n if globlib.fnmatch.fnmatch(path, excl_pattern):\n excluded = True\n break\n if not excluded:\n check_paths.append(path)\n else:\n # Falls kein Match, versuche Basisverzeichnis\n pattern_path = pattern.replace(\"**\", \"\").replace(\"*\", \"\").rstrip(\"\/\")\n full_path = os.path.join(self.BASE_PATH, pattern_path.lstrip(\"\/\"))\n if os.path.exists(full_path):\n check_paths.append(full_path)\n\n return list(set(check_paths)) # Duplikate entfernen\n\n def _validate_directory(self, dir_path: str, result: ContractValidationResult) -> None:\n \"\"\"Validiert alle Dateien in einem Verzeichnis\"\"\"\n for root, dirs, files in os.walk(dir_path):\n for f in files:\n if f.endswith((\".php\", \".js\", \".css\", \".py\")):\n file_path = os.path.join(root, f)\n self._validate_file(file_path, result)\n\n def _validate_file(self, file_path: str, result: ContractValidationResult) -> None:\n \"\"\"Validiert eine einzelne Datei gegen den Contract\"\"\"\n try:\n with open(file_path, \"r\", encoding=\"utf-8\") as f:\n content = f.read()\n\n # Neue Rules-basierte Validierung\n rules = self.contract_data.get(\"contract\", {}).get(\"rules\", [])\n for rule in rules:\n self._check_rule(file_path, content, rule, result)\n\n # Legacy: Forbidden elements prüfen\n self._check_forbidden_elements(file_path, content, result)\n\n # Legacy: Required elements prüfen\n self._check_required_elements(file_path, content, result)\n\n except Exception as e:\n result.minor += 1\n result.findings.append({\n \"type\": \"minor\",\n \"factor\": \"file_read_error\",\n \"file\": file_path,\n \"message\": str(e)[:100],\n })\n\n def _check_rule(\n self, file_path: str, content: str, rule: Dict[str, Any], result: ContractValidationResult\n ) -> None:\n \"\"\"Prüft eine einzelne Rule gegen eine Datei\"\"\"\n check_type = rule.get(\"check_type\", \"\")\n in_files = rule.get(\"in_files\", \"**\/*\")\n rule_id = rule.get(\"id\", \"unknown\")\n severity = rule.get(\"severity\", \"major\")\n description = rule.get(\"description\", \"\")\n\n # Prüfen ob Datei zum Rule-Pattern passt\n if not self._file_matches_pattern(file_path, in_files):\n return\n\n if check_type == \"line_count\":\n self._check_line_count(file_path, content, rule, result)\n elif check_type == \"forbidden_pattern\":\n self._check_forbidden_pattern(file_path, content, rule, result)\n elif check_type == \"required_pattern\":\n self._check_required_pattern(file_path, content, rule, result)\n elif check_type == \"dependency_check\":\n self._check_dependency(file_path, content, rule, result)\n\n def _file_matches_pattern(self, file_path: str, pattern: str) -> bool:\n \"\"\"Prüft ob Datei zum Glob-Pattern passt\"\"\"\n # Relative path from BASE_PATH\n rel_path = file_path.replace(self.BASE_PATH, \"\").lstrip(\"\/\")\n\n # Einfache Pattern-Prüfung\n if \"**\" in pattern:\n # z.B. Controller\/**\/*.php\n parts = pattern.split(\"**\")\n if len(parts) == 2:\n prefix = parts[0].rstrip(\"\/\")\n suffix = parts[1].lstrip(\"\/\")\n if prefix and not rel_path.startswith(prefix.lstrip(\"\/\")):\n return False\n if suffix and not globlib.fnmatch.fnmatch(rel_path, f\"*{suffix}\"):\n return False\n return True\n elif \"*\" in pattern:\n return globlib.fnmatch.fnmatch(rel_path, pattern)\n else:\n return pattern in rel_path\n\n return True\n\n def _check_line_count(\n self, file_path: str, content: str, rule: Dict[str, Any], result: ContractValidationResult\n ) -> None:\n \"\"\"Prüft Zeilenanzahl einer Datei\"\"\"\n max_lines = rule.get(\"max_lines\", 500)\n severity = rule.get(\"severity\", \"major\")\n rule_id = rule.get(\"id\", \"line_count\")\n\n line_count = len(content.splitlines())\n if line_count > max_lines:\n self._add_violation(result, severity, {\n \"rule_id\": rule_id,\n \"factor\": \"line_count\",\n \"file\": file_path,\n \"message\": f\"File has {line_count} lines (max: {max_lines})\",\n \"actual\": line_count,\n \"limit\": max_lines,\n })\n\n def _check_forbidden_pattern(\n self, file_path: str, content: str, rule: Dict[str, Any], result: ContractValidationResult\n ) -> None:\n \"\"\"Prüft auf verbotene Patterns\"\"\"\n patterns = rule.get(\"patterns\", [])\n severity = rule.get(\"severity\", \"major\")\n rule_id = rule.get(\"id\", \"forbidden_pattern\")\n description = rule.get(\"description\", \"\")\n\n for pattern in patterns:\n # Versuche als Regex, sonst als String\n try:\n matches = re.findall(pattern, content)\n except re.error:\n matches = [pattern] if pattern in content else []\n\n if matches:\n # Zeile finden\n line_no = None\n for i, line in enumerate(content.splitlines(), 1):\n if pattern in line or (re.search(pattern, line) if self._is_regex(pattern) else False):\n line_no = i\n break\n\n self._add_violation(result, severity, {\n \"rule_id\": rule_id,\n \"factor\": \"forbidden_pattern\",\n \"file\": file_path,\n \"message\": f\"Forbidden pattern found: '{pattern}'\",\n \"description\": description,\n \"line\": line_no,\n \"occurrences\": len(matches),\n })\n\n def _check_required_pattern(\n self, file_path: str, content: str, rule: Dict[str, Any], result: ContractValidationResult\n ) -> None:\n \"\"\"Prüft auf erforderliche Patterns\"\"\"\n patterns = rule.get(\"patterns\", [])\n severity = rule.get(\"severity\", \"major\")\n rule_id = rule.get(\"id\", \"required_pattern\")\n description = rule.get(\"description\", \"\")\n\n for pattern in patterns:\n try:\n found = bool(... [TRUNCATED-c25223228dfd8fdf]",
"numLines": 410,
"startLine": 1,
"totalLines": 410
}
}
}