claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.
312 lines (311 loc) • 10.8 kB
JavaScript
/**
* Skill Output Parser
* Task 5.4: Eliminate Bash Output Parsing
*
* Parses structured JSON outputs from skill execution with legacy text fallback
*
* @version 1.0.0
*/ // ============================================================================
// Parser Class
// ============================================================================
/**
* SkillOutputParser: Parses skill execution outputs
*
* Primary path: JSON parsing with schema validation
* Fallback path: Legacy text parsing
*/ export class SkillOutputParser {
config;
constructor(config = {}){
this.config = {
enableLegacyParsing: config.enableLegacyParsing ?? true,
defaultConfidence: config.defaultConfidence ?? 0.5,
strictValidation: config.strictValidation ?? false
};
}
// ==========================================================================
// Public API
// ==========================================================================
/**
* Parse skill output (JSON or legacy text)
*
* @param output - Raw skill output string
* @returns Parse result
*/ parse(output) {
const errors = [];
const warnings = [];
// Handle empty input
if (!output || output.trim().length === 0) {
warnings.push('Empty input received');
return this.createLegacyParseResult(output, warnings);
}
// Try JSON parsing first
const jsonResult = this.tryParseJSON(output);
if (jsonResult.success && jsonResult.data) {
// Validate JSON schema
const validationResult = this.validateSkillOutput(jsonResult.data);
if (validationResult.valid) {
return {
success: true,
output: jsonResult.data,
parseMethod: 'json',
confidence: 0.95,
errors: [],
warnings: this.config.strictValidation ? validationResult.warnings : []
};
} else {
// JSON parsed but validation failed - don't fall back to legacy
// because JSON structure exists, it's just invalid
errors.push(...validationResult.errors);
return {
success: false,
output: this.createDefaultOutput(),
parseMethod: 'validation_failed',
confidence: 0.0,
errors,
warnings
};
}
}
// JSON parsing failed
if (this.config.enableLegacyParsing) {
warnings.push('Using legacy text parsing (consider migrating to JSON output)');
return this.createLegacyParseResult(output, warnings);
} else {
errors.push('JSON parsing failed and legacy parsing disabled');
return {
success: false,
output: this.createDefaultOutput(),
parseMethod: 'validation_failed',
confidence: 0.0,
errors,
warnings
};
}
}
/**
* Parse multiple skill outputs in batch
*
* @param outputs - Array of raw skill output strings
* @returns Array of parse results
*/ parseBatch(outputs) {
return outputs.map((output)=>this.parse(output));
}
// ==========================================================================
// JSON Parsing
// ==========================================================================
/**
* Try to parse output as JSON
*
* @param output - Raw output string
* @returns Parse result with success flag and data
*/ tryParseJSON(output) {
try {
// Try to extract JSON from mixed output
const jsonMatch = output.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const data = JSON.parse(jsonMatch[0]);
return {
success: true,
data
};
}
// Try parsing entire output
const data = JSON.parse(output);
return {
success: true,
data
};
} catch (error) {
return {
success: false
};
}
}
/**
* Validate skill output against schema
*
* @param data - Parsed JSON data
* @returns Validation result
*/ validateSkillOutput(data) {
const errors = [];
const warnings = [];
// Required fields
if (typeof data.success !== 'boolean') {
errors.push('Missing required field: success (must be boolean)');
}
if (typeof data.confidence !== 'number') {
errors.push('Missing required field: confidence (must be number)');
} else if (data.confidence < 0.0 || data.confidence > 1.0) {
errors.push('Confidence must be between 0.0 and 1.0');
}
if (!Array.isArray(data.deliverables)) {
errors.push('Deliverables must be an array');
}
if (typeof data.metrics !== 'object' || data.metrics === null || Array.isArray(data.metrics)) {
errors.push('Metrics must be an object');
}
if (!Array.isArray(data.errors)) {
errors.push('Errors must be an array');
}
// Strict mode: check for unexpected fields
if (this.config.strictValidation) {
const allowedFields = new Set([
'success',
'confidence',
'deliverables',
'metrics',
'errors'
]);
const actualFields = Object.keys(data);
const unexpectedFields = actualFields.filter((field)=>!allowedFields.has(field));
if (unexpectedFields.length > 0) {
warnings.push(`Unexpected fields in output: ${unexpectedFields.join(', ')}`);
}
}
return {
valid: errors.length === 0,
errors,
warnings
};
}
// ==========================================================================
// Legacy Parsing
// ==========================================================================
/**
* Parse legacy text output
*
* @param output - Raw text output
* @param warnings - Existing warnings to include
* @returns Parse result
*/ createLegacyParseResult(output, warnings = []) {
// Special handling for empty input
const isEmpty = !output || output.trim().length === 0;
const parsedOutput = {
success: isEmpty ? false : this.detectSuccess(output),
confidence: isEmpty ? 0.0 : this.extractConfidence(output),
deliverables: this.extractDeliverables(output),
metrics: this.extractMetrics(output),
errors: this.extractErrors(output)
};
return {
success: true,
output: parsedOutput,
parseMethod: 'legacy',
confidence: parsedOutput.confidence,
errors: [],
warnings
};
}
/**
* Detect success from text patterns
*/ detectSuccess(output) {
const successPatterns = [
/SUCCESS/i,
/complete(d)?/i,
/passed/i,
/\[OK\]/i,
/✓/
];
const failurePatterns = [
/ERROR/i,
/FAILED/i,
/ABORT/i,
/\[FAIL\]/i,
/✗/
];
// Check for explicit failure first
for (const pattern of failurePatterns){
if (pattern.test(output)) {
return false;
}
}
// Check for success patterns
for (const pattern of successPatterns){
if (pattern.test(output)) {
return true;
}
}
// Default: assume success if no clear failure
return output.trim().length > 0;
}
/**
* Extract confidence from text
*/ extractConfidence(output) {
// Look for explicit confidence marker
const confidenceMatch = output.match(/confidence:?\s*(0?\.\d+|1\.0|0|1)/i);
if (confidenceMatch) {
return parseFloat(confidenceMatch[1]);
}
// Return default confidence
return this.config.defaultConfidence;
}
/**
* Extract deliverables from text
*/ extractDeliverables(output) {
const deliverables = [];
// Look for file patterns
const patterns = [
/(?:created|modified|updated|generated):?\s+([^\s\n]+)/gi,
/(?:file|path):?\s+([^\s\n]+)/gi
];
for (const pattern of patterns){
let match;
while((match = pattern.exec(output)) !== null){
const file = match[1].trim();
if (file && !deliverables.includes(file)) {
deliverables.push(file);
}
}
}
return deliverables;
}
/**
* Extract metrics from text
*/ extractMetrics(output) {
const metrics = {};
// Extract execution time
const timeMatch = output.match(/(?:execution\s+time|duration|took):?\s*(\d+)\s*ms/i);
if (timeMatch) {
metrics.execution_time_ms = parseInt(timeMatch[1], 10);
}
// Extract file count
const fileCountMatch = output.match(/(?:modified|created|updated)\s+(\d+)\s+files?/i);
if (fileCountMatch) {
metrics.files_modified = parseInt(fileCountMatch[1], 10);
}
return metrics;
}
/**
* Extract errors from text
*/ extractErrors(output) {
const errors = [];
// Look for error patterns
const errorMatches = output.matchAll(/ERROR:?\s*([^\n]+)/gi);
for (const match of errorMatches){
errors.push({
code: 'LEGACY_ERROR',
message: match[1].trim()
});
}
return errors;
}
// ==========================================================================
// Utilities
// ==========================================================================
/**
* Create default empty output
*/ createDefaultOutput() {
return {
success: false,
confidence: 0.0,
deliverables: [],
metrics: {},
errors: []
};
}
}
// ============================================================================
// Exports
// ============================================================================
export default SkillOutputParser;
//# sourceMappingURL=skill-output-parser.js.map