UNPKG

@jmkim85/dev-flow-mcp

Version:

MCP-based Dev Flow - AI-powered development workflow management with 13 essential tools for TDD and context management

193 lines 8.17 kB
/** * Mistake Pattern Learning System * Replaces complex database-based mistake tracking with simple pattern matching */ import { promises as fs } from 'fs'; import { join } from 'path'; export class MistakeLearner { projectRoot; mistakesPath; constructor(projectRoot) { this.projectRoot = projectRoot; // 기존 '.solo' → '.devflow' 로 통일 this.mistakesPath = join(projectRoot, '.devflow', 'mistakes.json'); } async loadMistakes() { try { const data = await fs.readFile(this.mistakesPath, 'utf-8'); return JSON.parse(data); } catch (error) { return []; } } async saveMistakes(mistakes) { const dir = join(this.projectRoot, '.devflow'); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(this.mistakesPath, JSON.stringify(mistakes, null, 2)); } async checkForKnownMistakes(code) { const mistakes = await this.loadMistakes(); const warnings = []; // Sort mistakes by frequency (most frequent first) for better prioritization const sortedMistakes = mistakes.sort((a, b) => b.frequency - a.frequency); for (const mistake of sortedMistakes) { const matchScore = this.calculateMatchScore(code, mistake.pattern); // Use a lower threshold for real-time warnings to catch more potential issues if (matchScore >= 0.6) { const confidenceLevel = matchScore >= 0.8 ? 'HIGH' : 'MEDIUM'; warnings.push({ pattern: mistake.pattern, message: `⚠️ Known mistake pattern detected (${confidenceLevel} confidence, occurred ${mistake.frequency} times)`, solutions: mistake.solutions }); } } // Limit to top 3 warnings to avoid overwhelming the user return warnings.slice(0, 3); } /** * Calculate a more sophisticated match score between code and pattern */ calculateMatchScore(code, pattern) { const normalizedCode = code.toLowerCase().replace(/\s+/g, ' '); const normalizedPattern = pattern.toLowerCase().replace(/\s+/g, ' '); // Exact substring match gets highest score if (normalizedCode.includes(normalizedPattern)) { return 1.0; } // Keyword-based matching with weighted scoring const patternKeywords = this.extractKeywords(normalizedPattern); const codeKeywords = this.extractKeywords(normalizedCode); if (patternKeywords.length === 0) return 0; let matchingKeywords = 0; let totalWeight = 0; for (const keyword of patternKeywords) { const weight = this.getKeywordWeight(keyword); totalWeight += weight; if (codeKeywords.includes(keyword)) { matchingKeywords += weight; } } return totalWeight > 0 ? matchingKeywords / totalWeight : 0; } /** * Assign weights to keywords based on their importance for mistake detection */ getKeywordWeight(keyword) { // High-importance keywords for mistake patterns const highImportance = ['error', 'fail', 'bug', 'issue', 'problem', 'wrong', 'incorrect', 'missing']; const mediumImportance = ['test', 'function', 'method', 'class', 'variable', 'import', 'export']; if (highImportance.includes(keyword)) return 3.0; if (mediumImportance.includes(keyword)) return 2.0; return 1.0; } async recordMistake(pattern, context, solution) { const mistakes = await this.loadMistakes(); const existing = mistakes.find(m => this.isSimilarPattern(m.pattern, pattern)); if (existing) { existing.frequency++; existing.last_occurred = new Date().toISOString(); if (solution && !existing.solutions.includes(solution)) { existing.solutions.push(solution); } } else { mistakes.push({ id: this.generateId(), pattern, context, frequency: 1, last_occurred: new Date().toISOString(), solutions: solution ? [solution] : [] }); } await this.saveMistakes(mistakes); } async getRecentMistakes(limit = 5) { const mistakes = await this.loadMistakes(); return mistakes .sort((a, b) => new Date(b.last_occurred).getTime() - new Date(a.last_occurred).getTime()) .slice(0, limit); } async getFrequentMistakes(minFrequency = 2) { const mistakes = await this.loadMistakes(); return mistakes .filter(m => m.frequency >= minFrequency) .sort((a, b) => b.frequency - a.frequency); } matchesPattern(code, pattern) { // Simple pattern matching - can be enhanced with regex or fuzzy matching const normalizedCode = code.toLowerCase().replace(/\s+/g, ' '); const normalizedPattern = pattern.toLowerCase().replace(/\s+/g, ' '); // Check for exact substring match if (normalizedCode.includes(normalizedPattern)) { return true; } // Check for keyword-based matching const patternKeywords = this.extractKeywords(normalizedPattern); const codeKeywords = this.extractKeywords(normalizedCode); const matchingKeywords = patternKeywords.filter(keyword => codeKeywords.includes(keyword)); // Consider it a match if 70% of pattern keywords are present return matchingKeywords.length / patternKeywords.length >= 0.7; } isSimilarPattern(pattern1, pattern2) { const similarity = this.calculateSimilarity(pattern1, pattern2); return similarity >= 0.8; // 80% similarity threshold } calculateSimilarity(str1, str2) { const longer = str1.length > str2.length ? str1 : str2; const shorter = str1.length > str2.length ? str2 : str1; if (longer.length === 0) return 1.0; const editDistance = this.levenshteinDistance(longer, shorter); return (longer.length - editDistance) / longer.length; } levenshteinDistance(str1, str2) { const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null)); for (let i = 0; i <= str1.length; i++) matrix[0][i] = i; for (let j = 0; j <= str2.length; j++) matrix[j][0] = j; for (let j = 1; j <= str2.length; j++) { for (let i = 1; i <= str1.length; i++) { const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; matrix[j][i] = Math.min(matrix[j][i - 1] + 1, // deletion matrix[j - 1][i] + 1, // insertion matrix[j - 1][i - 1] + indicator // substitution ); } } return matrix[str2.length][str1.length]; } extractKeywords(text) { // Extract meaningful keywords from text const stopWords = new Set([ 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should' ]); return text .split(/\W+/) .filter(word => word.length > 2 && !stopWords.has(word)) .map(word => word.toLowerCase()); } generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } async getMistakeStats() { const mistakes = await this.loadMistakes(); const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const recent = mistakes.filter(m => new Date(m.last_occurred) > oneWeekAgo).length; const frequent = await this.getFrequentMistakes(3); return { total: mistakes.length, recent, frequent }; } } //# sourceMappingURL=mistake-learner.js.map