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.

633 lines (567 loc) 17.2 kB
/** * Unified Output Processing Module for CFN Loop * Consolidates Loop 2 (validators) and Loop 3 (implementers) output parsing * * Purpose: * - Extract confidence scores from agent outputs * - Parse feedback/issues from validator outputs * - Validate parsed results against schema * - Calculate consensus from multiple validator results * - Verify deliverables from implementation results * * Replaces: * - .claude/skills/cfn-loop2-output-processing/parse-feedback.sh * - .claude/skills/cfn-loop3-output-processing/parse-confidence.sh * - .claude/skills/cfn-loop3-output-processing/calculate-confidence.sh */ /** * Core type definitions for output processing */ /** * Result from Loop 3 (Implementer) output processing * Represents work completed by coding/development agents */ export interface Loop3Result { agentId: string; confidence: number; confidenceSource: 'explicit' | 'calculated' | 'fallback'; filesChanged: number; deliverables: string[]; testsPassedCount?: number; testsFailed?: number; passRate?: number; output: string; iteration: number; timestamp: string; } /** * Feedback item categorized by severity */ export interface FeedbackItem { severity: 'CRITICAL' | 'WARNING' | 'SUGGESTION'; text: string; } /** * Result from Loop 2 (Validator) output processing * Represents validation/review feedback from reviewers and testers */ export interface Loop2Result { validatorId: string; score: number; scoreSource: 'explicit' | 'calculated' | 'qualitative'; issues: FeedbackItem[]; criticalCount: number; warningCount: number; suggestionCount: number; recommendations: string[]; output: string; iteration: number; timestamp: string; } /** * Consensus calculation from multiple Loop 2 validators */ export interface ConsensusResult { averageScore: number; threshold: number; passed: boolean; validatorCount: number; scoredCount: number; minScore: number; maxScore: number; summary: string; details: { criticalIssuesTotal: number; warningIssuesTotal: number; suggestionsTotal: number; }; } /** * Configuration for parsing behavior */ export interface ParsingConfig { strictMode: boolean; fallbackConfidence: number; minimumDeliverables: number; confidenceRange: [number, number]; } /** * Confidence extraction patterns * Priority order matters - first match wins */ const CONFIDENCE_PATTERNS = [ { name: 'explicit_header', regex: /Validation Confidence:\s*([0-9.]+)/i, priority: 1, }, { name: 'explicit_generic', regex: /[Cc]onfidence:\s*([0-9.]+)/, priority: 2, }, { name: 'score_field', regex: /[Ss]core:\s*([0-9.]+)/, priority: 3, }, { name: 'percentage', regex: /([0-9]{1,3})%/, priority: 4, transform: (match: string) => parseFloat(match) / 100, }, { name: 'parentheses', regex: /\(([0-9.]+)\)/, priority: 5, }, ]; /** * Qualitative confidence mappings */ const QUALITATIVE_CONFIDENCE: Record<string, number> = { 'very high': 0.95, 'extremely high': 0.95, 'excellent': 0.95, 'outstanding': 0.95, 'high': 0.90, 'strong': 0.90, 'good': 0.75, 'moderate': 0.75, 'medium': 0.75, 'fair': 0.60, 'low': 0.50, 'weak': 0.50, 'poor': 0.40, 'very low': 0.30, }; /** * Parse confidence score from agent output * Uses multi-pattern approach with fallbacks * * @param output Agent output text * @param config Parsing configuration * @returns Confidence score (0.0-1.0) and source */ export function parseConfidence( output: string, config: Partial<ParsingConfig> = {} ): { score: number; source: 'explicit' | 'qualitative' | 'none' } { if (!output || output.trim().length === 0) { return { score: 0.0, source: 'none' }; } const normalizedOutput = output.toLowerCase(); // Try explicit numeric patterns for (const pattern of CONFIDENCE_PATTERNS) { const match = output.match(pattern.regex); if (match && match[1]) { let score = parseFloat(match[1]); // Transform if needed (e.g., percentage to decimal) if (pattern.transform) { score = pattern.transform(match[1]); } // Validate range if (score >= 0.0 && score <= 1.0) { return { score, source: 'explicit' }; } // Handle percentage that might be > 1.0 if (score > 1.0 && score <= 100.0) { return { score: score / 100, source: 'explicit' }; } } } // Try qualitative mappings for (const [qualifier, score] of Object.entries(QUALITATIVE_CONFIDENCE)) { if ( normalizedOutput.includes(qualifier) && normalizedOutput.includes('confidence') ) { return { score, source: 'qualitative' }; } } return { score: 0.0, source: 'none' }; } /** * Extract feedback items from validator output * Looks for structured sections (### CRITICAL, ### WARNING, ### SUGGESTION) * * @param output Validator output text * @returns Categorized feedback items */ export function extractFeedback(output: string): FeedbackItem[] { if (!output || output.trim().length === 0) { return []; } const feedbackItems: FeedbackItem[] = []; const severities = ['CRITICAL', 'WARNING', 'SUGGESTION'] as const; for (const severity of severities) { // Pattern 1: Look for markdown sections (### SEVERITY Issues/Items) const sectionRegex = new RegExp( `### ${severity}[^\\n]*\\n([\\s\\S]*?)(?=###|$)`, 'i' ); const sectionMatch = output.match(sectionRegex); if (sectionMatch && sectionMatch[1]) { const content = sectionMatch[1]; // Extract bullet points const itemRegex = /^[-*•]\s+(.+)$/gm; let itemMatch; while ((itemMatch = itemRegex.exec(content)) !== null) { const text = itemMatch[1].trim(); if (text && text.toLowerCase() !== 'no issues found') { feedbackItems.push({ severity, text, }); } } } // Pattern 2: Look for inline format (SEVERITY: item) if (feedbackItems.length === 0) { const inlineRegex = new RegExp( `^${severity}:?\\s+(.+)$`, 'gim' ); let inlineMatch; while ((inlineMatch = inlineRegex.exec(output)) !== null) { const text = inlineMatch[1].trim(); if (text && text.toLowerCase() !== 'none') { feedbackItems.push({ severity, text, }); } } } } return feedbackItems; } /** * Extract recommendations from validator output * Looks for "Recommendations:" or "Suggestions:" sections * * @param output Validator output text * @returns List of recommendations */ export function extractRecommendations(output: string): string[] { if (!output || output.trim().length === 0) { return []; } const recommendations: string[] = []; // Pattern 1: Look for "Recommendations:" or "Suggestions:" section const recRegex = /(?:Recommendations?|Suggestions?):\s*\n([\s\S]*?)(?=\n\n|$)/i; const recMatch = output.match(recRegex); if (recMatch && recMatch[1]) { const content = recMatch[1]; const itemRegex = /^[-*•]\s+(.+)$/gm; let itemMatch; while ((itemMatch = itemRegex.exec(content)) !== null) { const text = itemMatch[1].trim(); if (text) { recommendations.push(text); } } } return recommendations; } /** * Verify deliverables and calculate confidence fallback for Loop 3 * Used when no explicit confidence is found * * @param filesChanged Number of files changed * @param deliverables List of changed files * @param testsInfo Optional test results * @returns Calculated confidence score */ export function calculateFallbackConfidence( filesChanged: number, deliverables: string[] = [], testsInfo?: { passed: number; failed: number } ): number { // No files changed = no delivery if (filesChanged === 0) { return 0.0; } // Minimal changes if (filesChanged <= 2) { return 0.5; } // Moderate changes if (filesChanged <= 5) { return 0.75; } // Significant changes let confidence = 0.85; // Boost if tests provided and passed if (testsInfo) { const totalTests = testsInfo.passed + testsInfo.failed; if (totalTests > 0) { const passRate = testsInfo.passed / totalTests; if (passRate === 1.0) { confidence = 0.95; } else if (passRate >= 0.9) { confidence = 0.90; } else if (passRate >= 0.8) { confidence = 0.85; } } } return confidence; } /** * Validate confidence score is in valid range * * @param score Confidence score to validate * @param min Minimum valid score (default 0.0) * @param max Maximum valid score (default 1.0) * @returns true if score is valid */ export function isValidConfidence( score: number, min: number = 0.0, max: number = 1.0 ): boolean { return !isNaN(score) && score >= min && score <= max; } /** * Parse complete Loop 3 agent output * Extracts confidence and deliverable information * * @param agentOutput Raw agent output * @param agentId Agent identifier * @param iteration Current iteration number * @param gitStatus Optional git status information * @returns Parsed Loop 3 result */ export function parseLoop3Output( agentOutput: string, agentId: string, iteration: number = 1, gitStatus?: { before: string; after: string; } ): Loop3Result { const { score: confidenceScore, source: confidenceSource } = parseConfidence( agentOutput ); let filesChanged = 0; let deliverables: string[] = []; // Calculate deliverables from git status if provided if (gitStatus?.before && gitStatus?.after) { const beforeLines = gitStatus.before.split('\n').filter((l) => l.trim()); const afterLines = gitStatus.after.split('\n').filter((l) => l.trim()); // Simple set difference - files in after but not in before const changedSet = new Set(afterLines); beforeLines.forEach((line) => changedSet.delete(line)); deliverables = Array.from(changedSet); filesChanged = deliverables.length; } // Determine final confidence let finalConfidence = confidenceScore; let finalSource: Loop3Result['confidenceSource'] = confidenceSource as any; if (confidenceScore === 0.0) { // No explicit confidence found, use fallback finalConfidence = calculateFallbackConfidence(filesChanged, deliverables); finalSource = 'calculated'; } // Extract test information if present const testPassRegex = /tests?\s+passed:\s*(\d+)/i; const testFailRegex = /tests?\s+failed:\s*(\d+)/i; const testPassMatch = agentOutput.match(testPassRegex); const testFailMatch = agentOutput.match(testFailRegex); const testsPassedCount = testPassMatch ? parseInt(testPassMatch[1], 10) : undefined; const testsFailed = testFailMatch ? parseInt(testFailMatch[1], 10) : undefined; let passRate: number | undefined; if (testsPassedCount !== undefined && testsFailed !== undefined) { const total = testsPassedCount + testsFailed; passRate = total > 0 ? testsPassedCount / total : 0; } return { agentId, confidence: parseFloat(finalConfidence.toFixed(2)), confidenceSource: finalSource, filesChanged, deliverables, testsPassedCount, testsFailed, passRate, output: agentOutput, iteration, timestamp: new Date().toISOString(), }; } /** * Parse complete Loop 2 validator output * Extracts confidence and feedback * * @param validatorOutput Raw validator output * @param validatorId Validator identifier * @param iteration Current iteration number * @returns Parsed Loop 2 result */ export function parseLoop2Output( validatorOutput: string, validatorId: string, iteration: number = 1 ): Loop2Result { const { score, source: scoreSource } = parseConfidence(validatorOutput); const feedbackItems = extractFeedback(validatorOutput); const recommendations = extractRecommendations(validatorOutput); // Count by severity const criticalCount = feedbackItems.filter( (f) => f.severity === 'CRITICAL' ).length; const warningCount = feedbackItems.filter( (f) => f.severity === 'WARNING' ).length; const suggestionCount = feedbackItems.filter( (f) => f.severity === 'SUGGESTION' ).length; // Validate score let finalScore = score; let finalSource = scoreSource; if (!isValidConfidence(finalScore)) { finalScore = 0.0; finalSource = 'calculated'; } return { validatorId, score: parseFloat(finalScore.toFixed(2)), scoreSource: finalSource as any, issues: feedbackItems, criticalCount, warningCount, suggestionCount, recommendations, output: validatorOutput, iteration, timestamp: new Date().toISOString(), }; } /** * Calculate consensus from multiple Loop 2 validator results * Determines if validators agree on code quality * * @param results Array of Loop 2 results * @param threshold Minimum average score to pass (default 0.70) * @returns Consensus assessment */ export function calculateConsensus( results: Loop2Result[], threshold: number = 0.70 ): ConsensusResult { if (results.length === 0) { return { averageScore: 0.0, threshold, passed: false, validatorCount: 0, scoredCount: 0, minScore: 0.0, maxScore: 0.0, summary: 'No validators provided', details: { criticalIssuesTotal: 0, warningIssuesTotal: 0, suggestionsTotal: 0, }, }; } // Filter valid scores const validResults = results.filter((r) => isValidConfidence(r.score)); if (validResults.length === 0) { return { averageScore: 0.0, threshold, passed: false, validatorCount: results.length, scoredCount: 0, minScore: 0.0, maxScore: 0.0, summary: 'No valid confidence scores from validators', details: { criticalIssuesTotal: results.reduce((sum, r) => sum + r.criticalCount, 0), warningIssuesTotal: results.reduce((sum, r) => sum + r.warningCount, 0), suggestionsTotal: results.reduce((sum, r) => sum + r.suggestionCount, 0), }, }; } const scores = validResults.map((r) => r.score); const averageScore = scores.reduce((a, b) => a + b, 0) / scores.length; const minScore = Math.min(...scores); const maxScore = Math.max(...scores); const passed = averageScore >= threshold; // Calculate total issues const criticalIssuesTotal = results.reduce((sum, r) => sum + r.criticalCount, 0); const warningIssuesTotal = results.reduce((sum, r) => sum + r.warningCount, 0); const suggestionsTotal = results.reduce((sum, r) => sum + r.suggestionCount, 0); // Build summary const roundedAverage = parseFloat(averageScore.toFixed(2)); const consensusPercentage = Math.round(averageScore * 100); const decision = passed ? 'PASS' : 'FAIL'; let issues = []; if (criticalIssuesTotal > 0) { issues.push(`${criticalIssuesTotal} critical`); } if (warningIssuesTotal > 0) { issues.push(`${warningIssuesTotal} warnings`); } const issueString = issues.length > 0 ? ` (${issues.join(', ')})` : ''; const summary = `${decision}: ${consensusPercentage}% consensus from ${validResults.length} validators${issueString}`; return { averageScore: roundedAverage, threshold, passed, validatorCount: results.length, scoredCount: validResults.length, minScore: parseFloat(minScore.toFixed(2)), maxScore: parseFloat(maxScore.toFixed(2)), summary, details: { criticalIssuesTotal, warningIssuesTotal, suggestionsTotal, }, }; } /** * Detect if output appears to be default/unprocessed * (e.g., 0.70 confidence with no feedback) * * @param result Loop 2 result to check * @returns true if output appears to be default */ export function isDefaultOutput(result: Loop2Result): boolean { return ( result.score === 0.7 && result.issues.length === 0 && result.recommendations.length === 0 && result.scoreSource === 'explicit' ); } /** * Format result as JSON for CLI output * * @param data Result object to format * @returns JSON string */ export function formatAsJson<T>(data: T): string { return JSON.stringify(data, null, 2); } /** * Parse JSON string safely * * @param json JSON string to parse * @returns Parsed object or null if invalid */ export function parseJson<T>(json: string): T | null { try { return JSON.parse(json) as T; } catch { return null; } }