#!/usr/bin/env python3 """ Script Health Checker Validates Python script quality across the orchestrator library: - Syntax validation - Import/dependency checking - Type hint completeness - Error handling patterns - Docstring coverage """ import ast import sys from pathlib import Path from typing import List, Dict, Tuple class ScriptHealthChecker: """Check health of orchestrator Python scripts.""" LIB_DIR = Path('/opt/server-agents/orchestrator/lib') def __init__(self): """Initialize script health checker.""" self.issues = [] def validate_all_scripts(self) -> Dict: """ Validate all Python scripts in orchestrator lib. Returns: Dict with validation results """ if not self.LIB_DIR.exists(): return { 'status': 'error', 'message': f'Lib directory not found: {self.LIB_DIR}', 'scripts': [] } scripts = list(self.LIB_DIR.glob('*.py')) results = { 'total_scripts': len(scripts), 'valid_scripts': 0, 'scripts': [], 'overall_health': 0 } for script_path in scripts: if script_path.name.startswith('_'): continue result = self.validate_script(script_path) results['scripts'].append(result) if result['status'] == 'valid': results['valid_scripts'] += 1 # Calculate overall health if results['total_scripts'] > 0: results['overall_health'] = (results['valid_scripts'] / results['total_scripts']) * 100 return results def validate_script(self, script_path: Path) -> Dict: """ Validate a single Python script. Args: script_path: Path to Python file Returns: Dict with validation results """ result = { 'script': script_path.name, 'path': str(script_path), 'status': 'unknown', 'issues': [], 'metrics': {} } try: content = script_path.read_text(encoding='utf-8') except Exception as e: result['status'] = 'error' result['issues'].append(f"Cannot read file: {e}") return result # 1. Syntax validation try: tree = ast.parse(content) result['metrics']['lines'] = len(content.split('\n')) except SyntaxError as e: result['status'] = 'syntax_error' result['issues'].append(f"Syntax error at line {e.lineno}: {e.msg}") return result # 2. Import validation import_issues = self._check_imports(tree, script_path) result['issues'].extend(import_issues) # 3. Type hint coverage type_coverage = self._check_type_hints(tree) result['metrics']['type_hint_coverage'] = type_coverage # 4. Docstring coverage docstring_coverage = self._check_docstrings(tree) result['metrics']['docstring_coverage'] = docstring_coverage # 5. Error handling patterns error_handling = self._check_error_handling(tree) result['metrics']['error_handling_score'] = error_handling # 6. Class and function count result['metrics']['classes'] = len([n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]) result['metrics']['functions'] = len([n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)]) # Determine overall status if not result['issues']: result['status'] = 'valid' elif len(result['issues']) <= 2: result['status'] = 'warnings' else: result['status'] = 'issues' return result def _check_imports(self, tree: ast.AST, script_path: Path) -> List[str]: """Check for import issues.""" issues = [] imports = [] for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: imports.append(alias.name) elif isinstance(node, ast.ImportFrom): if node.module: imports.append(node.module) # Check for unused imports imported_names = set(imports) content = script_path.read_text() for imported in imported_names: short_name = imported.split('.')[0] # Simple heuristic: if imported name doesn't appear in code if content.count(short_name) <= 1: # Only in import statement if not short_name.startswith('_'): issues.append(f"Possible unused import: {imported}") # Check for missing imports (stdlib coverage) required_stdlib = {'json', 'time', 'pathlib', 'typing'} stdlib_used = imported_names & required_stdlib if stdlib_used != required_stdlib: missing = required_stdlib - stdlib_used for module in missing: if module in content: issues.append(f"Missing import: {module}") return issues def _check_type_hints(self, tree: ast.AST) -> float: """Calculate type hint coverage percentage.""" functions = [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)] if not functions: return 100.0 functions_with_hints = 0 for func in functions: # Check if function has return type hint if func.returns: # Check if parameters have type hints params_with_hints = 0 for arg in func.args.args: if arg.annotation: params_with_hints += 1 # Consider function well-typed if most params are annotated if params_with_hints >= len(func.args.args) * 0.5: functions_with_hints += 1 return (functions_with_hints / len(functions)) * 100 def _check_docstrings(self, tree: ast.AST) -> float: """Calculate docstring coverage percentage.""" documented = 0 total = 0 for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.ClassDef)): total += 1 if ast.get_docstring(node): documented += 1 if total == 0: return 100.0 return (documented / total) * 100 def _check_error_handling(self, tree: ast.AST) -> float: """Score error handling patterns (try/except coverage).""" try_blocks = 0 functions = 0 for node in ast.walk(tree): if isinstance(node, ast.FunctionDef): functions += 1 for child in ast.walk(node): if isinstance(child, ast.Try): try_blocks += 1 if functions == 0: return 100.0 # Score based on try/except ratio error_handling_ratio = (try_blocks / functions) * 100 return min(100, error_handling_ratio) def get_module_dependencies(self) -> Dict: """Get all external module dependencies.""" dependencies = set() for script_path in self.LIB_DIR.glob('*.py'): if script_path.name.startswith('_'): continue try: content = script_path.read_text() tree = ast.parse(content) for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: module = alias.name.split('.')[0] if not self._is_stdlib(module): dependencies.add(module) elif isinstance(node, ast.ImportFrom): if node.module: module = node.module.split('.')[0] if not self._is_stdlib(module): dependencies.add(module) except Exception: pass return { 'external_dependencies': sorted(list(dependencies)), 'stdlib_usage': True, 'total_dependencies': len(dependencies) } @staticmethod def _is_stdlib(module_name: str) -> bool: """Check if module is Python standard library.""" stdlib_modules = { 'json', 'time', 'pathlib', 'typing', 'os', 'sys', 'sqlite3', 'datetime', 'shutil', 'signal', 'ast', 're', 'subprocess', 'threading', 'multiprocessing', 'logging', 'argparse' } return module_name in stdlib_modules def generate_script_health_report(self) -> Dict: """Generate comprehensive script health report.""" validation = self.validate_all_scripts() dependencies = self.get_module_dependencies() # Calculate overall health score health_score = 0 if validation['total_scripts'] > 0: health_score = validation['overall_health'] # Deduct for issues for script in validation['scripts']: if script['status'] == 'syntax_error': health_score -= 25 elif script['status'] == 'issues': health_score -= 5 elif script['status'] == 'warnings': health_score -= 2 health_score = max(0, min(100, health_score)) return { 'health_score': round(health_score, 1), 'status': 'healthy' if health_score >= 80 else 'degraded' if health_score >= 60 else 'critical', 'total_scripts': validation['total_scripts'], 'valid_scripts': validation['valid_scripts'], 'scripts': validation['scripts'], 'dependencies': dependencies, 'recommendations': self._generate_recommendations(validation, health_score), 'timestamp': time.time() } def _generate_recommendations(self, validation: Dict, health_score: float) -> List[str]: """Generate recommendations based on validation results.""" recommendations = [] if health_score < 80: recommendations.append("[ATTENTION] Script health degraded: fix validation issues") problematic_scripts = [s for s in validation['scripts'] if s['status'] in ['syntax_error', 'issues']] if problematic_scripts: recommendations.append(f"Fix {len(problematic_scripts)} script(s) with issues") # Check docstring coverage low_doc_scripts = [ s for s in validation['scripts'] if s['metrics'].get('docstring_coverage', 100) < 50 ] if low_doc_scripts: recommendations.append("Improve docstring coverage in modules") # Check type hints low_type_scripts = [ s for s in validation['scripts'] if s['metrics'].get('type_hint_coverage', 100) < 50 ] if low_type_scripts: recommendations.append("Add type hints to function signatures") if not recommendations: recommendations.append("Script health excellent - no immediate action needed") return recommendations if __name__ == '__main__': import time checker = ScriptHealthChecker() print("=" * 70) print("SCRIPT HEALTH CHECK") print("=" * 70) report = checker.generate_script_health_report() print(f"Health Score: {report['health_score']}/100 ({report['status'].upper()})") print(f"Valid scripts: {report['valid_scripts']}/{report['total_scripts']}") print(f"External dependencies: {report['dependencies']['total_dependencies']}") print("\nProblematic scripts:") for script in report['scripts']: if script['status'] != 'valid': print(f" {script['script']}: {script['status']}") for issue in script['issues'][:2]: print(f" - {issue}") print("\nRecommendations:") for rec in report['recommendations']: print(f" - {rec}")