@wiber/ccs
Version:
Turn any codebase into an AI-aware environment. Claude launches with full context, asks smart questions, and gets better with every interaction.
522 lines (436 loc) • 17.9 kB
JavaScript
/**
* Learning Engine - Recursive Self-Improvement for ccs
*
* This engine tracks operation performance, analyzes patterns,
* and autonomously optimizes prompts and configurations for better results.
* Inspired by ThetaDriven's biz-learning-store.js but extended for all operations.
*/
const fs = require('fs').promises;
const path = require('path');
class LearningEngine {
constructor(projectPath) {
this.projectPath = projectPath;
this.learningPath = path.join(projectPath, '.ccs', 'learnings');
this.feedbackPath = path.join(projectPath, '.ccs', 'feedback');
// Learning database structure
this.learningDb = {
version: '1.0.0',
created: new Date().toISOString(),
operations: {},
patterns: {
promptEffectiveness: {},
taskSplitSuccess: {},
timingOptimization: {},
errorRecovery: {},
contextRelevance: {}
},
evolution: {
promptTemplates: {},
configOptimizations: [],
performanceMetrics: {}
},
feedback: {
userRatings: [],
autoMetrics: [],
ciResults: []
}
};
}
async initialize() {
// Ensure directories exist
await fs.mkdir(this.learningPath, { recursive: true });
await fs.mkdir(this.feedbackPath, { recursive: true });
// Load existing learning data
await this.load();
console.log('🧠 Learning engine initialized');
console.log(` 📊 ${Object.keys(this.learningDb.operations).length} operations tracked`);
console.log(` 🔄 ${this.learningDb.evolution.configOptimizations.length} optimizations applied`);
console.log(` 👍 ${this.learningDb.feedback.userRatings.length} user ratings collected`);
}
async load() {
try {
const dbPath = path.join(this.learningPath, 'learning-database.json');
const data = await fs.readFile(dbPath, 'utf-8');
this.learningDb = { ...this.learningDb, ...JSON.parse(data) };
} catch (error) {
// New learning database - create with defaults
await this.save();
}
}
async save() {
const dbPath = path.join(this.learningPath, 'learning-database.json');
await fs.writeFile(dbPath, JSON.stringify(this.learningDb, null, 2));
}
/**
* Record operation execution for learning
*/
async recordOperation(operationName, executionData) {
if (!this.learningDb.operations[operationName]) {
this.learningDb.operations[operationName] = {
executions: [],
patterns: {},
performance: {
avgDuration: 0,
successRate: 0,
errorPatterns: {}
}
};
}
const operation = this.learningDb.operations[operationName];
const execution = {
id: executionData.id,
timestamp: executionData.timestamp || new Date().toISOString(),
duration: executionData.duration,
success: executionData.success,
context: executionData.context,
prompt: executionData.prompt,
output: executionData.output ? this.truncateOutput(executionData.output) : null,
errorType: executionData.error ? this.categorizeError(executionData.error) : null,
qualityScore: null, // To be filled by feedback
userRating: null
};
operation.executions.push(execution);
// Update performance metrics
await this.updatePerformanceMetrics(operationName);
// Analyze patterns
await this.analyzeExecutionPatterns(operationName, execution);
await this.save();
return execution.id;
}
/**
* Record user feedback for continuous improvement
*/
async recordFeedback(executionId, rating, comments = '') {
const feedback = {
executionId,
rating, // 1-5 scale
comments,
timestamp: new Date().toISOString()
};
this.learningDb.feedback.userRatings.push(feedback);
// Update execution record
for (const [operationName, operation] of Object.entries(this.learningDb.operations)) {
const execution = operation.executions.find(e => e.id === executionId);
if (execution) {
execution.userRating = rating;
execution.qualityScore = this.calculateQualityScore(execution, feedback);
// Trigger learning if we have enough feedback
if (operation.executions.filter(e => e.userRating !== null).length >= 5) {
await this.evolveOperation(operationName);
}
break;
}
}
await this.save();
}
/**
* Analyze patterns in successful vs failed executions
*/
async analyzeExecutionPatterns(operationName, execution) {
const operation = this.learningDb.operations[operationName];
// Context patterns
if (execution.context) {
const contextKeys = Object.keys(execution.context);
contextKeys.forEach(key => {
if (!this.learningDb.patterns.contextRelevance[key]) {
this.learningDb.patterns.contextRelevance[key] = {
appearances: 0,
successCorrelation: 0,
operations: {}
};
}
const pattern = this.learningDb.patterns.contextRelevance[key];
pattern.appearances++;
if (!pattern.operations[operationName]) {
pattern.operations[operationName] = { count: 0, successes: 0 };
}
pattern.operations[operationName].count++;
if (execution.success) {
pattern.operations[operationName].successes++;
pattern.successCorrelation += 1;
}
});
}
// Timing patterns
const hour = new Date(execution.timestamp).getHours();
const timingKey = `${operationName}_hour_${hour}`;
if (!this.learningDb.patterns.timingOptimization[timingKey]) {
this.learningDb.patterns.timingOptimization[timingKey] = {
executions: 0,
avgDuration: 0,
successRate: 0
};
}
const timing = this.learningDb.patterns.timingOptimization[timingKey];
timing.executions++;
timing.avgDuration = (timing.avgDuration * (timing.executions - 1) + execution.duration) / timing.executions;
if (execution.success) timing.successRate += 1/timing.executions;
}
/**
* Evolve operation based on learning patterns
*/
async evolveOperation(operationName) {
const operation = this.learningDb.operations[operationName];
const recentExecutions = operation.executions.slice(-20); // Last 20 executions
const successfulExecutions = recentExecutions.filter(e => e.success && e.userRating >= 4);
const failedExecutions = recentExecutions.filter(e => !e.success || e.userRating <= 2);
console.log(`🧠 Evolving ${operationName} based on ${recentExecutions.length} executions`);
// Analyze successful patterns
const improvements = {
promptOptimizations: [],
contextImprovements: [],
timingRecommendations: []
};
// Prompt analysis
if (successfulExecutions.length >= 3) {
const successfulPrompts = successfulExecutions.map(e => e.prompt).filter(Boolean);
const commonPatterns = this.extractCommonPatterns(successfulPrompts);
if (commonPatterns.length > 0) {
improvements.promptOptimizations.push({
type: 'successful_patterns',
patterns: commonPatterns,
confidence: successfulExecutions.length / recentExecutions.length
});
}
}
// Context analysis
const successfulContexts = successfulExecutions.map(e => e.context).filter(Boolean);
if (successfulContexts.length > 0) {
const contextPatterns = this.analyzeContextPatterns(successfulContexts);
improvements.contextImprovements.push(...contextPatterns);
}
// Timing analysis
const optimalTimes = this.findOptimalTiming(operationName);
if (optimalTimes.length > 0) {
improvements.timingRecommendations.push(...optimalTimes);
}
// Save improvements
if (Object.values(improvements).some(arr => arr.length > 0)) {
this.learningDb.evolution.configOptimizations.push({
operation: operationName,
timestamp: new Date().toISOString(),
improvements,
basedOnExecutions: recentExecutions.length,
successRate: successfulExecutions.length / recentExecutions.length
});
// Apply improvements
await this.applyImprovements(operationName, improvements);
}
await this.save();
}
/**
* Apply learned improvements to operation configuration
*/
async applyImprovements(operationName, improvements) {
// Update prompt templates
if (improvements.promptOptimizations.length > 0) {
const templatePath = path.join(this.learningPath, `${operationName}-optimized-prompt.md`);
const optimizedPrompt = this.generateOptimizedPrompt(operationName, improvements.promptOptimizations);
await fs.writeFile(templatePath, optimizedPrompt);
console.log(` 📝 Updated prompt template for ${operationName}`);
}
// Update context configuration
if (improvements.contextImprovements.length > 0) {
const contextPath = path.join(this.learningPath, `${operationName}-context-config.json`);
const contextConfig = {
recommendedContext: improvements.contextImprovements,
generatedAt: new Date().toISOString()
};
await fs.writeFile(contextPath, JSON.stringify(contextConfig, null, 2));
console.log(` 🎯 Updated context configuration for ${operationName}`);
}
// Update timing recommendations
if (improvements.timingRecommendations.length > 0) {
console.log(` ⏰ Optimal timing for ${operationName}:`, improvements.timingRecommendations.map(t => t.period).join(', '));
}
}
/**
* Get learning-optimized configuration for operation
*/
async getOptimizedConfig(operationName) {
const optimizedConfig = {
promptTemplate: null,
contextConfig: null,
timingRecommendations: [],
qualityThreshold: 4.0
};
try {
// Load optimized prompt template
const promptPath = path.join(this.learningPath, `${operationName}-optimized-prompt.md`);
try {
optimizedConfig.promptTemplate = await fs.readFile(promptPath, 'utf-8');
} catch (e) { /* No optimized prompt yet */ }
// Load context configuration
const contextPath = path.join(this.learningPath, `${operationName}-context-config.json`);
try {
const contextData = await fs.readFile(contextPath, 'utf-8');
optimizedConfig.contextConfig = JSON.parse(contextData);
} catch (e) { /* No optimized context yet */ }
// Get timing recommendations
optimizedConfig.timingRecommendations = this.findOptimalTiming(operationName);
} catch (error) {
console.warn(`⚠️ Could not load optimized config for ${operationName}:`, error.message);
}
return optimizedConfig;
}
/**
* Generate performance insights
*/
async generateInsights() {
const insights = {
operationPerformance: {},
globalPatterns: [],
recommendations: []
};
// Operation-specific insights
for (const [operationName, operation] of Object.entries(this.learningDb.operations)) {
const recentExecutions = operation.executions.slice(-10);
const successRate = recentExecutions.filter(e => e.success).length / recentExecutions.length;
const avgRating = recentExecutions
.filter(e => e.userRating !== null)
.reduce((sum, e) => sum + e.userRating, 0) / recentExecutions.filter(e => e.userRating !== null).length;
insights.operationPerformance[operationName] = {
successRate,
avgRating: avgRating || 0,
executions: operation.executions.length,
recentTrend: this.calculateTrend(operation.executions)
};
// Recommendations
if (successRate < 0.8) {
insights.recommendations.push(`${operationName}: Consider reviewing prompt template (${(successRate * 100).toFixed(1)}% success rate)`);
}
if (avgRating > 0 && avgRating < 3.5) {
insights.recommendations.push(`${operationName}: User satisfaction could be improved (${avgRating.toFixed(1)}/5 average rating)`);
}
}
// Global patterns
const bestPerformingHours = this.findBestPerformingTimes();
if (bestPerformingHours.length > 0) {
insights.globalPatterns.push(`Best performance typically occurs at: ${bestPerformingHours.join(', ')}`);
}
// Context patterns
const keyContextFactors = this.findKeyContextFactors();
if (keyContextFactors.length > 0) {
insights.globalPatterns.push(`Key success factors: ${keyContextFactors.join(', ')}`);
}
return insights;
}
// Helper methods
updatePerformanceMetrics(operationName) {
const operation = this.learningDb.operations[operationName];
const executions = operation.executions;
operation.performance.avgDuration = executions.reduce((sum, e) => sum + e.duration, 0) / executions.length;
operation.performance.successRate = executions.filter(e => e.success).length / executions.length;
}
truncateOutput(output) {
if (typeof output === 'string' && output.length > 1000) {
return output.substring(0, 1000) + '...[truncated]';
}
return output;
}
categorizeError(error) {
if (error.includes('timeout')) return 'timeout';
if (error.includes('network') || error.includes('connection')) return 'network';
if (error.includes('permission') || error.includes('auth')) return 'auth';
if (error.includes('not found')) return 'missing_resource';
return 'other';
}
calculateQualityScore(execution, feedback) {
let score = feedback.rating * 20; // Scale 1-5 to 20-100
// Adjust based on execution success
if (!execution.success) score *= 0.5;
// Adjust based on duration (faster is generally better)
if (execution.duration < 5000) score += 5; // Bonus for fast execution
else if (execution.duration > 30000) score -= 10; // Penalty for slow execution
return Math.max(0, Math.min(100, score));
}
extractCommonPatterns(prompts) {
// Simple pattern extraction - could be more sophisticated
const patterns = [];
const phrases = prompts.join(' ').toLowerCase().split(/[.!?]+/);
const phraseCounts = {};
phrases.forEach(phrase => {
const cleaned = phrase.trim();
if (cleaned.length > 10) {
phraseCounts[cleaned] = (phraseCounts[cleaned] || 0) + 1;
}
});
return Object.entries(phraseCounts)
.filter(([phrase, count]) => count >= 2)
.map(([phrase, count]) => ({ phrase, frequency: count }));
}
analyzeContextPatterns(contexts) {
const patterns = [];
const keyFrequency = {};
contexts.forEach(context => {
Object.keys(context).forEach(key => {
keyFrequency[key] = (keyFrequency[key] || 0) + 1;
});
});
return Object.entries(keyFrequency)
.filter(([key, count]) => count >= contexts.length * 0.6) // Present in 60%+ of successful executions
.map(([key, count]) => ({ contextKey: key, importance: count / contexts.length }));
}
findOptimalTiming(operationName) {
const timingPatterns = Object.entries(this.learningDb.patterns.timingOptimization)
.filter(([key]) => key.startsWith(operationName))
.map(([key, data]) => ({
period: key.split('_').slice(-1)[0],
successRate: data.successRate,
avgDuration: data.avgDuration,
executions: data.executions
}))
.filter(p => p.executions >= 3) // Require at least 3 data points
.sort((a, b) => b.successRate - a.successRate);
return timingPatterns.slice(0, 3); // Top 3 time periods
}
generateOptimizedPrompt(operationName, optimizations) {
return `# Optimized Prompt for ${operationName}
Generated: ${new Date().toISOString()}
Based on: ${optimizations.length} successful pattern(s)
## Learned Patterns
${optimizations.map(opt =>
`- ${opt.type}: ${opt.patterns.map(p => p.phrase || p).join(', ')}`
).join('\n')}
## Optimized Prompt Template
[This template will be dynamically generated based on successful patterns]
---
*Auto-generated by ccs learning engine*`;
}
calculateTrend(executions) {
if (executions.length < 5) return 'insufficient_data';
const recent = executions.slice(-5);
const older = executions.slice(-10, -5);
const recentSuccessRate = recent.filter(e => e.success).length / recent.length;
const olderSuccessRate = older.filter(e => e.success).length / older.length;
if (recentSuccessRate > olderSuccessRate + 0.1) return 'improving';
if (recentSuccessRate < olderSuccessRate - 0.1) return 'declining';
return 'stable';
}
findBestPerformingTimes() {
const hourlyStats = {};
Object.entries(this.learningDb.patterns.timingOptimization).forEach(([key, data]) => {
const hour = key.split('_').pop();
if (!hourlyStats[hour]) {
hourlyStats[hour] = { executions: 0, totalSuccess: 0 };
}
hourlyStats[hour].executions += data.executions;
hourlyStats[hour].totalSuccess += data.successRate * data.executions;
});
return Object.entries(hourlyStats)
.filter(([hour, stats]) => stats.executions >= 5)
.map(([hour, stats]) => ({ hour, successRate: stats.totalSuccess / stats.executions }))
.sort((a, b) => b.successRate - a.successRate)
.slice(0, 3)
.map(h => `${h.hour}:00`);
}
findKeyContextFactors() {
return Object.entries(this.learningDb.patterns.contextRelevance)
.filter(([key, data]) => data.appearances >= 5)
.map(([key, data]) => ({ key, successRate: data.successCorrelation / data.appearances }))
.sort((a, b) => b.successRate - a.successRate)
.slice(0, 5)
.map(f => f.key);
}
}
module.exports = LearningEngine;