UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

791 lines (684 loc) 26 kB
#!/usr/bin/env node /** * Enhanced Post-Edit Pipeline - Comprehensive Validation Hook * Validates edited files with TypeScript, ESLint, Prettier, Security Analysis, and Code Metrics * * Features: * - TypeScript validation with error categorization * - ESLint integration for code quality * - Prettier formatting checks * - Security analysis (integrated security scanner) * - Code metrics (lines, functions, classes, complexity) * - Actionable recommendations engine * * Usage: node dist/hooks/post-edit-pipeline.js <file_path> [--memory-key <key>] [--agent-id <id>] */ import { spawnSync } from 'child_process'; import { existsSync, readFileSync, appendFileSync, mkdirSync } from 'fs'; import { dirname, extname, resolve } from 'path'; // Parse arguments const args = process.argv.slice(2); const filePath = args[0]; const memoryKeyIndex = args.indexOf('--memory-key'); const memoryKey = memoryKeyIndex >= 0 ? args[memoryKeyIndex + 1] : null; const agentIdIndex = args.indexOf('--agent-id'); const agentId = agentIdIndex >= 0 ? args[agentIdIndex + 1] : null; if (!filePath) { console.error('Error: File path required'); console.error('Usage: node dist/hooks/post-edit-pipeline.js <file_path> [--memory-key <key>] [--agent-id <id>]'); process.exit(1); } // Ensure log directory exists const logDir = '.artifacts/logs'; if (!existsSync(logDir)) { mkdirSync(logDir, { recursive: true }); } const logFile = `${logDir}/post-edit-pipeline.log`; function log(status, message, metadata = {}) { const entry = JSON.stringify({ timestamp: new Date().toISOString(), file: filePath, status, message, memoryKey, agentId, ...metadata }); console.log(entry); appendFileSync(logFile, entry + '\n'); } // Check if file exists if (!existsSync(filePath)) { log('ERROR', 'File not found', { path: filePath }); process.exit(1); } // Read file content for analysis const fileContent = readFileSync(filePath, 'utf-8'); const ext = extname(filePath); const baseName = filePath.replace(ext, '').split('/').pop(); // Initialize results object const results = { typescript: null, eslint: null, prettier: null, security: null, metrics: null, recommendations: [] }; // [Remaining TypeScript, ESLint, and Prettier validation code remains the same] // ============================================================================ // PHASE 2: Security Analysis // ============================================================================ log('VALIDATING', 'Running security analysis'); try { // Primary scanner method: security scanner script const securityScanProcess = spawnSync('bash', [ '.claude/skills/hook-pipeline/security-scanner.sh', filePath ], { encoding: 'utf-8', timeout: 10000 }); const securityScanOutput = securityScanProcess.stdout || '{}'; const exitCode = securityScanProcess.status; log('DEBUG', 'Security scanner output', { stdout: securityScanOutput, stderr: securityScanProcess.stderr, exitCode: exitCode }); try { const securityScanResults = JSON.parse(securityScanOutput); results.security = { passed: securityScanResults.passed, confidence: securityScanResults.confidence || 0, issues: Array.isArray(securityScanResults.vulnerabilities) ? securityScanResults.vulnerabilities : JSON.parse(securityScanResults.vulnerabilities || '[]'), details: securityScanOutput }; if (results.security.issues.length > 0) { log('SECURITY_WARNING', `Security scanner detected ${results.security.issues.length} vulnerabilities`, { confidence: results.security.confidence, issueTypes: results.security.issues }); // Transform scanner issues into recommendations results.security.issues.slice(0, 3).forEach(vuln => { results.recommendations.push({ type: 'security', priority: 'critical', message: `Security vulnerability: ${vuln}`, action: `Review and remediate ${vuln} vulnerability` }); }); // Add general security warning results.recommendations.push({ type: 'security', priority: 'critical', message: 'Security vulnerabilities detected by security scanner', action: 'Conduct thorough security review and address all vulnerabilities' }); } else { log('SUCCESS', 'No security vulnerabilities detected'); } } catch (parseError) { log('ERROR', 'Failed to parse security scanner output', { parseError: parseError.message, output: securityScanOutput }); // Fallback vulnerability detection (minimal built-in checks) const builtinChecks = [ { pattern: /eval\(/, vulnerability: 'POTENTIAL_RCE', severity: 'critical' }, { pattern: /innerHTML\s*=/, vulnerability: 'XSS_POTENTIAL', severity: 'high' }, { pattern: /(password|secret|token|api[-_]?key|anthropic|openai|openrouter|kimi|npm[-_]?token|zai|z[-_]ai).*=.*['"]?[^'"\s]{20,}['"]?/i, vulnerability: 'HARDCODED_SECRET', severity: 'critical' } ]; const foundVulnerabilities = builtinChecks .filter(check => check.pattern.test(fileContent)) .map(check => ({ type: check.vulnerability, severity: check.severity })); results.security = { passed: foundVulnerabilities.length === 0, confidence: 50, issues: foundVulnerabilities, details: 'Fallback vulnerability detection' }; if (foundVulnerabilities.length > 0) { log('SECURITY_WARNING', 'Vulnerabilities detected by fallback method', { vulnerabilities: foundVulnerabilities }); foundVulnerabilities.forEach(vuln => { results.recommendations.push({ type: 'security', priority: vuln.severity === 'critical' ? 'critical' : 'high', message: `Potential ${vuln.type} vulnerability detected`, action: `Manually review code for ${vuln.type} vulnerability` }); }); } } } catch (error) { log('CRITICAL_ERROR', 'Unexpected security scanning failure', { error: error.message, stack: error.stack }); results.security = { passed: false, confidence: 0, issues: [], details: 'Complete security scanning failure' }; results.recommendations.push({ type: 'security', priority: 'critical', message: 'Security scanning infrastructure failure', action: 'Verify security scanning script and dependencies' }); } // ============================================================================ // PHASE 2.5: Bash Validator Integration // ============================================================================ log('VALIDATING', 'Running bash validators'); // Validator mapping by file extension const validatorsByExtension = { '.sh': [ 'bash-pipe-safety.sh', 'bash-dependency-checker.sh', 'enforce-lf.sh' ], '.bash': [ 'bash-pipe-safety.sh', 'bash-dependency-checker.sh', 'enforce-lf.sh' ], '.py': [ 'python-subprocess-safety.py', 'python-async-safety.py', 'python-import-checker.py', 'enforce-lf.sh' ], '.js': [ 'js-promise-safety.sh', 'enforce-lf.sh' ], '.ts': [ 'js-promise-safety.sh', 'enforce-lf.sh' ], '.jsx': [ 'js-promise-safety.sh', 'enforce-lf.sh' ], '.tsx': [ 'js-promise-safety.sh', 'enforce-lf.sh' ], '.rs': [ 'rust-command-safety.sh', 'rust-future-safety.sh', 'rust-dependency-checker.sh', 'enforce-lf.sh' ] }; // Helper function to run a single validator function runValidator(validatorName, targetFile) { const validatorPath = `.claude/skills/hook-pipeline/${validatorName}`; log('DEBUG', `Executing validator: ${validatorName}`, { targetFile }); try { // Determine interpreter based on file extension const isPython = validatorName.endsWith('.py'); const interpreter = isPython ? 'python3' : 'bash'; const result = spawnSync(interpreter, [validatorPath, targetFile], { encoding: 'utf-8', timeout: 5000, cwd: process.cwd() }); const exitCode = result.status; const stdout = (result.stdout || '').trim(); const stderr = (result.stderr || '').trim(); log('DEBUG', `Validator ${validatorName} completed`, { exitCode, stdout: stdout.substring(0, 200), // Truncate for logging stderr: stderr.substring(0, 200) }); // Exit code convention: // 0 = pass (no issues) // 1 = error (blocking issue) // 2 = warning (non-blocking issue) return { validator: validatorName, exitCode, passed: exitCode === 0, isBlocking: exitCode === 1, isWarning: exitCode === 2, message: stderr || stdout || 'Validator passed', stdout, stderr }; } catch (error) { log('ERROR', `Validator ${validatorName} execution failed`, { error: error.message, stack: error.stack }); return { validator: validatorName, exitCode: -1, passed: false, isBlocking: false, isWarning: true, message: `Validator execution failed: ${error.message}`, error: error.message }; } } // Run validators for applicable file types const applicableValidators = validatorsByExtension[ext] || []; if (applicableValidators.length > 0) { log('INFO', `Running ${applicableValidators.length} bash validators for ${ext} file`); // Sequential execution of validators const validatorResults = applicableValidators.map(validator => runValidator(validator, filePath) ); // Process validator results validatorResults.forEach(result => { if (result.isBlocking) { // Blocking error (exit code 1) log('VALIDATOR_ERROR', `Blocking issue detected by ${result.validator}`, { message: result.message }); results.recommendations.push({ type: 'bash-validator', priority: 'critical', message: `${result.validator}: ${result.message}`, action: 'Fix blocking issue before proceeding' }); } else if (result.isWarning) { // Warning (exit code 2) log('VALIDATOR_WARNING', `Warning from ${result.validator}`, { message: result.message }); results.recommendations.push({ type: 'bash-safety', priority: 'medium', message: `${result.validator}: ${result.message}`, action: 'Review recommendations and consider fixing' }); } else if (result.passed) { // Pass (exit code 0) log('SUCCESS', `Validator ${result.validator} passed`); } }); // Store validator results for exit code determination results.bashValidators = { executed: validatorResults.length, passed: validatorResults.filter(r => r.passed).length, warnings: validatorResults.filter(r => r.isWarning).length, errors: validatorResults.filter(r => r.isBlocking).length, results: validatorResults }; log('SUCCESS', `Bash validators completed`, { executed: results.bashValidators.executed, passed: results.bashValidators.passed, warnings: results.bashValidators.warnings, errors: results.bashValidators.errors }); } else { log('DEBUG', `No bash validators configured for ${ext} files`); } // ============================================================================ // PHASE 3: Root Directory Detection // ============================================================================ log('VALIDATING', 'Checking file location (root directory warning)'); const isRootFile = dirname(resolve(filePath)) === resolve('.'); if (isRootFile && !filePath.match(/^(package\.json|tsconfig\.json|\.gitignore|\.env.*|README\.md|LICENSE|CLAUDE\.md)$/)) { // Suggest appropriate location based on file type const suggestions = []; if (ext.match(/\.(js|ts|jsx|tsx)$/)) { suggestions.push({ location: `src/${filePath}`, reason: 'Source files belong in src/' }); } if (ext.match(/\.(test|spec)\.(js|ts|jsx|tsx)$/)) { suggestions.push({ location: `tests/${filePath}`, reason: 'Test files belong in tests/' }); } if (ext === '.md' && !filePath.match(/^(README|CLAUDE)\.md$/)) { suggestions.push({ location: `docs/${filePath}`, reason: 'Documentation belongs in docs/' }); } if (ext === '.json' && !filePath.match(/^package\.json$/)) { suggestions.push({ location: `config/${filePath}`, reason: 'Config files belong in config/' }); } if (ext === '.sh') { suggestions.push({ location: `scripts/${filePath}`, reason: 'Scripts belong in scripts/' }); } if (suggestions.length > 0) { log('ROOT_WARNING', 'File in root directory - should be organized', { file: filePath, suggestions }); results.recommendations.push({ type: 'organization', priority: 'high', message: `File "${filePath}" should not be in root directory`, action: `Move to: ${suggestions[0].location}`, suggestions }); // Store for handler processing results.rootWarning = { suggestions }; } } // ============================================================================ // PHASE 4: TDD Violation Detection // ============================================================================ if (ext.match(/\.(js|ts|jsx|tsx|py|go|rs)$/) && !filePath.match(/\.(test|spec)\./)) { log('VALIDATING', 'Checking TDD compliance'); const testPatterns = { js: [`${dirname(filePath)}/${baseName}.test.js`, `tests/${baseName}.test.js`], ts: [`${dirname(filePath)}/${baseName}.test.ts`, `tests/${baseName}.test.ts`], py: [`${dirname(filePath)}/test_${baseName}.py`, `tests/test_${baseName}.py`], go: [`${dirname(filePath)}/${baseName}_test.go`], rs: null // Rust uses inline tests }; const langKey = ext.replace('.', ''); const patterns = testPatterns[langKey]; if (patterns) { const hasTest = patterns.some(p => existsSync(p)); if (!hasTest) { log('TDD_VIOLATION', 'No test file found', { file: filePath, expectedLocations: patterns }); results.recommendations.push({ type: 'testing', priority: 'high', message: 'No test file found for this module', action: 'Create test file or run feedback-resolver.sh --type TDD_VIOLATION' }); results.tddViolation = { hasTests: false, testFile: patterns[0], recommendations: [`Create ${patterns[0]}`] }; } } } // ============================================================================ // PHASE 5: Code Metrics and Complexity Analysis // ============================================================================ log('VALIDATING', 'Calculating code metrics'); const lines = fileContent.split('\n').length; const functions = (fileContent.match(/function\s+\w+|const\s+\w+\s*=\s*\(/g) || []).length; const classes = (fileContent.match(/class\s+\w+/g) || []).length; const todos = (fileContent.match(/\/\/\s*TODO/gi) || []).length; const fixmes = (fileContent.match(/\/\/\s*FIXME/gi) || []).length; results.metrics = { lines, functions, classes, todos, fixmes, complexity: lines > 300 ? 'high' : lines > 100 ? 'medium' : 'low' }; log('SUCCESS', 'Code metrics calculated', results.metrics); // ============================================================================ // PHASE 5.1: Cyclomatic Complexity Analysis // ============================================================================ log('VALIDATING', 'Analyzing cyclomatic complexity'); // Only analyze files >200 lines to reduce overhead if (lines > 200 && ext.match(/\.(sh|js|ts|jsx|tsx|py)$/)) { try { // Use simple-complexity.sh for bash scripts if (ext === '.sh') { const complexityResult = spawnSync('bash', [ 'tools/simple-complexity.sh', filePath ], { encoding: 'utf-8', timeout: 5000 }); if (complexityResult.status === 0) { const output = complexityResult.stdout; const complexityMatch = output.match(/Total Complexity:\s*(\d+)/); if (complexityMatch) { const complexity = parseInt(complexityMatch[1], 10); results.metrics.cyclomaticComplexity = complexity; log('SUCCESS', `Cyclomatic complexity: ${complexity}`, { complexity }); // Warning threshold: 30 if (complexity >= 30 && complexity < 40) { log('COMPLEXITY_WARNING', `Moderate complexity detected: ${complexity}`, { threshold: 30, complexity }); results.recommendations.push({ type: 'complexity', priority: 'medium', message: `Cyclomatic complexity is ${complexity} (threshold: 30)`, action: 'Consider refactoring to reduce complexity' }); } // Critical threshold: 40 - invoke lizard for detailed analysis if (complexity >= 40) { log('COMPLEXITY_CRITICAL', `High complexity detected: ${complexity}, invoking lizard`, { threshold: 40, complexity }); // Check if lizard is available const lizardCheck = spawnSync('which', ['lizard'], { encoding: 'utf-8' }); if (lizardCheck.status === 0) { // Run lizard for detailed analysis const lizardResult = spawnSync('lizard', [ filePath, '-C', '15' // Show functions with complexity >15 ], { encoding: 'utf-8', timeout: 10000 }); if (lizardResult.status === 0) { const lizardOutput = lizardResult.stdout; log('LIZARD_ANALYSIS', 'Detailed complexity analysis', { output: lizardOutput }); results.complexityAnalysis = { tool: 'lizard', complexity, detailedReport: lizardOutput }; results.recommendations.push({ type: 'complexity', priority: 'critical', message: `Critical complexity level: ${complexity} (threshold: 40)`, action: 'Refactor immediately. Run cyclomatic-complexity-reducer agent', details: lizardOutput }); } else { log('WARN', 'Lizard analysis failed', { stderr: lizardResult.stderr }); } } else { log('WARN', 'Lizard not installed, skipping detailed analysis'); results.recommendations.push({ type: 'complexity', priority: 'critical', message: `Critical complexity level: ${complexity} (threshold: 40)`, action: 'Refactor immediately. Install lizard: ./tools/install-lizard.sh' }); } } } } else { log('WARN', 'Complexity analysis failed', { stderr: complexityResult.stderr }); } } // For TypeScript/JavaScript, use lizard directly if available else if (ext.match(/\.(js|ts|jsx|tsx)$/)) { const lizardCheck = spawnSync('which', ['lizard'], { encoding: 'utf-8' }); if (lizardCheck.status === 0) { const lizardResult = spawnSync('lizard', [ filePath, '--json' ], { encoding: 'utf-8', timeout: 10000 }); if (lizardResult.status === 0) { try { const lizardData = JSON.parse(lizardResult.stdout); // Calculate average complexity let totalComplexity = 0; let functionCount = 0; if (lizardData.function_list) { lizardData.function_list.forEach(func => { totalComplexity += func.cyclomatic_complexity || 0; functionCount++; }); } const avgComplexity = functionCount > 0 ? Math.round(totalComplexity / functionCount) : 0; results.metrics.cyclomaticComplexity = avgComplexity; if (avgComplexity >= 30) { log('COMPLEXITY_WARNING', `Average complexity: ${avgComplexity}`, { avgComplexity, functionCount }); results.recommendations.push({ type: 'complexity', priority: avgComplexity >= 40 ? 'critical' : 'medium', message: `Average cyclomatic complexity: ${avgComplexity}`, action: avgComplexity >= 40 ? 'Critical: Refactor high-complexity functions immediately' : 'Consider refactoring complex functions' }); } } catch (parseError) { log('WARN', 'Failed to parse lizard JSON output', { error: parseError.message }); } } } } } catch (error) { log('WARN', 'Complexity analysis error', { error: error.message }); } } // Check for Rust-specific quality issues if (ext === '.rs') { log('VALIDATING', 'Running Rust quality checks'); const rustIssues = []; if (fileContent.match(/println!\(/)) rustIssues.push('debug_println'); if (fileContent.match(/unwrap\(\)/)) rustIssues.push('unwrap_usage'); if (fileContent.match(/panic!\(/)) rustIssues.push('panic_usage'); if (rustIssues.length > 0) { log('RUST_QUALITY', 'Rust quality issues detected', { issues: rustIssues }); results.recommendations.push({ type: 'rust', priority: 'medium', message: 'Rust quality issues detected', action: 'Run: cargo fmt && cargo clippy --fix --allow-dirty' }); results.rustQuality = { issues: rustIssues }; } } // ============================================================================ // PHASE 6: Final Recommendations // ============================================================================ log('VALIDATING', 'Generating recommendations'); // Type safety recommendations if (ext.match(/\.(ts|tsx)$/) && fileContent.match(/:\s*any\b/)) { results.recommendations.push({ type: 'typescript', priority: 'medium', message: 'Avoid using "any" type when possible', action: 'Use specific types or unknown for better type safety' }); } // Testing recommendations if (!filePath.match(/\.(test|spec)\./)) { results.recommendations.push({ type: 'testing', priority: 'medium', message: 'Consider writing tests for this module', action: 'Create corresponding test file to ensure code reliability' }); } log('SUCCESS', `Generated ${results.recommendations.length} recommendations`); // ============================================================================ // PHASE 7: Exit Code Determination // ============================================================================ let exitCode = 0; let finalStatus = 'SUCCESS'; // Check for critical complexity issues const hasComplexityIssue = results.recommendations.find(r => r.type === 'complexity'); // Check for bash validator issues const hasBashValidatorError = results.bashValidators && results.bashValidators.errors > 0; const hasBashValidatorWarning = results.bashValidators && results.bashValidators.warnings > 0; if (hasBashValidatorError) { exitCode = 9; finalStatus = 'BASH_VALIDATOR_ERROR'; } else if (results.rootWarning) { exitCode = 2; finalStatus = 'ROOT_WARNING'; } else if (results.tddViolation) { exitCode = 3; finalStatus = 'TDD_VIOLATION'; } else if (hasBashValidatorWarning) { exitCode = 10; finalStatus = 'BASH_VALIDATOR_WARNING'; } else if (hasComplexityIssue && hasComplexityIssue.priority === 'critical') { exitCode = 7; finalStatus = 'COMPLEXITY_CRITICAL'; } else if (hasComplexityIssue && hasComplexityIssue.priority === 'medium') { exitCode = 8; finalStatus = 'COMPLEXITY_WARNING'; } else if (results.rustQuality) { exitCode = 5; finalStatus = 'RUST_QUALITY'; } else if (results.prettier && !results.prettier.passed) { exitCode = 6; finalStatus = 'LINT_ISSUES'; } else if (results.typescript && !results.typescript.passed) { exitCode = 1; finalStatus = 'TYPE_WARNING'; } else if (results.recommendations.length > 0) { finalStatus = 'IMPROVEMENTS_SUGGESTED'; } const finalResult = { typescript: results.typescript, eslint: results.eslint, prettier: results.prettier, security: results.security, metrics: results.metrics, recommendationCount: results.recommendations.length, topRecommendations: results.recommendations.slice(0, 3) }; // Include structured data for feedback handlers if (results.rootWarning) { finalResult.rootWarning = results.rootWarning; } if (results.tddViolation) { finalResult.tddViolation = results.tddViolation; } if (results.bashValidators) { finalResult.bashValidators = results.bashValidators; } if (results.rustQuality) { finalResult.rustQuality = results.rustQuality; } if (results.complexityAnalysis) { finalResult.complexityAnalysis = results.complexityAnalysis; } log(finalStatus, 'Pipeline validation complete', finalResult); process.exit(exitCode);