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.

547 lines (542 loc) â€ĸ 20 kB
/** * Edge Case Tracker Service * * Comprehensive edge case tracking with feedback loop, deduplication, * expert notification, and analytics. * * Part of Phase 2, Task P2-2.1: Edge Case Feedback Loop */ import * as sqlite3 from 'better-sqlite3'; import { EdgeCaseDeduplicator } from '../lib/edge-case-deduplicator.js'; import { EdgeCaseType, EdgeCaseCategory, EdgeCasePriority, EdgeCaseStatus } from '../types/edge-case.js'; /** * Edge Case Tracker * * Manages the complete edge case feedback loop: * 1. Detection and recording with deduplication * 2. Priority scoring based on frequency, recency, severity, impact * 3. Expert notification queue with <1h SLA * 4. Workflow management (NEW → INVESTIGATING → RESOLVED → CLOSED) * 5. Resolution tracking and verification * 6. Analytics and reporting */ export class EdgeCaseTracker { db = null; deduplicator; config; constructor(config){ this.config = { autoCloseAfterDays: 7, notificationThrottleMinutes: 60, ...config }; this.deduplicator = new EdgeCaseDeduplicator(); } /** * Initialize the tracker (create database tables) */ async initialize() { this.db = new sqlite3.default(this.config.dbPath); // Execute migration to create tables const migration = ` CREATE TABLE IF NOT EXISTS edge_case_tracker ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), signature TEXT NOT NULL UNIQUE, type TEXT NOT NULL CHECK(type IN ( 'syntax_error', 'logic_error', 'timeout', 'data_validation', 'system_error' )), category TEXT NOT NULL CHECK(category IN ( 'skill_execution', 'database_operation', 'coordination', 'file_operation', 'api_call' )), priority TEXT NOT NULL CHECK(priority IN ('critical', 'high', 'medium', 'low')), context TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'new' CHECK(status IN ( 'new', 'investigating', 'resolved', 'closed', 'wont_fix' )), first_occurred DATETIME DEFAULT CURRENT_TIMESTAMP, last_occurred DATETIME DEFAULT CURRENT_TIMESTAMP, occurrence_count INTEGER DEFAULT 1, assigned_expert TEXT, investigation_started_at DATETIME, resolved_at DATETIME, resolution TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS idx_edge_case_tracker_signature ON edge_case_tracker(signature); CREATE INDEX IF NOT EXISTS idx_edge_case_tracker_priority_status ON edge_case_tracker(priority, status); CREATE TABLE IF NOT EXISTS edge_case_notifications ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), edge_case_id TEXT NOT NULL, priority TEXT NOT NULL, type TEXT NOT NULL, category TEXT NOT NULL, message TEXT NOT NULL, channel TEXT NOT NULL CHECK(channel IN ('slack', 'email')), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, sent_at DATETIME, delivery_status TEXT CHECK(delivery_status IN ('pending', 'sent', 'failed')), FOREIGN KEY (edge_case_id) REFERENCES edge_case_tracker(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_edge_case_notifications_pending ON edge_case_notifications(created_at, sent_at) WHERE sent_at IS NULL; `; this.db.exec(migration); } /** * Record an edge case with automatic deduplication * * Performance target: <100ms */ async recordEdgeCase(input) { if (!this.db) { throw new Error('EdgeCaseTracker not initialized. Call initialize() first.'); } // Generate signature for deduplication const signature = this.deduplicator.generateSignature(input); // Check for existing edge case with same signature const existing = this.db.prepare(` SELECT * FROM edge_case_tracker WHERE signature = ? `).get(signature); if (existing) { // Update existing edge case (increment count, update last_occurred) const updatedCount = existing.occurrence_count + 1; // Recalculate priority based on new occurrence count const updatedPriority = this.calculatePriority({ ...input, occurrenceCount: updatedCount, firstOccurred: new Date(existing.first_occurred), lastOccurred: new Date() }); this.db.prepare(` UPDATE edge_case_tracker SET occurrence_count = ?, last_occurred = CURRENT_TIMESTAMP, priority = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run(updatedCount, updatedPriority, existing.id); // Return updated edge case return this.getEdgeCase(existing.id); } else { // Create new edge case const id = this.generateId(); const priority = this.calculatePriority(input); this.db.prepare(` INSERT INTO edge_case_tracker ( id, signature, type, category, priority, context, status, first_occurred, last_occurred, occurrence_count ) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1) `).run(id, signature, input.type, input.category, priority, JSON.stringify(input.context), EdgeCaseStatus.NEW); // Queue notification await this.queueNotification(id, input.type, input.category, priority); // Return created edge case return this.getEdgeCase(id); } } /** * Calculate priority score based on frequency, recency, severity, impact * * Scoring algorithm: * - Frequency: occurrence_count > 100 (+30), > 10 (+20), > 1 (+10) * - Recency: < 1h (+20), < 24h (+10) * - Type severity: system_error (+20), logic_error (+10) * - Category impact: database_operation (+15), coordination (+10) * * Priority thresholds: * - CRITICAL: >= 60 * - HIGH: >= 40 * - MEDIUM: >= 20 * - LOW: < 20 */ calculatePriority(input) { let score = 0; // Frequency scoring const count = input.occurrenceCount || 1; if (count > 100) score += 30; else if (count > 10) score += 20; else if (count > 1) score += 10; // Recency scoring if (input.firstOccurred) { const hoursSinceFirst = (Date.now() - input.firstOccurred.getTime()) / 3600000; if (hoursSinceFirst < 1) score += 20; else if (hoursSinceFirst < 24) score += 10; } // Type severity scoring if (input.type === EdgeCaseType.SYSTEM_ERROR) score += 20; else if (input.type === EdgeCaseType.LOGIC_ERROR) score += 10; else if (input.type === EdgeCaseType.TIMEOUT) score += 5; // Category impact scoring if (input.category === EdgeCaseCategory.DATABASE_OPERATION) score += 15; else if (input.category === EdgeCaseCategory.COORDINATION) score += 10; else if (input.category === EdgeCaseCategory.API_CALL) score += 8; // Determine priority if (score >= 60) return EdgeCasePriority.CRITICAL; if (score >= 40) return EdgeCasePriority.HIGH; if (score >= 20) return EdgeCasePriority.MEDIUM; return EdgeCasePriority.LOW; } /** * Queue expert notification * * Notifications are queued but not sent immediately to allow throttling. * External service should process the queue periodically. */ async queueNotification(edgeCaseId, type, category, priority) { if (!this.db) return; // Check if notification already exists for this edge case const existing = this.db.prepare(` SELECT COUNT(*) as count FROM edge_case_notifications WHERE edge_case_id = ? AND sent_at IS NULL `).get(edgeCaseId); if (existing.count > 0) { // Don't duplicate notifications return; } // Determine notification channels based on priority const channels = []; if (priority === EdgeCasePriority.CRITICAL || priority === EdgeCasePriority.HIGH) { if (this.config.notificationConfig.slack?.enabled) { channels.push('slack'); } } if (this.config.notificationConfig.email?.enabled) { channels.push('email'); } // Create notification messages for (const channel of channels){ const message = this.formatNotificationMessage(type, category, priority); this.db.prepare(` INSERT INTO edge_case_notifications ( edge_case_id, priority, type, category, message, channel, delivery_status ) VALUES (?, ?, ?, ?, ?, ?, 'pending') `).run(edgeCaseId, priority, type, category, message, channel); } } /** * Format notification message */ formatNotificationMessage(type, category, priority) { const emoji = { [EdgeCasePriority.CRITICAL]: '🚨', [EdgeCasePriority.HIGH]: 'âš ī¸', [EdgeCasePriority.MEDIUM]: '⚡', [EdgeCasePriority.LOW]: 'â„šī¸' }; return `${emoji[priority]} Edge Case Detected\n` + `Priority: ${priority.toUpperCase()}\n` + `Type: ${type}\n` + `Category: ${category}\n` + `Please investigate and assign.`; } /** * Get edge case by ID */ getEdgeCase(id) { if (!this.db) return null; const row = this.db.prepare(` SELECT * FROM edge_case_tracker WHERE id = ? `).get(id); if (!row) return null; return this.rowToEdgeCase(row); } /** * Update edge case status */ async updateStatus(id, status, assignedExpert) { if (!this.db) return; const updates = [ 'status = ?' ]; const params = [ status ]; if (assignedExpert) { updates.push('assigned_expert = ?'); params.push(assignedExpert); } if (status === EdgeCaseStatus.INVESTIGATING) { updates.push('investigation_started_at = CURRENT_TIMESTAMP'); } if (status === EdgeCaseStatus.RESOLVED) { updates.push('resolved_at = CURRENT_TIMESTAMP'); } params.push(id); this.db.prepare(` UPDATE edge_case_tracker SET ${updates.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run(...params); } /** * Resolve edge case with resolution details */ async resolveEdgeCase(id, resolution) { if (!this.db) return; this.db.prepare(` UPDATE edge_case_tracker SET status = 'resolved', resolved_at = CURRENT_TIMESTAMP, resolution = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run(JSON.stringify(resolution), id); } /** * Get pending notifications */ async getPendingNotifications() { if (!this.db) return []; const rows = this.db.prepare(` SELECT * FROM edge_case_notifications WHERE sent_at IS NULL ORDER BY created_at ASC `).all(); return rows.map((row)=>({ id: row.id, edgeCaseId: row.edge_case_id, priority: row.priority, type: row.type, category: row.category, message: row.message, createdAt: new Date(row.created_at), sentAt: row.sent_at ? new Date(row.sent_at) : undefined, channel: row.channel })); } /** * Mark notification as sent */ async markNotificationSent(notificationId) { if (!this.db) return; this.db.prepare(` UPDATE edge_case_notifications SET sent_at = CURRENT_TIMESTAMP, delivery_status = 'sent' WHERE id = ? `).run(notificationId); } /** * Check for auto-close eligibility (7 days without recurrence) */ async checkAutoClose() { if (!this.db) return; const daysThreshold = this.config.autoCloseAfterDays || 7; this.db.prepare(` UPDATE edge_case_tracker SET status = 'closed', updated_at = CURRENT_TIMESTAMP WHERE status = 'resolved' AND julianday('now') - julianday(resolved_at) >= ? `).run(daysThreshold); } /** * Get top edge cases (for analytics) * * Performance target: <500ms */ async getTopEdgeCases(query = {}) { if (!this.db) return []; const limit = query.limit || 10; const orderBy = query.orderBy || 'frequency'; let orderClause = 'occurrence_count DESC'; if (orderBy === 'recent') orderClause = 'last_occurred DESC'; if (orderBy === 'priority') orderClause = ` CASE priority WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 WHEN 'low' THEN 4 END `; let whereClause = "status NOT IN ('closed', 'wont_fix')"; const params = []; if (query.category) { whereClause += ' AND category = ?'; params.push(query.category); } if (query.type) { whereClause += ' AND type = ?'; params.push(query.type); } if (query.status) { whereClause = whereClause.replace("status NOT IN ('closed', 'wont_fix')", 'status = ?'); params.push(query.status); } params.push(limit); const rows = this.db.prepare(` SELECT * FROM edge_case_tracker WHERE ${whereClause} ORDER BY ${orderClause} LIMIT ? `).all(...params); return rows.map((row)=>this.rowToEdgeCase(row)); } /** * Get edge cases by priority */ async getEdgeCasesByPriority(priority) { if (!this.db) return []; const rows = this.db.prepare(` SELECT * FROM edge_case_tracker WHERE priority = ? AND status NOT IN ('closed', 'wont_fix') ORDER BY last_occurred DESC `).all(priority); return rows.map((row)=>this.rowToEdgeCase(row)); } /** * Get edge cases grouped by category */ async getEdgeCasesByCategory() { if (!this.db) { return { [EdgeCaseCategory.SKILL_EXECUTION]: 0, [EdgeCaseCategory.DATABASE_OPERATION]: 0, [EdgeCaseCategory.COORDINATION]: 0, [EdgeCaseCategory.FILE_OPERATION]: 0, [EdgeCaseCategory.API_CALL]: 0 }; } const rows = this.db.prepare(` SELECT category, COUNT(*) as count FROM edge_case_tracker WHERE status NOT IN ('closed', 'wont_fix') GROUP BY category `).all(); const result = {}; for (const row of rows){ result[row.category] = row.count; } return result; } /** * Get analytics * * Performance target: <500ms */ async getAnalytics() { if (!this.db) { return { totalCases: 0, resolvedCases: 0, resolutionRate: 0, avgResolutionTimeHours: 0, casesByPriority: { [EdgeCasePriority.CRITICAL]: 0, [EdgeCasePriority.HIGH]: 0, [EdgeCasePriority.MEDIUM]: 0, [EdgeCasePriority.LOW]: 0 }, casesByCategory: { [EdgeCaseCategory.SKILL_EXECUTION]: 0, [EdgeCaseCategory.DATABASE_OPERATION]: 0, [EdgeCaseCategory.COORDINATION]: 0, [EdgeCaseCategory.FILE_OPERATION]: 0, [EdgeCaseCategory.API_CALL]: 0 }, casesByType: { [EdgeCaseType.SYNTAX_ERROR]: 0, [EdgeCaseType.LOGIC_ERROR]: 0, [EdgeCaseType.TIMEOUT]: 0, [EdgeCaseType.DATA_VALIDATION]: 0, [EdgeCaseType.SYSTEM_ERROR]: 0 }, topEdgeCases: [] }; } const stats = this.db.prepare(` SELECT COUNT(*) as total_cases, SUM(CASE WHEN status IN ('resolved', 'closed') THEN 1 ELSE 0 END) as resolved_cases, AVG(CASE WHEN resolved_at IS NOT NULL THEN (julianday(resolved_at) - julianday(first_occurred)) * 24 ELSE NULL END) as avg_resolution_hours FROM edge_case_tracker `).get(); const totalCases = stats.total_cases || 0; const resolvedCases = stats.resolved_cases || 0; const resolutionRate = totalCases > 0 ? resolvedCases / totalCases : 0; return { totalCases, resolvedCases, resolutionRate, avgResolutionTimeHours: stats.avg_resolution_hours || 0, casesByPriority: await this.getCasesByPriority(), casesByCategory: await this.getEdgeCasesByCategory(), casesByType: await this.getCasesByType(), topEdgeCases: (await this.getTopEdgeCases({ limit: 10 })).map((ec)=>({ id: ec.id, signature: ec.signature, type: ec.type, category: ec.category, occurrenceCount: ec.occurrenceCount, priority: ec.priority })) }; } /** * Get cases by priority (for analytics) */ async getCasesByPriority() { if (!this.db) { return { [EdgeCasePriority.CRITICAL]: 0, [EdgeCasePriority.HIGH]: 0, [EdgeCasePriority.MEDIUM]: 0, [EdgeCasePriority.LOW]: 0 }; } const rows = this.db.prepare(` SELECT priority, COUNT(*) as count FROM edge_case_tracker GROUP BY priority `).all(); const result = {}; for (const row of rows){ result[row.priority] = row.count; } return result; } /** * Get cases by type (for analytics) */ async getCasesByType() { if (!this.db) { return { [EdgeCaseType.SYNTAX_ERROR]: 0, [EdgeCaseType.LOGIC_ERROR]: 0, [EdgeCaseType.TIMEOUT]: 0, [EdgeCaseType.DATA_VALIDATION]: 0, [EdgeCaseType.SYSTEM_ERROR]: 0 }; } const rows = this.db.prepare(` SELECT type, COUNT(*) as count FROM edge_case_tracker GROUP BY type `).all(); const result = {}; for (const row of rows){ result[row.type] = row.count; } return result; } /** * Convert database row to EdgeCase object */ rowToEdgeCase(row) { return { id: row.id, signature: row.signature, type: row.type, category: row.category, priority: row.priority, context: JSON.parse(row.context), status: row.status, firstOccurred: new Date(row.first_occurred), lastOccurred: new Date(row.last_occurred), occurrenceCount: row.occurrence_count, assignedExpert: row.assigned_expert || undefined, resolvedAt: row.resolved_at ? new Date(row.resolved_at) : undefined, resolution: row.resolution || undefined }; } /** * Generate unique ID */ generateId() { return `edge-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Close database connection */ async close() { if (this.db) { this.db.close(); this.db = null; } } } //# sourceMappingURL=edge-case-tracker.js.map