#!/usr/bin/env node /** * Self-Correcting Memory Architecture - Critic Engine * * Flow: * 1. Generate response → 2. Critic checks against correction store → * 3. If violations → Block & regenerate → 4. If new pattern → Auto-store * * Principles: * - ALL output validated against correction store before delivery * - Quantitative claims without proof → auto-correction * - Corrections immediately block future occurrences * - System learns from its own mistakes */ const fs = require('fs'); const path = require('path'); class CriticEngine { constructor(options = {}) { this.correctionStorePath = options.correctionStorePath || '/home/jcbeasley/.openclaw/workspace/memory/corrections'; this.autoLearnEnabled = options.autoLearn !== false; this.strictMode = options.strictMode || false; // Block on warnings too // Correction patterns (loaded from store + hardcoded rules) this.corrections = []; this.patternRules = this.getPatternRules(); } /** * Pattern Rules - Auto-detect uncorrected issues */ getPatternRules() { return [ { id: 'quantitative_without_proof', name: 'Quantitative Claim Without Evidence', pattern: /\b\d+\s*(%|percent|times|x|percent|fold)\b/i, exclude: /\b(according to|based on|from|measured|tested|verified|source:|citation|data:)\b/i, severity: 'auto-correct', message: 'Quantitative claim requires evidence', autoStore: true }, { id: 'unverified_performance', name: 'Performance Claim Without Benchmark', pattern: /\b(faster|slower|better|worse|improved|optimized)\s+(than|by)\b/i, exclude: /\b(measured|benchmarked|tested|profiled|verified)\b/i, severity: 'auto-correct', message: 'Performance claims require benchmarks', autoStore: true }, { id: 'absolute_without_qualification', name: 'Absolute Statement Without Qualification', pattern: /\b(always|never|all|none|every|impossible)\b/i, exclude: /\b(in this case|for this|under these|given the|based on)\b/i, severity: 'warning', message: 'Absolute statements need qualification', autoStore: false // Manual review first }, { id: 'vague_quantification', name: 'Vague Quantification', pattern: /\b(many|few|several|some|most|lots|a lot)\b/i, exclude: /\b(specifically|exactly|precisely)\b/i, severity: 'auto-correct', message: 'Use specific numbers or omit', autoStore: true }, { id: 'unverified_completion', name: '"Done" Without Verification', pattern: /\b(done|complete|finished|working)\b/i, exclude: /\b(verified|tested|confirmed|validated|checked)\b/i, severity: 'error', message: 'Completion claims require verification evidence', autoStore: true } ]; } /** * Load corrections from store */ async loadCorrections() { // Load from NocoDB (to be implemented) or local JSON const correctionsFile = path.join(this.correctionStorePath, '_index.json'); try { if (fs.existsSync(correctionsFile)) { const data = JSON.parse(fs.readFileSync(correctionsFile, 'utf8')); this.corrections = data.corrections || []; } } catch (err) { console.warn('Could not load corrections:', err.message); } return this; } /** * Main Critic Check * Returns: { passed: bool, violations: [], newPatterns: [], corrected: string } */ async critique(response, context = {}) { await this.loadCorrections(); const violations = []; const newPatterns = []; // 1. Check against known corrections (blocking) const knownViolations = this.checkKnownCorrections(response); violations.push(...knownViolations); // 2. Auto-detect new patterns (if enabled) if (this.autoLearnEnabled && violations.length === 0) { const detectedPatterns = this.detectNewPatterns(response, context); for (const pattern of detectedPatterns) { if (pattern.severity === 'auto-correct') { // Auto-store as new correction await this.storeCorrection(pattern); violations.push({ ...pattern, message: `${pattern.message} [AUTO-STORED AS CORRECTION]`, autoStored: true }); } else if (pattern.severity === 'warning') { newPatterns.push(pattern); } } } // 3. Generate corrected version if violations found let corrected = response; if (violations.length > 0) { corrected = await this.generateCorrection(response, violations); } return { passed: violations.length === 0, blocked: violations.length > 0 && violations.some(v => v.blocking), violations, newPatterns, corrected, original: response }; } /** * Check against known corrections (from store) */ checkKnownCorrections(response) { const violations = []; for (const correction of this.corrections) { const pattern = new RegExp(correction.pattern, 'i'); if (pattern.test(response)) { // Check if exclusion applies if (correction.excludePattern) { const exclude = new RegExp(correction.excludePattern, 'i'); if (exclude.test(response)) { continue; // Has exclusion, skip } } violations.push({ id: correction.id, type: 'known-correction', severity: correction.severity || 'error', blocking: correction.blocking !== false, message: correction.message, suggestion: correction.suggestion, originalPattern: correction.pattern }); } } return violations; } /** * Detect new patterns that should become corrections */ detectNewPatterns(response, context) { const detected = []; for (const rule of this.patternRules) { if (rule.pattern.test(response)) { // Check if exclusion applies if (rule.exclude && rule.exclude.test(response)) { continue; } detected.push({ id: rule.id, type: 'auto-detected', severity: rule.severity, message: rule.message, pattern: rule.pattern.source, autoStore: rule.autoStore, context: context.task || 'unknown' }); } } return detected; } /** * Store new correction (immediate write) */ async storeCorrection(pattern) { const correction = { id: `correction_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, pattern: pattern.pattern, severity: pattern.severity, message: pattern.message, suggestion: `Add evidence or qualification`, blocking: true, autoDetected: true, createdAt: new Date().toISOString(), context: pattern.context }; // Add to memory this.corrections.push(correction); // Persist to disk (immediate) await this.persistCorrections(); // Log for audit console.log(`[CRITIC] Auto-stored correction: ${correction.id} - ${pattern.message}`); return correction; } /** * Persist corrections to disk (will move to NocoDB) */ async persistCorrections() { try { if (!fs.existsSync(this.correctionStorePath)) { fs.mkdirSync(this.correctionStorePath, { recursive: true }); } const indexFile = path.join(this.correctionStorePath, '_index.json'); fs.writeFileSync(indexFile, JSON.stringify({ version: 1, updatedAt: new Date().toISOString(), corrections: this.corrections }, null, 2)); // Also write individual correction files for inspection for (const correction of this.corrections.slice(-10)) { // Last 10 const filePath = path.join(this.correctionStorePath, `${correction.id}.json`); fs.writeFileSync(filePath, JSON.stringify(correction, null, 2)); } } catch (err) { console.error('[CRITIC] Failed to persist corrections:', err); } } /** * Generate corrected version of response */ async generateCorrection(original, violations) { // Simple correction: add warning header const warnings = violations.map(v => `- ${v.message}`).join('\n'); return `⚠️ CORRECTIONS REQUIRED:\n${warnings}\n\n---\n\n${original}`; // Future: Use LLM to actually fix the content // const fixPrompt = buildFixPrompt(original, violations); // return await llm.generate(fixPrompt); } /** * Manual correction entry (for user feedback) */ async addManualCorrection(originalText, correctionText, reason) { const correction = { id: `manual_${Date.now()}`, pattern: this.escapeRegex(originalText.substring(0, 100)), severity: 'error', message: reason, suggestion: correctionText, blocking: true, manualEntry: true, createdAt: new Date().toISOString() }; this.corrections.push(correction); await this.persistCorrections(); return correction; } /** * Escape regex special chars */ escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } } // Export module.exports = CriticEngine; // CLI usage if (require.main === module) { const responseFile = process.argv[2]; if (!responseFile) { console.error('Usage: node critic.js '); process.exit(1); } const response = fs.readFileSync(responseFile, 'utf8'); const critic = new CriticEngine(); critic.critique(response) .then(result => { console.log(JSON.stringify(result, null, 2)); }) .catch(err => { console.error('Critic error:', err); process.exit(1); }); }