claude-hooks/lib/shadow-learner.js

579 lines
16 KiB
JavaScript

/**
* Shadow Learner - Pattern learning and prediction system
* Node.js implementation
*/
const fs = require('fs-extra');
const path = require('path');
class ConfidenceCalculator {
/**
* Calculate confidence for command failure patterns
*/
static calculateCommandConfidence(successCount, failureCount, recencyFactor) {
const totalAttempts = successCount + failureCount;
if (totalAttempts === 0) return 0.0;
// Base confidence from failure rate
const failureRate = failureCount / totalAttempts;
// Sample size adjustment (more data = more confidence)
const sampleFactor = Math.min(1.0, totalAttempts / 10.0); // Plateau at 10 samples
// Time decay (recent failures are more relevant)
const confidence = failureRate * sampleFactor * (0.5 + 0.5 * recencyFactor);
return Math.min(0.99, Math.max(0.1, confidence)); // Clamp between 0.1 and 0.99
}
/**
* Calculate confidence for tool sequence patterns
*/
static calculateSequenceConfidence(successfulSequences, totalSequences) {
if (totalSequences === 0) return 0.0;
const successRate = successfulSequences / totalSequences;
const sampleFactor = Math.min(1.0, totalSequences / 5.0);
return successRate * sampleFactor;
}
}
class PatternMatcher {
constructor(db) {
this.db = db;
}
/**
* Find similar command patterns using fuzzy matching
*/
fuzzyCommandMatch(command, threshold = 0.8) {
const cmdTokens = command.toLowerCase().split(' ');
if (cmdTokens.length === 0) return [];
const baseCmd = cmdTokens[0];
const matches = [];
for (const pattern of Object.values(this.db.commandPatterns)) {
const patternCmd = (pattern.trigger.command || '').toLowerCase();
// Exact match
if (patternCmd === baseCmd) {
matches.push(pattern);
}
// Fuzzy match on command name
else if (this._similarity(patternCmd, baseCmd) > threshold) {
matches.push(pattern);
}
// Partial match (e.g., "pip3" matches "pip install")
else if (cmdTokens.some(token => patternCmd.includes(token))) {
matches.push(pattern);
}
}
return matches.sort((a, b) => b.confidence - a.confidence);
}
/**
* Match patterns based on current context
*/
contextPatternMatch(currentContext) {
const matches = [];
for (const pattern of Object.values(this.db.contextPatterns)) {
if (this._contextMatches(currentContext, pattern.trigger)) {
matches.push(pattern);
}
}
return matches.sort((a, b) => b.confidence - a.confidence);
}
/**
* Simple string similarity (Jaccard similarity)
*/
_similarity(str1, str2) {
const set1 = new Set(str1.split(''));
const set2 = new Set(str2.split(''));
const intersection = new Set([...set1].filter(x => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}
/**
* Check if current context matches trigger conditions
*/
_contextMatches(current, trigger) {
for (const [key, expectedValue] of Object.entries(trigger)) {
if (!(key in current)) return false;
const currentValue = current[key];
// Handle different value types
if (typeof expectedValue === 'string' && typeof currentValue === 'string') {
if (!currentValue.toLowerCase().includes(expectedValue.toLowerCase())) {
return false;
}
} else if (expectedValue !== currentValue) {
return false;
}
}
return true;
}
}
class LearningEngine {
constructor(db) {
this.db = db;
this.confidenceCalc = ConfidenceCalculator;
}
/**
* Main learning entry point
*/
learnFromExecution(execution) {
// Learn command patterns
if (execution.tool === 'Bash') {
this._learnCommandPattern(execution);
}
// Learn tool sequences
this._learnSequencePattern(execution);
// Learn context patterns
if (!execution.success) {
this._learnFailureContext(execution);
}
}
/**
* Learn from bash command executions
*/
_learnCommandPattern(execution) {
const command = execution.parameters.command || '';
if (!command) return;
const baseCmd = command.split(' ')[0];
const patternId = `cmd_${baseCmd}`;
if (patternId in this.db.commandPatterns) {
const pattern = this.db.commandPatterns[patternId];
// Update statistics
if (execution.success) {
pattern.prediction.successCount = (pattern.prediction.successCount || 0) + 1;
} else {
pattern.prediction.failureCount = (pattern.prediction.failureCount || 0) + 1;
}
// Recalculate confidence
const recency = this._calculateRecency(execution.timestamp);
pattern.confidence = this.confidenceCalc.calculateCommandConfidence(
pattern.prediction.successCount || 0,
pattern.prediction.failureCount || 0,
recency
);
pattern.lastSeen = execution.timestamp;
pattern.evidenceCount += 1;
} else {
// Create new pattern
this.db.commandPatterns[patternId] = {
patternId,
patternType: 'command_execution',
trigger: { command: baseCmd },
prediction: {
successCount: execution.success ? 1 : 0,
failureCount: execution.success ? 0 : 1,
commonErrors: execution.errorMessage ? [execution.errorMessage] : []
},
confidence: 0.3, // Start with low confidence
evidenceCount: 1,
lastSeen: execution.timestamp,
successRate: execution.success ? 1.0 : 0.0
};
}
}
/**
* Learn from tool sequence patterns
*/
_learnSequencePattern(execution) {
// Get recent tool history (last 5 tools)
const recentTools = this.db.executionHistory.slice(-5).map(e => e.tool);
recentTools.push(execution.tool);
// Look for sequences of 2-3 tools
for (let seqLen = 2; seqLen <= 3; seqLen++) {
if (recentTools.length >= seqLen) {
const sequence = recentTools.slice(-seqLen);
const patternId = `seq_${sequence.join('_')}`;
// Update or create sequence pattern
// (Simplified implementation - could be expanded)
}
}
}
/**
* Learn from failure contexts
*/
_learnFailureContext(execution) {
if (!execution.errorMessage) return;
// Extract key error indicators
const errorKey = this._extractErrorKey(execution.errorMessage);
if (!errorKey) return;
const patternId = `ctx_error_${errorKey}`;
if (patternId in this.db.contextPatterns) {
const pattern = this.db.contextPatterns[patternId];
pattern.evidenceCount += 1;
pattern.lastSeen = execution.timestamp;
// Update confidence based on repeated failures
pattern.confidence = Math.min(0.95, pattern.confidence + 0.05);
} else {
// Create new context pattern
this.db.contextPatterns[patternId] = {
patternId,
patternType: 'context_error',
trigger: {
tool: execution.tool,
errorType: errorKey
},
prediction: {
likelyError: execution.errorMessage,
suggestions: this._generateSuggestions(execution)
},
confidence: 0.4,
evidenceCount: 1,
lastSeen: execution.timestamp,
successRate: 0.0
};
}
}
/**
* Calculate recency factor (1.0 = very recent, 0.0 = very old)
*/
_calculateRecency(timestamp) {
const now = new Date();
const ageHours = (now - new Date(timestamp)) / (1000 * 60 * 60);
// Exponential decay: recent events matter more
return Math.max(0.0, Math.exp(-ageHours / 24.0)); // 24 hour half-life
}
/**
* Extract key error indicators from error messages
*/
_extractErrorKey(errorMessage) {
const message = errorMessage.toLowerCase();
const errorPatterns = {
'command_not_found': ['command not found', 'not found'],
'permission_denied': ['permission denied', 'access denied'],
'file_not_found': ['no such file', 'file not found'],
'connection_error': ['connection refused', 'network unreachable'],
'syntax_error': ['syntax error', 'invalid syntax']
};
for (const [errorType, patterns] of Object.entries(errorPatterns)) {
if (patterns.some(pattern => message.includes(pattern))) {
return errorType;
}
}
return null;
}
/**
* Generate suggestions based on failed execution
*/
_generateSuggestions(execution) {
const suggestions = [];
if (execution.tool === 'Bash') {
const command = execution.parameters.command || '';
if (command) {
const baseCmd = command.split(' ')[0];
// Common command alternatives
const alternatives = {
'pip': ['pip3', 'python -m pip', 'python3 -m pip'],
'python': ['python3'],
'node': ['nodejs'],
'vim': ['nvim', 'nano']
};
if (baseCmd in alternatives) {
const remainingArgs = command.split(' ').slice(1).join(' ');
suggestions.push(
...alternatives[baseCmd].map(alt => `Try '${alt} ${remainingArgs}'`)
);
}
}
}
return suggestions;
}
}
class PredictionEngine {
constructor(matcher) {
this.matcher = matcher;
}
/**
* Predict if a command will succeed and suggest alternatives
*/
predictCommandOutcome(command, context = {}) {
// Find matching patterns
const commandPatterns = this.matcher.fuzzyCommandMatch(command);
const contextPatterns = this.matcher.contextPatternMatch(context);
const prediction = {
likelySuccess: true,
confidence: 0.5,
warnings: [],
suggestions: []
};
// Analyze command patterns
for (const pattern of commandPatterns.slice(0, 3)) { // Top 3 matches
if (pattern.confidence > 0.7) {
const failureRate = (pattern.prediction.failureCount || 0) / Math.max(1, pattern.evidenceCount);
if (failureRate > 0.6) { // High failure rate
prediction.likelySuccess = false;
prediction.confidence = pattern.confidence;
prediction.warnings.push(`Command '${command.split(' ')[0]}' often fails`);
// Add suggestions from pattern
const suggestions = pattern.prediction.suggestions || [];
prediction.suggestions.push(...suggestions);
}
}
}
return prediction;
}
}
class ShadowLearner {
constructor(storagePath = '.claude_hooks/patterns') {
this.storagePath = path.resolve(storagePath);
fs.ensureDirSync(this.storagePath);
this.db = this._loadDatabase();
this.matcher = new PatternMatcher(this.db);
this.learningEngine = new LearningEngine(this.db);
this.predictionEngine = new PredictionEngine(this.matcher);
// Performance cache (simple in-memory cache)
this.predictionCache = new Map();
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
}
/**
* Learn from tool execution
*/
learnFromExecution(execution) {
try {
this.learningEngine.learnFromExecution(execution);
this.db.executionHistory.push(execution);
// Trim history to keep memory usage reasonable
if (this.db.executionHistory.length > 1000) {
this.db.executionHistory = this.db.executionHistory.slice(-500);
}
} catch (error) {
// Learning failures shouldn't break the system
console.error('Shadow learner error:', error.message);
}
}
/**
* Predict command outcome with caching
*/
predictCommandOutcome(command, context = {}) {
const cacheKey = `cmd_pred:${this._hash(command)}`;
const now = Date.now();
// Check cache
if (this.predictionCache.has(cacheKey)) {
const cached = this.predictionCache.get(cacheKey);
if (now - cached.timestamp < this.cacheTimeout) {
return cached.prediction;
}
}
const prediction = this.predictionEngine.predictCommandOutcome(command, context);
// Cache result
this.predictionCache.set(cacheKey, { prediction, timestamp: now });
// Clean old cache entries
this._cleanCache();
return prediction;
}
/**
* Quick method for command failure learning (backward compatibility)
*/
learnCommandFailure(command, suggestion, confidence) {
const execution = {
tool: 'Bash',
parameters: { command },
success: false,
timestamp: new Date(),
errorMessage: `Command failed: ${command}`
};
this.learnFromExecution(execution);
// Also store the specific suggestion
const baseCmd = command.split(' ')[0];
const patternId = `cmd_${baseCmd}`;
if (patternId in this.db.commandPatterns) {
const pattern = this.db.commandPatterns[patternId];
pattern.prediction.suggestions = pattern.prediction.suggestions || [];
if (!pattern.prediction.suggestions.includes(suggestion)) {
pattern.prediction.suggestions.push(suggestion);
}
}
}
/**
* Get suggestion for a command (backward compatibility)
*/
getSuggestion(command) {
const prediction = this.predictCommandOutcome(command);
if (!prediction.likelySuccess && prediction.suggestions.length > 0) {
return {
suggestion: prediction.suggestions[0].replace(/^Try '|'$/g, ''), // Clean format
confidence: prediction.confidence
};
}
return null;
}
/**
* Save learned patterns to disk
*/
async saveDatabase() {
try {
const patternsFile = path.join(this.storagePath, 'patterns.json');
const backupFile = path.join(this.storagePath, 'patterns.backup.json');
// Create backup of existing data
if (await fs.pathExists(patternsFile)) {
await fs.move(patternsFile, backupFile, { overwrite: true });
}
// Save new data
await fs.writeJson(patternsFile, this._serializeDatabase(), { spaces: 2 });
} catch (error) {
// Save failures shouldn't break the system
console.error('Failed to save shadow learner database:', error.message);
}
}
/**
* Load patterns database from disk
*/
_loadDatabase() {
const patternsFile = path.join(this.storagePath, 'patterns.json');
try {
if (fs.existsSync(patternsFile)) {
const data = fs.readJsonSync(patternsFile);
return this._deserializeDatabase(data);
}
} catch (error) {
// If loading fails, start with empty database
console.error('Failed to load shadow learner database, starting fresh:', error.message);
}
return {
commandPatterns: {},
contextPatterns: {},
sequencePatterns: {},
executionHistory: []
};
}
/**
* Serialize database for JSON storage
*/
_serializeDatabase() {
return {
commandPatterns: this.db.commandPatterns,
contextPatterns: this.db.contextPatterns,
sequencePatterns: this.db.sequencePatterns || {},
executionHistory: this.db.executionHistory.slice(-100), // Keep last 100 executions
metadata: {
version: '1.0.0',
lastSaved: new Date().toISOString()
}
};
}
/**
* Deserialize database from JSON
*/
_deserializeDatabase(data) {
return {
commandPatterns: data.commandPatterns || {},
contextPatterns: data.contextPatterns || {},
sequencePatterns: data.sequencePatterns || {},
executionHistory: (data.executionHistory || []).map(e => ({
...e,
timestamp: new Date(e.timestamp)
}))
};
}
/**
* Simple hash function for cache keys
*/
_hash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash.toString();
}
/**
* Clean expired cache entries
*/
_cleanCache() {
const now = Date.now();
for (const [key, value] of this.predictionCache.entries()) {
if (now - value.timestamp > this.cacheTimeout) {
this.predictionCache.delete(key);
}
}
}
}
module.exports = {
ShadowLearner,
ConfidenceCalculator,
PatternMatcher,
LearningEngine,
PredictionEngine
};