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.
388 lines (387 loc) • 14.4 kB
JavaScript
/**
* Post-Edit Validator - File validation after modifications
* Runs syntax, formatting, and linting checks on edited files
*/ import * as fs from 'fs/promises';
import * as path from 'path';
import { spawn } from 'child_process';
export class PostEditValidator {
config;
projectRoot;
configPath;
constructor(projectRoot = process.cwd()){
this.projectRoot = projectRoot;
this.configPath = path.join(projectRoot, '.claude/hooks/cfn-post-edit.config.json');
this.config = {
checkSyntax: true,
checkFormatting: true,
checkDuplication: false,
blockingValidation: false
};
}
/**
* Load validation config from JSON file
*/ async loadConfig() {
try {
const configContent = await fs.readFile(this.configPath, 'utf-8');
const rawConfig = JSON.parse(configContent);
// Merge with defaults
this.config = {
checkSyntax: rawConfig.validation?.syntax?.enabled ?? true,
checkFormatting: rawConfig.validation?.formatting?.enabled ?? true,
checkDuplication: rawConfig.validation?.duplication?.enabled ?? false,
blockingValidation: rawConfig.blocking ?? false,
typescript: rawConfig.validation?.typescript ?? {
enabled: true,
noEmit: true,
skipLibCheck: true
},
bash: rawConfig.validation?.bash ?? {
enabled: true,
validators: [
'pipe-safety',
'dependency-checker',
'line-endings'
],
timeout: 5000
}
};
return this.config;
} catch (error) {
// Return defaults if config not found
return this.config;
}
}
/**
* Get file extension to determine file type
*/ getFileExtension(filePath) {
return path.extname(filePath).toLowerCase();
}
/**
* Validate TypeScript/JavaScript files
*/ async validateTypeScript(filePath) {
const startTime = Date.now();
const result = {
passed: true,
errors: [],
warnings: [],
suggestions: [],
timestamp: new Date().toISOString(),
filePath,
executionTime: 0
};
try {
// Check if tsc is available
const tscPath = path.join(this.projectRoot, 'node_modules/.bin/tsc');
await fs.stat(tscPath);
return await this.runTypeScriptCheck(filePath, result);
} catch {
// TypeScript not available, skip
result.warnings.push('TypeScript compiler not available, skipping type checking');
}
result.executionTime = Date.now() - startTime;
return result;
}
/**
* Run TypeScript compilation check
*/ runTypeScriptCheck(filePath, result) {
return new Promise((resolve)=>{
try {
const tsc = spawn('npx', [
'tsc',
'--noEmit',
filePath
], {
cwd: this.projectRoot,
timeout: 10000
});
let stderr = '';
let stdout = '';
tsc.stdout?.on('data', (data)=>{
stdout += data.toString();
});
tsc.stderr?.on('data', (data)=>{
stderr += data.toString();
});
tsc.on('close', (code)=>{
if (code !== 0 && stdout) {
result.passed = false;
result.errors.push(`TypeScript compilation failed:\n${stdout}`);
}
result.executionTime = Date.now() - new Date(result.timestamp).getTime();
resolve(result);
});
tsc.on('error', (error)=>{
result.warnings.push(`TypeScript check skipped: ${error.message}`);
result.executionTime = Date.now() - new Date(result.timestamp).getTime();
resolve(result);
});
} catch (error) {
result.warnings.push(`Failed to run TypeScript check: ${error}`);
result.executionTime = Date.now() - new Date(result.timestamp).getTime();
resolve(result);
}
});
}
/**
* Validate Bash scripts
*/ async validateBash(filePath) {
const startTime = Date.now();
const result = {
passed: true,
errors: [],
warnings: [],
suggestions: [],
timestamp: new Date().toISOString(),
filePath,
executionTime: 0
};
try {
// Run basic bash syntax check with bash -n
const content = await fs.readFile(filePath, 'utf-8');
// Check for common bash issues
if (content.includes('[ -z')) {
result.warnings.push('Consider using [[ -z instead of [ -z for more robust string checking');
}
if (content.match(/\|\s*while.*read/)) {
result.warnings.push('Pipe to while-read can cause issues with variable scope, consider using process substitution');
}
if (!content.includes('set -euo pipefail')) {
result.suggestions.push('Add "set -euo pipefail" at the top of the script for safety');
}
// Check for unquoted variables
const unquotedVars = content.match(/\$\w+(?![\w"}])/g) || [];
if (unquotedVars.length > 0) {
result.warnings.push(`Found ${unquotedVars.length} potentially unquoted variables - consider quoting them`);
}
result.executionTime = Date.now() - startTime;
return result;
} catch (error) {
result.errors.push(`Bash validation failed: ${error}`);
result.passed = false;
result.executionTime = Date.now() - startTime;
return result;
}
}
/**
* Validate JSON files
*/ async validateJSON(filePath) {
const startTime = Date.now();
const result = {
passed: true,
errors: [],
warnings: [],
suggestions: [],
timestamp: new Date().toISOString(),
filePath,
executionTime: 0
};
try {
const content = await fs.readFile(filePath, 'utf-8');
JSON.parse(content);
} catch (error) {
result.passed = false;
result.errors.push(`JSON validation failed: ${error}`);
}
result.executionTime = Date.now() - startTime;
return result;
}
/**
* Check for code duplication
*/ async checkDuplication(filePath) {
const startTime = Date.now();
const result = {
passed: true,
errors: [],
warnings: [],
suggestions: [],
timestamp: new Date().toISOString(),
filePath,
executionTime: 0
};
try {
const content = await fs.readFile(filePath, 'utf-8');
const lines = content.split('\n');
// Check for duplicate consecutive lines
const seenLines = new Map();
for(let i = 0; i < lines.length; i++){
const line = lines[i].trim();
if (line && line.length > 20) {
// Only check non-trivial lines
const count = seenLines.get(line) || 0;
if (count > 0) {
result.suggestions.push(`Duplicate line found at line ${i + 1}: "${line.substring(0, 50)}..."`);
}
seenLines.set(line, count + 1);
}
}
} catch (error) {
result.warnings.push(`Duplication check skipped: ${error}`);
}
result.executionTime = Date.now() - startTime;
return result;
}
/**
* Check file formatting consistency
*/ async checkFormatting(filePath) {
const startTime = Date.now();
const result = {
passed: true,
errors: [],
warnings: [],
suggestions: [],
timestamp: new Date().toISOString(),
filePath,
executionTime: 0
};
try {
const content = await fs.readFile(filePath, 'utf-8');
const lines = content.split('\n');
// Check for mixed line endings
const hasWindowsLineEndings = content.includes('\r\n');
const hasUnixLineEndings = content.includes('\n');
if (hasWindowsLineEndings && hasUnixLineEndings) {
result.warnings.push('Mixed line endings detected (CRLF and LF)');
}
// Check for trailing whitespace
let trailingWhitespaceCount = 0;
for (const line of lines){
if (line !== line.trimRight()) {
trailingWhitespaceCount++;
}
}
if (trailingWhitespaceCount > 0) {
result.suggestions.push(`${trailingWhitespaceCount} lines have trailing whitespace`);
}
// Check for tabs vs spaces
const hasTabs = content.includes('\t');
const hasSpaces = /^ +/.test(content);
if (hasTabs && hasSpaces) {
result.warnings.push('Mixed tabs and spaces detected');
}
} catch (error) {
result.warnings.push(`Formatting check skipped: ${error}`);
}
result.executionTime = Date.now() - startTime;
return result;
}
/**
* Run full validation pipeline on a file
*/ async validateFile(filePath, agentId) {
try {
await this.loadConfig();
// Validate file exists
await fs.stat(filePath);
} catch {
return {
passed: false,
errors: [
`File does not exist: ${filePath}`
],
warnings: [],
suggestions: [],
timestamp: new Date().toISOString(),
filePath,
executionTime: 0
};
}
// Run validation pipeline
return await this.runValidationPipeline(filePath);
}
/**
* Execute complete validation pipeline
*/ async runValidationPipeline(filePath) {
const startTime = Date.now();
const ext = this.getFileExtension(filePath);
const aggregatedResult = {
passed: true,
errors: [],
warnings: [],
suggestions: [],
timestamp: new Date().toISOString(),
filePath,
executionTime: 0
};
try {
// Run type-specific validation
let typeValidation;
if ([
'.ts',
'.tsx'
].includes(ext)) {
if (this.config.typescript?.enabled) {
typeValidation = await this.validateTypeScript(filePath);
this.mergeResults(aggregatedResult, typeValidation);
}
} else if ([
'.js',
'.jsx'
].includes(ext)) {
// Basic JS validation would go here
} else if ([
'.json'
].includes(ext)) {
typeValidation = await this.validateJSON(filePath);
this.mergeResults(aggregatedResult, typeValidation);
} else if ([
'.sh',
'.bash'
].includes(ext)) {
if (this.config.bash?.enabled) {
typeValidation = await this.validateBash(filePath);
this.mergeResults(aggregatedResult, typeValidation);
}
}
// Run common checks
if (this.config.checkFormatting) {
const formattingResult = await this.checkFormatting(filePath);
this.mergeResults(aggregatedResult, formattingResult);
}
if (this.config.checkDuplication) {
const duplicationResult = await this.checkDuplication(filePath);
this.mergeResults(aggregatedResult, duplicationResult);
}
aggregatedResult.executionTime = Date.now() - startTime;
return aggregatedResult;
} catch (error) {
aggregatedResult.errors.push(`Validation pipeline failed: ${error}`);
aggregatedResult.passed = false;
aggregatedResult.executionTime = Date.now() - startTime;
return aggregatedResult;
}
}
/**
* Merge validation results
*/ mergeResults(target, source) {
target.errors.push(...source.errors);
target.warnings.push(...source.warnings);
target.suggestions.push(...source.suggestions);
if (!source.passed) {
target.passed = false;
}
}
/**
* Get validation feedback summary
*/ getValidationSummary(result) {
const lines = [];
if (result.passed) {
lines.push('✅ Validation passed');
} else {
lines.push('❌ Validation failed');
}
if (result.errors.length > 0) {
lines.push(`\nErrors (${result.errors.length}):`);
result.errors.forEach((error)=>lines.push(` - ${error}`));
}
if (result.warnings.length > 0) {
lines.push(`\nWarnings (${result.warnings.length}):`);
result.warnings.forEach((warning)=>lines.push(` - ${warning}`));
}
if (result.suggestions.length > 0) {
lines.push(`\nSuggestions (${result.suggestions.length}):`);
result.suggestions.forEach((suggestion)=>lines.push(` - ${suggestion}`));
}
lines.push(`\nExecution time: ${result.executionTime}ms`);
return lines.join('\n');
}
}
//# sourceMappingURL=post-edit-validator.js.map