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.
378 lines (373 loc) • 13.8 kB
JavaScript
/**
* Patch Generator
* Part of Task 5.1: Edge Case Analyzer & Skill Patcher
*
* Generates simple patch templates for common failure patterns.
* Phase 1 supports basic patches: error handling, null checks, type validation, timeouts, file checks.
*
* Features:
* - Simple patch template generation
* - Confidence calculation (≥0.85 threshold)
* - Patch preview generation
* - Integration with DatabaseService
* - PENDING_UPDATE status for manual approval
* - Performance optimized (<1s for patch generation)
*
* Usage:
* const generator = new PatchGenerator({ dbPath: './patches.db' });
* const patch = generator.generatePatch(failure, category);
* if (patch.confidence >= 0.85) {
* const proposal = await generator.createPatchProposal(patch);
* }
*/ import * as crypto from 'crypto';
import Database from 'better-sqlite3';
import { createLogger } from '../lib/logging.js';
import { ErrorCode, createError } from '../lib/errors.js';
import { FailureCategory } from './edge-case-analyzer.js';
const logger = createLogger('patch-generator');
/**
* Patch type classification
*/ export var PatchType = /*#__PURE__*/ function(PatchType) {
PatchType["ADD_ERROR_HANDLING"] = "ADD_ERROR_HANDLING";
PatchType["ADD_NULL_CHECK"] = "ADD_NULL_CHECK";
PatchType["ADD_TYPE_VALIDATION"] = "ADD_TYPE_VALIDATION";
PatchType["ADD_TIMEOUT"] = "ADD_TIMEOUT";
PatchType["ADD_FILE_CHECK"] = "ADD_FILE_CHECK";
return PatchType;
}({});
/**
* Patch status
*/ export var PatchStatus = /*#__PURE__*/ function(PatchStatus) {
PatchStatus["PENDING_UPDATE"] = "PENDING_UPDATE";
PatchStatus["APPROVED"] = "APPROVED";
PatchStatus["DEPLOYED"] = "DEPLOYED";
PatchStatus["REJECTED"] = "REJECTED";
PatchStatus["ROLLED_BACK"] = "ROLLED_BACK";
return PatchStatus;
}({});
/**
* Patch Generator Service
*/ export class PatchGenerator {
db;
confidenceThreshold;
constructor(config){
this.db = new Database(config.dbPath);
this.confidenceThreshold = config.confidenceThreshold || 0.85;
this.initializeDatabase();
}
/**
* Initialize database schema
*/ initializeDatabase() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS skill_patches (
id TEXT PRIMARY KEY,
skill_id TEXT NOT NULL,
failure_id TEXT NOT NULL,
category TEXT NOT NULL,
patch_content TEXT NOT NULL,
confidence REAL NOT NULL,
status TEXT DEFAULT 'PENDING_UPDATE',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
approved_by TEXT,
deployed_at TEXT,
success INTEGER,
rollback_reason TEXT
);
CREATE INDEX IF NOT EXISTS idx_skill_patches_skill ON skill_patches(skill_id);
CREATE INDEX IF NOT EXISTS idx_skill_patches_status ON skill_patches(status);
CREATE INDEX IF NOT EXISTS idx_skill_patches_confidence ON skill_patches(confidence);
`);
}
/**
* Parse target file and line from stack trace
*/ parseStackTrace(stackTrace) {
// Try to extract file and line from stack trace
// Formats:
// - at functionName (file.ts:42:10)
// - at /absolute/path/file.ts:42:10
// - at functionName (/absolute/path/file.ts:42:10)
const patterns = [
/at\s+.*?\(([^:]+):(\d+):\d+\)/,
/at\s+([^:]+):(\d+):\d+/,
/\(([^:]+):(\d+):\d+\)/
];
for (const pattern of patterns){
const match = stackTrace.match(pattern);
if (match) {
const file = match[1].trim();
const line = parseInt(match[2], 10);
return {
file,
line
};
}
}
return {
file: 'unknown',
line: 0
};
}
/**
* Generate patch for failure
*
* Performance target: <1s
*/ generatePatch(failure, category) {
const startTime = Date.now();
const { file, line } = this.parseStackTrace(failure.stackTrace);
let patchType;
let content;
// Determine patch type and generate content based on failure category
if (category === FailureCategory.TIMEOUT) {
patchType = "ADD_TIMEOUT";
const timeout = failure.context.timeout || 5000;
content = `const result = await withTimeout(operation(), ${timeout});`;
} else if (category === FailureCategory.VALIDATION_ERROR && (failure.errorMessage.includes('null') || failure.errorMessage.includes('undefined'))) {
patchType = "ADD_NULL_CHECK";
const variableName = this.extractVariableName(failure.errorMessage);
content = `if (${variableName} === null || ${variableName} === undefined) {
throw new StandardError('NULL_VALUE', '${variableName} cannot be null or undefined');
}`;
} else if (category === FailureCategory.VALIDATION_ERROR && (failure.errorMessage.includes('Expected') || failure.errorMessage.includes('type'))) {
patchType = "ADD_TYPE_VALIDATION";
const expectedType = failure.context.expectedType || 'string';
const variableName = this.extractVariableName(failure.errorMessage);
content = `if (typeof ${variableName} !== '${expectedType}') {
throw new StandardError('INVALID_TYPE', 'Expected ${expectedType} but got ' + typeof ${variableName});
}`;
} else if (category === FailureCategory.LOGIC_ERROR && failure.errorMessage.includes('File not found')) {
patchType = "ADD_FILE_CHECK";
const filePath = failure.context.filePath || 'filePath';
content = `if (!fs.existsSync(${filePath})) {
throw new StandardError('FILE_NOT_FOUND', \`File not found: \${${filePath}}\`);
}`;
} else {
// Default to error handling
patchType = "ADD_ERROR_HANDLING";
const operation = failure.context.operation || 'operation';
content = `try {
// existing code for ${operation}
} catch (error) {
logger.error('${operation} failed', error);
throw new StandardError('OPERATION_FAILED', '${operation} failed', {}, error);
}`;
}
const patch = {
id: crypto.randomUUID(),
failureId: failure.id,
skillId: failure.skillId,
type: patchType,
category,
content,
targetFile: file,
targetLine: line,
confidence: 0,
similarFailureCount: 0
};
logger.debug('Patch generated', {
patchType,
targetFile: file,
targetLine: line,
durationMs: Date.now() - startTime
});
return patch;
}
/**
* Extract variable name from error message
*/ extractVariableName(errorMessage) {
// Try to extract variable name from error messages like:
// - "Cannot read property 'x' of null"
// - "userData is null"
// - "Expected string but got number for field"
const patterns = [
/Cannot read property.*?of (\w+)/,
/(\w+) is (null|undefined)/,
/for (\w+)/,
/(\w+) cannot be/
];
for (const pattern of patterns){
const match = errorMessage.match(pattern);
if (match && match[1] !== 'null' && match[1] !== 'undefined') {
return match[1];
}
}
return 'value';
}
/**
* Calculate patch confidence
*
* Factors:
* - Number of similar failures (higher = more confident)
* - Patch type (some types are more reliable)
* - Category (some categories are easier to fix)
*
* Threshold: ≥0.85 for auto-approval consideration
*/ calculatePatchConfidence(patch) {
let confidence = 0.5; // Base confidence
// Similar failure count boost (up to +0.4)
const failureBoost = Math.min(0.4, patch.similarFailureCount * 0.04);
confidence += failureBoost;
// Patch type boost
if (patch.type === "ADD_ERROR_HANDLING" || patch.type === "ADD_NULL_CHECK") {
confidence += 0.1; // These are generally safe
}
if (patch.type === "ADD_FILE_CHECK" || patch.type === "ADD_TIMEOUT") {
confidence += 0.05; // Moderately safe
}
// Category boost
if (patch.category === FailureCategory.VALIDATION_ERROR) {
confidence += 0.05; // Validation fixes are usually straightforward
}
return Math.min(0.98, confidence);
}
/**
* Create patch proposal (requires confidence ≥ threshold)
*/ async createPatchProposal(patch) {
const startTime = Date.now();
// Calculate confidence if not set
if (patch.confidence === 0) {
patch.confidence = this.calculatePatchConfidence(patch);
}
// Enforce confidence threshold
if (patch.confidence < this.confidenceThreshold) {
throw createError(ErrorCode.VALIDATION_FAILED, `Patch confidence ${patch.confidence.toFixed(2)} below threshold ${this.confidenceThreshold}`, {
patchId: patch.id,
confidence: patch.confidence
});
}
// Generate preview
const preview = this.generatePatchPreview(patch);
// Store in database
this.db.prepare(`
INSERT INTO skill_patches (id, skill_id, failure_id, category, patch_content, confidence, status)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(patch.id, patch.skillId, patch.failureId, patch.category, patch.content, patch.confidence, "PENDING_UPDATE");
const proposal = {
patch,
status: "PENDING_UPDATE",
createdAt: new Date(),
preview
};
logger.info('Patch proposal created', {
patchId: patch.id,
confidence: patch.confidence,
status: "PENDING_UPDATE",
durationMs: Date.now() - startTime
});
return proposal;
}
/**
* Generate human-readable patch preview
*/ generatePatchPreview(patch) {
return `
Patch Preview
=============
ID: ${patch.id}
Type: ${patch.type}
Category: ${patch.category}
Skill: ${patch.skillId}
Confidence: ${patch.confidence.toFixed(2)}
Target:
File: ${patch.targetFile}
Line: ${patch.targetLine}
Patch Content:
${patch.content.split('\n').map((line)=>' ' + line).join('\n')}
Similar Failures: ${patch.similarFailureCount}
Status: Pending Manual Approval
`.trim();
}
/**
* Get patch proposal by ID
*/ getPatchProposal(patchId) {
const stmt = this.db.prepare('SELECT * FROM skill_patches WHERE id = ?');
const row = stmt.get(patchId);
if (!row) {
return undefined;
}
const patch = {
id: row.id,
failureId: row.failure_id,
skillId: row.skill_id,
type: row.category,
category: row.category,
content: row.patch_content,
targetFile: '',
targetLine: 0,
confidence: row.confidence,
similarFailureCount: 0
};
const proposal = {
patch,
status: row.status,
createdAt: new Date(row.created_at),
preview: this.generatePatchPreview(patch),
approvedBy: row.approved_by,
deployedAt: row.deployed_at ? new Date(row.deployed_at) : undefined,
success: row.success !== null ? Boolean(row.success) : undefined,
rollbackReason: row.rollback_reason
};
return proposal;
}
/**
* Get pending patches (optionally filtered by skill)
*/ getPendingPatches(skillId) {
let query = `
SELECT * FROM skill_patches
WHERE status = 'PENDING_UPDATE'
`;
const params = [];
if (skillId) {
query += ' AND skill_id = ?';
params.push(skillId);
}
query += ' ORDER BY confidence DESC';
const stmt = this.db.prepare(query);
const rows = stmt.all(...params);
return rows.map((row)=>{
const patch = {
id: row.id,
failureId: row.failure_id,
skillId: row.skill_id,
type: this.inferPatchType(row.patch_content),
category: row.category,
content: row.patch_content,
targetFile: '',
targetLine: 0,
confidence: row.confidence,
similarFailureCount: 0
};
return {
patch,
status: row.status,
createdAt: new Date(row.created_at),
preview: this.generatePatchPreview(patch),
approvedBy: row.approved_by,
deployedAt: row.deployed_at ? new Date(row.deployed_at) : undefined,
success: row.success !== null ? Boolean(row.success) : undefined,
rollbackReason: row.rollback_reason
};
});
}
/**
* Infer patch type from content
*/ inferPatchType(content) {
if (content.includes('withTimeout')) {
return "ADD_TIMEOUT";
}
if (content.includes('null') || content.includes('undefined')) {
return "ADD_NULL_CHECK";
}
if (content.includes('typeof')) {
return "ADD_TYPE_VALIDATION";
}
if (content.includes('fs.existsSync')) {
return "ADD_FILE_CHECK";
}
return "ADD_ERROR_HANDLING";
}
/**
* Close database connection
*/ close() {
this.db.close();
}
}
//# sourceMappingURL=patch-generator.js.map