@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
JavaScript
/**
* 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