UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

392 lines (339 loc) 11.2 kB
/** * L3 Semantic Memory for External Ralph Loop * * Persistent cross-loop knowledge store that accumulates learnings across * multiple loop executions. Stores proven strategies, anti-patterns, * project conventions, and time/iteration estimates. * * Memory Levels: * - L1: Working Memory (Claude session) - Temporary * - L2: Episodic Memory (loop state) - Single loop history * - L3: Semantic Memory (this) - Cross-loop persistent knowledge * * @implements @.aiwg/working/issue-ralph-external-completion.md Section L3 * @schema @agentic/code/addons/ralph/schemas/semantic-memory.yaml * @issue #24 */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { randomUUID } from 'crypto'; import { createHash } from 'crypto'; /** * @typedef {Object} Learning * @property {string} id - Unique learning ID (learn-xxx) * @property {string} type - strategy|antipattern|estimate|convention * @property {string} taskType - test-fix|feature|refactor|bug-fix * @property {Object} content - Learning content (type-specific) * @property {number} confidence - Confidence score 0.0-1.0 * @property {string[]} sourceLoops - Loop IDs this learning came from * @property {string} createdAt - ISO timestamp * @property {string} updatedAt - ISO timestamp * @property {number} useCount - Times this learning was retrieved * @property {number} successRate - Success rate 0.0-1.0 */ /** * @typedef {Object} SemanticMemoryStore * @property {string} version - Schema version * @property {string} checksum - SHA-256 checksum for corruption detection * @property {string} lastUpdated - Last update timestamp * @property {Learning[]} learnings - All learnings * @property {Object} stats - Statistics */ const SCHEMA_VERSION = '1.0.0'; const DEFAULT_KNOWLEDGE_PATH = join(process.cwd(), '.aiwg', 'knowledge'); export class SemanticMemory { /** * @param {string} [knowledgeDir] - Knowledge directory (defaults to .aiwg/knowledge) */ constructor(knowledgeDir = DEFAULT_KNOWLEDGE_PATH) { this.knowledgeDir = knowledgeDir; this.storePath = join(knowledgeDir, 'ralph-learnings.json'); this.ensureKnowledgeDir(); } /** * Ensure knowledge directory exists */ ensureKnowledgeDir() { if (!existsSync(this.knowledgeDir)) { mkdirSync(this.knowledgeDir, { recursive: true }); } } /** * Calculate checksum of learnings for corruption detection * @param {Learning[]} learnings - Learnings array * @returns {string} SHA-256 checksum */ calculateChecksum(learnings) { const content = JSON.stringify(learnings, null, 0); return createHash('sha256').update(content).digest('hex'); } /** * Load semantic memory store with corruption detection * @returns {SemanticMemoryStore} */ load() { if (!existsSync(this.storePath)) { // Initialize empty store return this.initializeStore(); } try { const content = readFileSync(this.storePath, 'utf8'); const store = JSON.parse(content); // Verify checksum const expectedChecksum = this.calculateChecksum(store.learnings); if (store.checksum !== expectedChecksum) { throw new Error('Checksum mismatch - data may be corrupted'); } return store; } catch (e) { if (e.message.includes('Checksum mismatch')) { // Attempt recovery from backup if available const backupPath = `${this.storePath}.bak`; if (existsSync(backupPath)) { try { const backupContent = readFileSync(backupPath, 'utf8'); const backupStore = JSON.parse(backupContent); // Verify backup checksum const backupChecksum = this.calculateChecksum(backupStore.learnings); if (backupStore.checksum === backupChecksum) { console.warn('Recovered from backup due to checksum mismatch'); this.save(backupStore); return backupStore; } } catch (backupError) { console.error('Backup also corrupted:', backupError.message); } } throw new Error(`Knowledge store corrupted and backup recovery failed: ${e.message}`); } throw new Error(`Failed to load semantic memory: ${e.message}`); } } /** * Initialize empty store * @returns {SemanticMemoryStore} */ initializeStore() { const store = { version: SCHEMA_VERSION, checksum: this.calculateChecksum([]), lastUpdated: new Date().toISOString(), learnings: [], stats: { totalLearnings: 0, byType: { strategy: 0, antipattern: 0, estimate: 0, convention: 0, }, byTaskType: {}, }, }; this.save(store); return store; } /** * Save semantic memory store atomically with backup * @param {SemanticMemoryStore} store */ save(store) { this.ensureKnowledgeDir(); // Update timestamp and stats store.lastUpdated = new Date().toISOString(); store.stats.totalLearnings = store.learnings.length; // Recalculate type counts store.stats.byType = { strategy: 0, antipattern: 0, estimate: 0, convention: 0, }; store.stats.byTaskType = {}; for (const learning of store.learnings) { store.stats.byType[learning.type] = (store.stats.byType[learning.type] || 0) + 1; store.stats.byTaskType[learning.taskType] = (store.stats.byTaskType[learning.taskType] || 0) + 1; } // Calculate checksum store.checksum = this.calculateChecksum(store.learnings); // Create backup of existing store if (existsSync(this.storePath)) { const backupPath = `${this.storePath}.bak`; try { const currentContent = readFileSync(this.storePath, 'utf8'); writeFileSync(backupPath, currentContent); } catch (e) { console.warn('Failed to create backup:', e.message); } } // Write atomically (temp file then rename) const tempPath = `${this.storePath}.tmp`; writeFileSync(tempPath, JSON.stringify(store, null, 2)); // Atomic rename const fs = require('fs'); fs.renameSync(tempPath, this.storePath); } /** * Store a new learning * @param {string} type - Learning type * @param {string} taskType - Task type * @param {Object} content - Learning content * @param {Object} metadata - Additional metadata * @returns {Learning} */ store(type, taskType, content, metadata = {}) { const store = this.load(); const learning = { id: `learn-${randomUUID().split('-')[0]}`, type, taskType, content, confidence: metadata.confidence || 0.5, sourceLoops: metadata.sourceLoops || [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), useCount: 0, successRate: metadata.successRate || 0.0, }; store.learnings.push(learning); this.save(store); return learning; } /** * Retrieve learning by ID * @param {string} id - Learning ID * @returns {Learning|null} */ retrieve(id) { const store = this.load(); const learning = store.learnings.find(l => l.id === id); if (learning) { // Increment use count learning.useCount++; learning.updatedAt = new Date().toISOString(); this.save(store); } return learning || null; } /** * Query learnings by pattern * @param {Object} pattern - Query pattern * @param {string} [pattern.type] - Learning type filter * @param {string} [pattern.taskType] - Task type filter * @param {number} [pattern.minConfidence] - Minimum confidence * @param {number} [pattern.minSuccessRate] - Minimum success rate * @param {number} [pattern.limit] - Maximum results * @returns {Learning[]} */ query(pattern = {}) { const store = this.load(); let results = store.learnings; // Apply filters if (pattern.type) { results = results.filter(l => l.type === pattern.type); } if (pattern.taskType) { results = results.filter(l => l.taskType === pattern.taskType); } if (pattern.minConfidence !== undefined) { results = results.filter(l => l.confidence >= pattern.minConfidence); } if (pattern.minSuccessRate !== undefined) { results = results.filter(l => l.successRate >= pattern.minSuccessRate); } // Sort by confidence * successRate * useCount (weighted relevance) results.sort((a, b) => { const scoreA = a.confidence * (a.successRate || 0.5) * Math.log10(a.useCount + 1); const scoreB = b.confidence * (b.successRate || 0.5) * Math.log10(b.useCount + 1); return scoreB - scoreA; }); // Apply limit if (pattern.limit) { results = results.slice(0, pattern.limit); } return results; } /** * Update learning metadata * @param {string} id - Learning ID * @param {Partial<Learning>} updates - Fields to update * @returns {Learning|null} */ update(id, updates) { const store = this.load(); const learning = store.learnings.find(l => l.id === id); if (!learning) { return null; } // Apply updates (prevent changing immutable fields) const immutableFields = ['id', 'createdAt', 'sourceLoops']; for (const [key, value] of Object.entries(updates)) { if (!immutableFields.includes(key)) { learning[key] = value; } } learning.updatedAt = new Date().toISOString(); this.save(store); return learning; } /** * Delete learning by ID * @param {string} id - Learning ID * @returns {boolean} True if deleted */ delete(id) { const store = this.load(); const index = store.learnings.findIndex(l => l.id === id); if (index === -1) { return false; } store.learnings.splice(index, 1); this.save(store); return true; } /** * Get statistics about stored knowledge * @returns {Object} */ getStats() { const store = this.load(); return { ...store.stats, totalSize: store.learnings.length, lastUpdated: store.lastUpdated, averageConfidence: store.learnings.reduce((sum, l) => sum + l.confidence, 0) / (store.learnings.length || 1), averageSuccessRate: store.learnings.reduce((sum, l) => sum + l.successRate, 0) / (store.learnings.length || 1), mostUsedLearning: store.learnings.reduce((max, l) => l.useCount > max.useCount ? l : max, { useCount: 0 }), }; } /** * Verify store integrity * @returns {{valid: boolean, error?: string}} */ verify() { try { const store = this.load(); const expectedChecksum = this.calculateChecksum(store.learnings); if (store.checksum !== expectedChecksum) { return { valid: false, error: 'Checksum mismatch - data may be corrupted', }; } return { valid: true }; } catch (e) { return { valid: false, error: e.message, }; } } /** * Clear all learnings (DANGEROUS - for testing only) */ clear() { const store = this.initializeStore(); this.save(store); } } export default SemanticMemory;