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
JavaScript
/**
* 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);