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.
504 lines (452 loc) • 13.9 kB
text/typescript
/**
* CFN Validator - Unified Validation Module
*
* Core validation functionality for CFN Loop system:
* - Deliverable verification (prevents "consensus on vapor")
* - Success criteria validation
* - Gate threshold checking
* - Consensus validation
* - Vapor detection
*
* @module cfn-loop-validation/validator
*/
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { execSync } from 'child_process';
import {
ExecutionMode,
ValidationConfig,
DeliverableValidation,
DeliverableValidationResult,
SuccessCriteria,
SuccessCriteriaValidationResult,
GateValidationResult,
ConsensusValidationResult,
VaporDetectionResult,
ValidationResult,
ValidationError,
ConsensusOnVaporError,
} from './types';
/**
* CFN Validator class
*/
export class CFNValidator {
private config: ValidationConfig;
private readonly modeThresholds = {
mvp: 0.70,
standard: 0.95,
enterprise: 0.98,
};
constructor(config: ValidationConfig) {
this.config = config;
}
/**
* Get mode-specific threshold
*/
getModeThreshold(mode?: ExecutionMode): number {
const targetMode = mode || this.config.mode;
return this.modeThresholds[targetMode];
}
/**
* Validate deliverables exist and are accessible
*/
async validateDeliverables(
paths: string[]
): Promise<DeliverableValidationResult> {
const deliverables: DeliverableValidation[] = [];
let totalSizeBytes = 0;
for (const filePath of paths) {
const validation: DeliverableValidation = {
path: filePath,
exists: false,
};
try {
// Check if file exists
if (!fs.existsSync(filePath)) {
deliverables.push(validation);
continue;
}
const stats = fs.statSync(filePath);
validation.exists = true;
validation.sizeBytes = stats.size;
validation.lastModified = stats.mtime.toISOString();
validation.readable = true;
totalSizeBytes += stats.size;
// Get MIME type (basic detection)
validation.mimeType = this.detectMimeType(filePath);
} catch (error) {
validation.exists = false;
validation.error = error instanceof Error ? error.message : String(error);
validation.readable = false;
}
deliverables.push(validation);
}
const existingFiles = deliverables.filter((d) => d.exists).length;
return {
deliverables,
totalFiles: paths.length,
existingFiles,
missingFiles: paths.length - existingFiles,
totalSizeBytes,
allExist: existingFiles === paths.length,
timestamp: Date.now(),
};
}
/**
* Check if success criteria are met
*/
async checkSuccessCriteria(
criteria: SuccessCriteria[]
): Promise<SuccessCriteriaValidationResult> {
const results: SuccessCriteriaValidationResult['details'] = [];
let passedCount = 0;
for (const criterion of criteria) {
const result: SuccessCriteriaValidationResult['details'][0] = {
criterion,
passed: false,
};
try {
switch (criterion.type) {
case 'file_exists':
result.passed = await this.validateFileExists(criterion);
break;
case 'test_pass':
result.passed = await this.validateTestPass(criterion);
break;
case 'command_output':
result.passed = await this.validateCommandOutput(criterion);
break;
case 'custom':
result.passed = await this.validateCustom(criterion);
break;
}
if (result.passed) {
passedCount++;
}
} catch (error) {
result.error = error instanceof Error ? error.message : String(error);
result.passed = false;
}
results.push(result);
}
return {
passed: passedCount === criteria.length,
criteria,
passedCount,
failedCount: criteria.length - passedCount,
details: results,
timestamp: Date.now(),
};
}
/**
* Validate file exists
*/
private async validateFileExists(criterion: SuccessCriteria): Promise<boolean> {
if (!criterion.paths || criterion.paths.length === 0) {
throw new ValidationError('No paths provided for file_exists criterion', 'INVALID_CRITERION');
}
for (const filePath of criterion.paths) {
if (!fs.existsSync(filePath)) {
return false;
}
}
return true;
}
/**
* Validate test passes
*/
private async validateTestPass(criterion: SuccessCriteria): Promise<boolean> {
if (!criterion.command) {
throw new ValidationError('No command provided for test_pass criterion', 'INVALID_CRITERION');
}
try {
const timeout = criterion.timeout || 30000;
execSync(criterion.command, {
timeout,
stdio: 'pipe',
});
return true;
} catch (error) {
return false;
}
}
/**
* Validate command output
*/
private async validateCommandOutput(criterion: SuccessCriteria): Promise<boolean> {
if (!criterion.command) {
throw new ValidationError('No command provided for command_output criterion', 'INVALID_CRITERION');
}
try {
const timeout = criterion.timeout || 30000;
const output = execSync(criterion.command, {
timeout,
stdio: 'pipe',
encoding: 'utf-8',
});
if (!criterion.expected) {
return true;
}
return output.trim().includes(String(criterion.expected));
} catch (error) {
return false;
}
}
/**
* Validate custom criterion
*/
private async validateCustom(criterion: SuccessCriteria): Promise<boolean> {
// Custom validation - can be extended by subclasses
// For now, just return false unless explicitly implemented
return false;
}
/**
* Validate gate pass (simple threshold check)
*/
async validateGatePass(
passRate: number,
mode?: ExecutionMode
): Promise<GateValidationResult> {
const targetMode = mode || this.config.mode;
const threshold = this.getModeThreshold(targetMode);
const passed = passRate >= threshold;
return {
passed,
passRate,
threshold,
mode: targetMode,
gap: passed ? 0 : threshold - passRate,
reason: passed
? `Gate PASSED: pass rate ${(passRate * 100).toFixed(2)}% >= threshold ${(threshold * 100).toFixed(2)}%`
: `Gate FAILED: pass rate ${(passRate * 100).toFixed(2)}% < threshold ${(threshold * 100).toFixed(2)}%`,
timestamp: Date.now(),
};
}
/**
* Validate consensus pass
*/
async validateConsensus(
scores: number[],
mode?: ExecutionMode
): Promise<ConsensusValidationResult> {
if (scores.length === 0) {
throw new ValidationError('No consensus scores provided', 'INVALID_CONSENSUS');
}
const targetMode = mode || this.config.mode;
const threshold = this.getModeThreshold(targetMode);
// Calculate average consensus score
const consensusScore = scores.reduce((a, b) => a + b, 0) / scores.length;
const passed = consensusScore >= threshold;
return {
passed,
consensusScore,
threshold,
mode: targetMode,
validatorCount: scores.length,
scores,
timestamp: Date.now(),
};
}
/**
* Detect consensus on vapor - prevents false completion claims
*
* Returns true if agent claims completion but deliverables don't exist
*/
async detectConsensusOnVapor(
agentOutput: string,
expectedDeliverables: string[]
): Promise<VaporDetectionResult> {
// Check if agent claims completion/success
const claimsCompletion =
/complete|done|finished|success|delivered|implemented|created|generated/i.test(
agentOutput
);
// Validate deliverables exist
const validation = await this.validateDeliverables(expectedDeliverables);
const result: VaporDetectionResult = {
detected: claimsCompletion && !validation.allExist,
claimsCompletion,
deliverablesMissing: !validation.allExist,
missingDeliverables: validation.deliverables
.filter((d) => !d.exists)
.map((d) => d.path),
agentOutput: agentOutput.substring(0, 500), // Truncate for privacy
expectedDeliverables,
confidence: this.calculateVaporConfidence(
claimsCompletion,
validation.missingFiles,
expectedDeliverables.length
),
timestamp: Date.now(),
};
return result;
}
/**
* Calculate vapor detection confidence
*
* Higher confidence when more deliverables are missing despite completion claims
*/
private calculateVaporConfidence(
claimsCompletion: boolean,
missingFileCount: number,
totalFileCount: number
): number {
if (!claimsCompletion || missingFileCount === 0) {
return 0.0;
}
// Confidence increases with percentage of missing files
const missingRatio = missingFileCount / totalFileCount;
return Math.min(1.0, missingRatio);
}
/**
* Detect MIME type from file extension
*/
private detectMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: Record<string, string> = {
'.js': 'application/javascript',
'.ts': 'application/typescript',
'.json': 'application/json',
'.html': 'text/html',
'.css': 'text/css',
'.md': 'text/markdown',
'.txt': 'text/plain',
'.sh': 'application/x-sh',
'.py': 'text/x-python',
'.java': 'text/x-java-source',
};
return mimeTypes[ext] || 'application/octet-stream';
}
/**
* Perform comprehensive validation
*/
async performValidation(options: {
deliverables?: string[];
successCriteria?: SuccessCriteria[];
passRate?: number;
consensusScores?: number[];
agentOutput?: string;
}): Promise<ValidationResult> {
const errors: ValidationError[] = [];
const warnings: string[] = [];
const result: ValidationResult = {
taskId: this.config.taskId,
timestamp: Date.now(),
mode: this.config.mode,
passed: true,
errors,
warnings,
};
// Validate deliverables
if (options.deliverables) {
try {
result.deliverables = await this.validateDeliverables(options.deliverables);
if (!result.deliverables.allExist) {
warnings.push(
`${result.deliverables.missingFiles} deliverables missing`
);
}
} catch (error) {
errors.push(
new ValidationError(
`Deliverable validation failed: ${error}`,
'DELIVERABLE_VALIDATION_FAILED'
)
);
result.passed = false;
}
}
// Validate success criteria
if (options.successCriteria && options.successCriteria.length > 0) {
try {
result.successCriteria = await this.checkSuccessCriteria(
options.successCriteria
);
if (!result.successCriteria.passed) {
warnings.push(
`${result.successCriteria.failedCount} success criteria not met`
);
}
} catch (error) {
errors.push(
new ValidationError(
`Success criteria validation failed: ${error}`,
'CRITERIA_VALIDATION_FAILED'
)
);
result.passed = false;
}
}
// Validate gate
if (typeof options.passRate === 'number') {
try {
result.gate = await this.validateGatePass(options.passRate);
if (!result.gate.passed) {
result.passed = false;
}
} catch (error) {
errors.push(
new ValidationError(
`Gate validation failed: ${error}`,
'GATE_VALIDATION_FAILED'
)
);
result.passed = false;
}
}
// Validate consensus
if (options.consensusScores && options.consensusScores.length > 0) {
try {
result.consensus = await this.validateConsensus(
options.consensusScores
);
if (!result.consensus.passed) {
result.passed = false;
}
} catch (error) {
errors.push(
new ValidationError(
`Consensus validation failed: ${error}`,
'CONSENSUS_VALIDATION_FAILED'
)
);
result.passed = false;
}
}
// Detect vapor
if (
options.agentOutput &&
options.deliverables &&
options.deliverables.length > 0
) {
try {
result.vapor = await this.detectConsensusOnVapor(
options.agentOutput,
options.deliverables
);
if (result.vapor.detected) {
errors.push(
new ConsensusOnVaporError(
'Consensus on vapor detected: agent claims completion but deliverables missing',
{
missingDeliverables: result.vapor.missingDeliverables,
confidence: result.vapor.confidence,
}
)
);
result.passed = false;
}
} catch (error) {
errors.push(
new ValidationError(
`Vapor detection failed: ${error}`,
'VAPOR_DETECTION_FAILED'
)
);
}
}
return result;
}
}
export default CFNValidator;