UNPKG

@r-huijts/ethics-vibe-check

Version:

🛡️ Make AI interrupt itself and challenge your thinking. Turns Claude into a philosophical sparring partner who actively contradicts comfortable conversations and challenges confirmation bias.

466 lines (465 loc) 17.3 kB
import fs from 'fs'; import path from 'path'; export const ETHICAL_CATEGORIES = [ 'Privacy Violation', 'Bias and Discrimination', 'Misinformation', 'Harmful Content', 'Manipulation', 'Consent Issues', 'Transparency Concerns', 'Fairness Issues', 'Confirmation Bias', 'Other' ]; // Storage configuration let STORAGE_DIR = path.join(process.cwd(), '.ethics-data'); let CONCERNS_FILE = path.join(STORAGE_DIR, 'concerns.json'); let USE_FILE_STORAGE = true; // In-memory fallback storage let memoryStorage = []; // Initialize storage system function initializeStorage() { // Try primary directory try { if (!fs.existsSync(STORAGE_DIR)) { fs.mkdirSync(STORAGE_DIR, { recursive: true }); } // Test write access const testFile = path.join(STORAGE_DIR, 'test.tmp'); fs.writeFileSync(testFile, 'test'); fs.unlinkSync(testFile); // Silent success - no console output return true; } catch (error) { console.error(`Primary storage failed: ${error}`); } // Try fallback directory const fallbackDir = path.join(process.cwd(), 'temp-ethics-data'); try { if (!fs.existsSync(fallbackDir)) { fs.mkdirSync(fallbackDir, { recursive: true }); } // Test write access const testFile = path.join(fallbackDir, 'test.tmp'); fs.writeFileSync(testFile, 'test'); fs.unlinkSync(testFile); STORAGE_DIR = fallbackDir; CONCERNS_FILE = path.join(fallbackDir, 'concerns.json'); console.error(`Using fallback directory: ${fallbackDir}`); return true; } catch (error) { console.error(`Fallback storage failed: ${error}`); } // Use in-memory storage as final fallback USE_FILE_STORAGE = false; console.error(`Using in-memory storage (data will not persist)`); return false; } // Load existing concerns function loadConcerns() { if (!USE_FILE_STORAGE) { return memoryStorage; } if (!fs.existsSync(CONCERNS_FILE)) { return []; } try { const data = fs.readFileSync(CONCERNS_FILE, 'utf-8'); return JSON.parse(data); } catch (error) { console.error('Error loading concerns, using empty array:', error); return []; } } // Save concerns to file or memory function saveConcerns(concerns) { if (!USE_FILE_STORAGE) { memoryStorage = [...concerns]; return; } try { fs.writeFileSync(CONCERNS_FILE, JSON.stringify(concerns, null, 2)); } catch (error) { console.error('Error saving concerns to file, switching to memory storage:', error); USE_FILE_STORAGE = false; memoryStorage = [...concerns]; } } // Simple string similarity check (Levenshtein-like) function getStringSimilarity(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 = getEditDistance(longer, shorter); return (longer.length - editDistance) / longer.length; } function getEditDistance(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 substitutionCost = str1[i - 1] === str2[j - 1] ? 0 : 1; matrix[j][i] = Math.min(matrix[j][i - 1] + 1, // insertion matrix[j - 1][i] + 1, // deletion matrix[j - 1][i - 1] + substitutionCost // substitution ); } } return matrix[str2.length][str1.length]; } // Check if a concern is a duplicate function isDuplicateConcern(newConcern, existingConcerns) { const now = Date.now(); const recentTimeframe = 24 * 60 * 60 * 1000; // 24 hours in milliseconds for (const existing of existingConcerns) { const existingTime = new Date(existing.timestamp).getTime(); const timeDiff = now - existingTime; // Check for duplicates within recent timeframe if (timeDiff <= recentTimeframe) { // Exact match check if (existing.concern.toLowerCase() === newConcern.concern.toLowerCase() && existing.category === newConcern.category) { console.error(`🔄 Duplicate detected (exact match): "${newConcern.concern.substring(0, 50)}..."`); return true; } // Same session + same category check (more likely to be duplicate) if (existing.sessionId && newConcern.sessionId && existing.sessionId === newConcern.sessionId && existing.category === newConcern.category) { const similarity = getStringSimilarity(existing.concern.toLowerCase(), newConcern.concern.toLowerCase()); if (similarity > 0.8) { // 80% similarity threshold console.error(`🔄 Duplicate detected (session + similarity): "${newConcern.concern.substring(0, 50)}..." (${Math.round(similarity * 100)}% similar)`); return true; } } // High similarity check across all recent concerns const similarity = getStringSimilarity(existing.concern.toLowerCase(), newConcern.concern.toLowerCase()); if (similarity > 0.9) { // 90% similarity threshold for different sessions console.error(`🔄 Duplicate detected (high similarity): "${newConcern.concern.substring(0, 50)}..." (${Math.round(similarity * 100)}% similar)`); return true; } } } return false; } // Add a new ethical concern with duplicate detection export function addEthicalConcern(concern) { try { // Initialize storage on first use if (USE_FILE_STORAGE && !fs.existsSync(STORAGE_DIR)) { initializeStorage(); } const concerns = loadConcerns(); // Check for duplicates if (isDuplicateConcern(concern, concerns)) { console.error(`⚠️ Skipping duplicate concern: "${concern.concern.substring(0, 50)}..."`); return false; // Return false to indicate no storage occurred (but not an error) } const newConcern = { ...concern, id: Date.now().toString(), timestamp: new Date().toISOString() }; concerns.push(newConcern); saveConcerns(concerns); console.error(`✅ Stored new concern: ${concern.category} - "${concern.concern.substring(0, 50)}..."`); return true; } catch (error) { console.error('Error adding ethical concern:', error); return false; } } // Store a complete ethical concern (for direct storage) export function storeConcern(concern) { try { // Initialize storage on first use if (USE_FILE_STORAGE && !fs.existsSync(STORAGE_DIR)) { initializeStorage(); } const concerns = loadConcerns(); concerns.push(concern); saveConcerns(concerns); return true; } catch (error) { console.error('Error storing concern:', error); return false; } } // Get all concerns export function getAllConcerns() { try { return loadConcerns(); } catch (error) { console.error('Error getting all concerns:', error); return []; } } // Get concerns by category export function getConcernsByCategory(category) { try { const concerns = loadConcerns(); return concerns.filter(concern => concern.category === category); } catch (error) { console.error('Error getting concerns by category:', error); return []; } } // Get concerns by session ID export function getConcernsBySession(sessionId) { try { const concerns = loadConcerns(); return concerns.filter(concern => concern.sessionId === sessionId); } catch (error) { console.error('Error getting concerns by session:', error); return []; } } // Get concerns by severity export function getConcernsBySeverity(severity) { try { const concerns = loadConcerns(); return concerns.filter(concern => concern.severity === severity); } catch (error) { console.error('Error getting concerns by severity:', error); return []; } } // Clear all concerns (for testing) export function clearAllConcerns() { try { saveConcerns([]); return true; } catch (error) { console.error('Error clearing concerns:', error); return false; } } // Get category statistics export function getCategoryStats() { try { const concerns = loadConcerns(); const categoryMap = new Map(); // Initialize all categories ETHICAL_CATEGORIES.forEach(category => { categoryMap.set(category, { category, count: 0 }); }); // Count concerns by category concerns.forEach(concern => { const stats = categoryMap.get(concern.category); if (stats) { stats.count++; if (!stats.recentExample || concern.timestamp > stats.recentExample.timestamp) { stats.recentExample = concern; } } }); // Return sorted by count (descending) return Array.from(categoryMap.values()) .filter(stats => stats.count > 0) .sort((a, b) => b.count - a.count); } catch (error) { console.error('Error getting category stats:', error); return []; } } // Get recent concerns for pattern analysis export function getRecentConcerns(limit = 10) { try { const concerns = loadConcerns(); return concerns .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) .slice(0, limit); } catch (error) { console.error('Error getting recent concerns:', error); return []; } } // 🧠 WEIGHTED PATTERN RECOGNITION SYSTEM // Calculate recency score (0-1, where 1 is most recent) function calculateRecencyScore(timestamp, maxAge = 30 * 24 * 60 * 60 * 1000) { const now = Date.now(); const concernTime = new Date(timestamp).getTime(); const age = now - concernTime; if (age < 0) return 1; // Future timestamps get max score if (age > maxAge) return 0.05; // Very old concerns get minimal score (was 0.1) // Exponential decay: more recent = higher score return Math.max(0.05, Math.exp(-age / (maxAge * 0.3))); } // Calculate severity score (0-1, where 1 is critical) function calculateSeverityScore(severity) { const severityMap = { 'low': 0.25, 'medium': 0.5, 'high': 0.75, 'critical': 1.0 }; return severityMap[severity]; } // Calculate relevance score based on context matching function calculateRelevanceScore(concern, context, category, sessionId) { let score = 0.5; // Base relevance // Category match boost if (category && concern.category === category) { score += 0.3; } // Session match boost (very relevant for ongoing conversations) if (sessionId && concern.sessionId === sessionId) { score += 0.4; } // Context/tags matching if (context && concern.contextTags) { const contextWords = context.toLowerCase().split(/\s+/); const matchingTags = concern.contextTags.filter(tag => contextWords.some(word => tag.toLowerCase().includes(word))); score += (matchingTags.length / concern.contextTags.length) * 0.2; } // Text similarity for context matching if (context) { const similarity = getStringSimilarity(concern.concern.toLowerCase(), context.toLowerCase()); score += similarity * 0.1; } return Math.min(1.0, score); } // Apply weighted scoring to a concern function applyWeightedScoring(concern, context, category, sessionId, weights = { recency: 0.3, severity: 0.25, success: 0.25, relevance: 0.2 }) { const recencyScore = calculateRecencyScore(concern.timestamp); const severityScore = calculateSeverityScore(concern.severity); const successScore = concern.successScore ?? 0.5; // Default neutral score const relevanceScore = calculateRelevanceScore(concern, context, category, sessionId); // Calculate weighted total score const totalScore = (recencyScore * weights.recency + severityScore * weights.severity + successScore * weights.success + relevanceScore * weights.relevance); return { ...concern, weight: totalScore, recencyScore, severityScore, successScore, relevanceScore, totalScore }; } // Get weighted concerns for pattern analysis (MAIN FUNCTION) export function getWeightedConcerns(options = {}) { try { const { limit = 10, context, category, sessionId, minWeight = 0.1, weights = { recency: 0.3, severity: 0.25, success: 0.25, relevance: 0.2 } } = options; const concerns = loadConcerns(); // Apply weighted scoring to all concerns const weightedConcerns = concerns .map(concern => applyWeightedScoring(concern, context, category, sessionId, weights)) .filter(concern => concern.totalScore >= minWeight) .sort((a, b) => b.totalScore - a.totalScore) .slice(0, limit); return weightedConcerns; } catch (error) { console.error('Error getting weighted concerns:', error); return []; } } // Get category-specific weighted patterns export function getWeightedCategoryPatterns(category, limit = 5) { return getWeightedConcerns({ limit, category, weights: { recency: 0.2, severity: 0.3, success: 0.3, relevance: 0.2 } // Higher weight on severity and success for category patterns }); } // Get session-specific weighted patterns export function getWeightedSessionPatterns(sessionId, limit = 8) { return getWeightedConcerns({ limit, sessionId, weights: { recency: 0.4, severity: 0.2, success: 0.2, relevance: 0.2 } // Higher weight on recency for session patterns }); } // Update success score for a concern (for tracking recommendation effectiveness) export function updateConcernSuccessScore(concernId, successScore) { try { const concerns = loadConcerns(); const concernIndex = concerns.findIndex(c => c.id === concernId); if (concernIndex === -1) { console.error(`Concern with ID ${concernId} not found`); return false; } concerns[concernIndex].successScore = Math.max(0, Math.min(1, successScore)); saveConcerns(concerns); console.error(`✅ Updated success score for concern ${concernId}: ${successScore}`); return true; } catch (error) { console.error('Error updating concern success score:', error); return false; } } // Get pattern insights for debugging/monitoring export function getPatternInsights() { try { const concerns = loadConcerns(); const weightedConcerns = concerns.map(concern => applyWeightedScoring(concern)); const totalConcerns = concerns.length; const averageWeight = weightedConcerns.reduce((sum, c) => sum + c.totalScore, 0) / totalConcerns; // Category distribution const categoryDistribution = {}; concerns.forEach(concern => { categoryDistribution[concern.category] = (categoryDistribution[concern.category] || 0) + 1; }); // Recent trends (last 7 days vs previous 7 days) const now = Date.now(); const weekAgo = now - (7 * 24 * 60 * 60 * 1000); const twoWeeksAgo = now - (14 * 24 * 60 * 60 * 1000); const recentTrends = []; ETHICAL_CATEGORIES.forEach(category => { const recentCount = concerns.filter(c => c.category === category && new Date(c.timestamp).getTime() > weekAgo).length; const previousCount = concerns.filter(c => c.category === category && new Date(c.timestamp).getTime() > twoWeeksAgo && new Date(c.timestamp).getTime() <= weekAgo).length; let trend = 'stable'; if (recentCount > previousCount * 1.2) trend = 'increasing'; else if (recentCount < previousCount * 0.8) trend = 'decreasing'; if (recentCount > 0 || previousCount > 0) { recentTrends.push({ category, trend }); } }); return { totalConcerns, averageWeight: averageWeight || 0, categoryDistribution, recentTrends }; } catch (error) { console.error('Error getting pattern insights:', error); return { totalConcerns: 0, averageWeight: 0, categoryDistribution: {}, recentTrends: [] }; } } // Initialize storage system on module load initializeStorage();