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.
266 lines (265 loc) • 9.9 kB
JavaScript
/**
* Edge Case Deduplicator Service
*
* Deduplicates edge cases to prevent redundant tracking.
* Part of Task 1.5: MVP Edge Case Feedback Loop
*
* Features:
* - Similarity detection using Levenshtein distance
* - Pattern matching for error messages and stack traces
* - Configurable similarity threshold (default: 90%)
* - Occurrence count tracking
* - Smart deduplication based on skill + error type + message similarity
*
* Usage:
* const deduplicator = new EdgeCaseDeduplicator(dbService, logger);
* const isDuplicate = await deduplicator.deduplicateEdgeCase(edgeCase);
*/ import { createLogger } from '../lib/logging.js';
/**
* Edge case deduplicator service
*/ export class EdgeCaseDeduplicator {
dbService;
logger;
config;
constructor(dbService, logger, config){
this.dbService = dbService;
this.logger = logger || createLogger('edge-case-deduplicator');
// Set default config
this.config = {
similarityThreshold: config?.similarityThreshold ?? 0.90,
maxAgedays: config?.maxAgedays ?? 30,
maxCandidates: config?.maxCandidates ?? 50
};
}
/**
* Deduplicate edge case against existing records
*
* @param edgeCase - Edge case to deduplicate
* @returns True if duplicate found and handled, false otherwise
*/ async deduplicateEdgeCase(edgeCase) {
try {
// 1. Find similar failures
const similar = await this.findSimilarFailures(edgeCase);
if (similar.length === 0) {
this.logger.debug('No similar edge cases found', {
skill_id: edgeCase.skill_id,
error_type: edgeCase.error_type
});
return false;
}
this.logger.debug(`Found ${similar.length} similar edge cases`, {
skill_id: edgeCase.skill_id,
error_type: edgeCase.error_type
});
// 2. Calculate similarity scores
for (const candidate of similar){
const score = this.calculateSimilarity(edgeCase, candidate);
this.logger.debug('Similarity score calculated', {
candidate_id: candidate.id,
total_score: score.total,
threshold: this.config.similarityThreshold
});
// 3. If >threshold% similar, it's a duplicate
if (score.total >= this.config.similarityThreshold) {
this.logger.info('Duplicate edge case found', {
edge_case_id: edgeCase.id,
duplicate_of: candidate.id,
similarity_score: score.total
});
// Update existing edge case
await this.incrementOccurrenceCount(candidate.id);
return true;
}
}
// No duplicates found
return false;
} catch (error) {
this.logger.error('Failed to deduplicate edge case', error, {
edge_case_id: edgeCase.id
});
// Don't throw - allow edge case to be stored even if deduplication fails
return false;
}
}
/**
* Find similar failures based on skill and error type
*
* @param edgeCase - Edge case to find similar failures for
* @returns Array of similar edge cases
*/ async findSimilarFailures(edgeCase) {
const sqlite = this.dbService.getAdapter('sqlite');
// Calculate cutoff date
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.config.maxAgedays);
// Query for similar edge cases
const query = `
SELECT *
FROM edge_cases
WHERE skill_id = ?
AND error_type = ?
AND status = 'new'
AND last_seen >= ?
ORDER BY occurrence_count DESC, last_seen DESC
LIMIT ?
`;
const rows = await sqlite.raw(query, [
edgeCase.skill_id,
edgeCase.error_type,
cutoffDate.toISOString(),
this.config.maxCandidates
]);
if (!Array.isArray(rows)) {
return [];
}
return rows.map((row)=>this.mapRowToEdgeCase(row));
}
/**
* Calculate similarity between two edge cases
*
* Similarity breakdown:
* - Same skill + error type: 40%
* - Error message similarity: 30%
* - Stack trace similarity: 30%
*
* @param a - First edge case
* @param b - Second edge case
* @returns Similarity score (0.0 - 1.0)
*/ calculateSimilarity(a, b) {
let score = 0;
const breakdown = {
total: 0,
skillAndType: 0,
messageMatch: 0,
stackMatch: 0
};
// Same skill + error type = 40%
if (a.skill_id === b.skill_id && a.error_type === b.error_type) {
score += 0.40;
breakdown.skillAndType = 0.40;
}
// Similar error message = 30%
const messageSimilarity = this.computeLevenshteinSimilarity(a.error_message, b.error_message);
const messageScore = messageSimilarity * 0.30;
score += messageScore;
breakdown.messageMatch = messageScore;
// Similar stack trace = 30%
const stackA = a.stack_trace || '';
const stackB = b.stack_trace || '';
const stackSimilarity = this.computeLevenshteinSimilarity(stackA, stackB);
const stackScore = stackSimilarity * 0.30;
score += stackScore;
breakdown.stackMatch = stackScore;
breakdown.total = score;
return breakdown;
}
/**
* Compute Levenshtein similarity (normalized)
*
* @param a - First string
* @param b - Second string
* @returns Similarity score (0.0 - 1.0)
*/ computeLevenshteinSimilarity(a, b) {
if (a === b) return 1.0;
if (!a || !b) return 0.0;
const distance = this.levenshteinDistance(a, b);
const maxLength = Math.max(a.length, b.length);
if (maxLength === 0) return 1.0;
return 1.0 - distance / maxLength;
}
/**
* Calculate Levenshtein distance between two strings
*
* @param a - First string
* @param b - Second string
* @returns Edit distance
*/ levenshteinDistance(a, b) {
const matrix = [];
// Initialize matrix
for(let i = 0; i <= b.length; i++){
matrix[i] = [
i
];
}
for(let j = 0; j <= a.length; j++){
matrix[0][j] = j;
}
// Fill matrix
for(let i = 1; i <= b.length; i++){
for(let j = 1; j <= a.length; j++){
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 // deletion
);
}
}
}
return matrix[b.length][a.length];
}
/**
* Increment occurrence count for existing edge case
*
* @param edgeCaseId - Edge case ID
*/ async incrementOccurrenceCount(edgeCaseId) {
const sqlite = this.dbService.getAdapter('sqlite');
const query = `
UPDATE edge_cases
SET occurrence_count = occurrence_count + 1,
last_seen = CURRENT_TIMESTAMP
WHERE id = ?
`;
const result = await sqlite.raw(query, [
edgeCaseId
]);
this.logger.info('Incremented occurrence count', {
edge_case_id: edgeCaseId
});
}
/**
* Get deduplication statistics
*/ async getStats() {
const sqlite = this.dbService.getAdapter('sqlite');
// Total edge cases
const totalResult = await sqlite.raw('SELECT COUNT(*) as count FROM edge_cases WHERE status = "new"');
const totalEdgeCases = Array.isArray(totalResult) && totalResult[0] ? totalResult[0].count : 0;
// Unique skills
const uniqueResult = await sqlite.raw('SELECT COUNT(DISTINCT skill_id) as count FROM edge_cases WHERE status = "new"');
const uniqueSkills = Array.isArray(uniqueResult) && uniqueResult[0] ? uniqueResult[0].count : 0;
// Average occurrence count
const avgResult = await sqlite.raw('SELECT AVG(occurrence_count) as avg FROM edge_cases WHERE status = "new"');
const avgOccurrenceCount = Array.isArray(avgResult) && avgResult[0] ? avgResult[0].avg || 0 : 0;
// Most frequent failure
const frequentResult = await sqlite.raw(`SELECT id, skill_id, error_type, occurrence_count
FROM edge_cases
WHERE status = 'new'
ORDER BY occurrence_count DESC
LIMIT 1`);
const mostFrequentFailure = Array.isArray(frequentResult) && frequentResult[0] ? frequentResult[0] : null;
return {
totalEdgeCases,
uniqueSkills,
avgOccurrenceCount,
mostFrequentFailure
};
}
/**
* Map database row to EdgeCase object
*/ mapRowToEdgeCase(row) {
return {
id: row.id,
skill_id: row.skill_id,
error_type: row.error_type,
severity: row.severity,
error_message: row.error_message,
stack_trace: row.stack_trace,
input_context: row.input_context,
output_context: row.output_context,
first_seen: new Date(row.first_seen),
last_seen: new Date(row.last_seen),
occurrence_count: row.occurrence_count,
status: row.status,
metadata: row.metadata ? JSON.parse(row.metadata) : {}
};
}
}
//# sourceMappingURL=edge-case-deduplicator.js.map