ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
402 lines • 17.5 kB
JavaScript
/**
* AI Feedback Privacy Aggregator
*
* Privacy-preserving data aggregation for AI feedback collection that enables
* valuable analytics while protecting user privacy through differential privacy,
* k-anonymity, and data minimization techniques.
*/
import * as crypto from 'crypto';
/**
* Privacy-Preserving AI Feedback Aggregator
*/
export class AIFeedbackPrivacyAggregator {
config;
privacyBudgetUsed = 0;
aggregationHistory = [];
constructor(config = {}) {
this.config = {
enableDifferentialPrivacy: true,
epsilonValue: 1.0, // Standard privacy budget
kAnonymityThreshold: 5, // Minimum 5 users per group
enableDataMinimization: true,
retentionPeriodDays: 90,
allowedAggregations: ['count', 'average', 'distribution', 'trends'],
...config
};
}
/**
* Aggregate feedback data with privacy preservation
*/
async aggregateWithPrivacy(feedbackData, aggregationType = 'weekly') {
// Apply privacy filters first
const privacyFilteredData = this.applyPrivacyFilters(feedbackData);
// Check k-anonymity requirements
const kAnonymityMet = this.checkKAnonymity(privacyFilteredData);
// Apply data minimization
const minimizedData = this.config.enableDataMinimization
? this.applyDataMinimization(privacyFilteredData)
: privacyFilteredData;
// Generate base aggregations
const baseMetrics = this.calculateBaseMetrics(minimizedData);
// Apply differential privacy noise if enabled
const noisyMetrics = this.config.enableDifferentialPrivacy
? this.addDifferentialPrivacyNoise(baseMetrics)
: baseMetrics;
const aggregatedMetrics = {
timestamp: Date.now(),
aggregationType,
privacyLevel: this.determinePrivacyLevel(),
metrics: noisyMetrics,
privacyMetadata: {
noiseAdded: this.config.enableDifferentialPrivacy,
kAnonymityMet,
dataMinimized: this.config.enableDataMinimization,
originalEntryCount: feedbackData.length,
aggregatedEntryCount: minimizedData.length
}
};
// Store aggregation for budget tracking
this.aggregationHistory.push(aggregatedMetrics);
this.updatePrivacyBudget();
return aggregatedMetrics;
}
/**
* Generate privacy compliance report
*/
generatePrivacyReport(feedbackData) {
const risks = [];
const recommendations = [];
// Check data retention compliance
const now = Date.now();
const retentionThreshold = now - (this.config.retentionPeriodDays * 24 * 60 * 60 * 1000);
const expiredEntries = feedbackData.filter(entry => entry.timestamp < retentionThreshold);
if (expiredEntries.length > 0) {
risks.push(`${expiredEntries.length} entries exceed retention period`);
recommendations.push('Implement automated data purging for expired entries');
}
// Check for potential re-identification risks
const identificationRisks = this.assessReidentificationRisk(feedbackData);
risks.push(...identificationRisks);
// Privacy budget assessment
if (this.privacyBudgetUsed > 0.8) {
risks.push('Privacy budget nearly exhausted (>80% used)');
recommendations.push('Consider reducing query frequency or increasing epsilon value');
}
// Anonymization effectiveness
const anonymizationScore = this.calculateAnonymizationScore(feedbackData);
return {
complianceLevel: risks.length === 0 ? 'compliant' :
risks.length <= 2 ? 'warning' : 'violation',
privacyRisks: risks,
recommendedActions: recommendations,
dataRetentionStatus: {
totalEntries: feedbackData.length,
expiredEntries: expiredEntries.length,
retainedEntries: feedbackData.length - expiredEntries.length
},
anonymizationEffectiveness: {
score: anonymizationScore,
vulnerabilities: anonymizationScore < 70 ? [
'Insufficient k-anonymity',
'Potential quasi-identifier leakage'
] : [],
improvements: [
'Implement stronger hashing algorithms',
'Increase k-anonymity threshold',
'Add more differential privacy noise'
]
}
};
}
/**
* Export privacy-compliant dataset
*/
exportPrivacyCompliantDataset(feedbackData, exportLevel = 'research') {
let processedData = [...feedbackData];
const guarantees = [];
const limitations = [];
// Apply privacy level specific transformations
switch (exportLevel) {
case 'public':
processedData = this.applyMaximalPrivacy(processedData);
guarantees.push('k-anonymity with k ≥ 10');
guarantees.push('Differential privacy with ε = 0.5');
guarantees.push('All identifiers removed or hashed');
limitations.push('Reduced data granularity');
limitations.push('Some metrics may have statistical noise');
break;
case 'research':
processedData = this.applyResearchPrivacy(processedData);
guarantees.push('k-anonymity with k ≥ 5');
guarantees.push('Differential privacy with ε = 1.0');
guarantees.push('Session IDs hashed');
limitations.push('Timestamp granularity reduced to days');
break;
case 'internal':
processedData = this.applyInternalPrivacy(processedData);
guarantees.push('Data minimization applied');
guarantees.push('Retention policy enforced');
break;
}
return {
dataset: processedData,
privacyGuarantees: guarantees,
limitations
};
}
// Private methods
applyPrivacyFilters(data) {
// Remove entries that don't meet privacy requirements
return data.filter(entry => {
// Ensure minimum data quality
if (!entry.sessionId || !entry.agentType)
return false;
// Apply retention policy
const retentionThreshold = Date.now() - (this.config.retentionPeriodDays * 24 * 60 * 60 * 1000);
if (entry.timestamp < retentionThreshold)
return false;
return true;
});
}
checkKAnonymity(data) {
// Group by quasi-identifiers (framework + complexity + agent type)
const groups = new Map();
data.forEach(entry => {
const key = `${entry.contextualData.framework}_${entry.contextualData.projectComplexity}_${entry.agentType}`;
groups.set(key, (groups.get(key) || 0) + 1);
});
// Check if all groups meet k-anonymity threshold
for (const count of groups.values()) {
if (count < this.config.kAnonymityThreshold) {
return false;
}
}
return true;
}
applyDataMinimization(data) {
return data.map(entry => ({
...entry,
// Hash session ID for privacy
sessionId: this.hashValue(entry.sessionId),
// Remove or generalize detailed technical metrics
technicalMetrics: {
...entry.technicalMetrics,
errorsEncountered: [], // Remove specific error messages
recoveryActions: [] // Remove specific recovery details
},
// Generalize feedback text
feedback: {
...entry.feedback,
strengths: this.generalizeTextArray(entry.feedback.strengths),
weaknesses: this.generalizeTextArray(entry.feedback.weaknesses),
suggestions: this.generalizeTextArray(entry.feedback.suggestions)
}
})); // Type assertion since we're maintaining the structure
}
calculateBaseMetrics(data) {
const totalSessions = data.length;
// Calculate average satisfaction
const satisfactionValues = data.map(d => d.userExperience.satisfaction);
const averageSatisfaction = satisfactionValues.reduce((sum, val) => sum + val, 0) / satisfactionValues.length;
// Satisfaction distribution
const satisfactionDistribution = {};
satisfactionValues.forEach(val => {
const bucket = this.getSatisfactionBucket(val);
satisfactionDistribution[bucket] = (satisfactionDistribution[bucket] || 0) + 1;
});
// Agent performance metrics
const agentPerformance = {};
const agentGroups = this.groupByAgent(data);
Object.entries(agentGroups).forEach(([agent, entries]) => {
const avgRating = entries.reduce((sum, e) => sum + e.userExperience.satisfaction, 0) / entries.length;
agentPerformance[agent] = {
usageCount: entries.length,
averageRating: Math.round(avgRating * 10) / 10,
anonymizedFeedback: this.extractAnonymizedFeedback(entries)
};
});
// Framework metrics
const frameworkMetrics = {};
const frameworkGroups = this.groupByFramework(data);
Object.entries(frameworkGroups).forEach(([framework, entries]) => {
const successCount = entries.filter(e => e.outcome === 'success').length;
frameworkMetrics[framework] = {
sessionCount: entries.length,
averageComplexity: this.getMostCommonComplexity(entries),
successRate: Math.round((successCount / entries.length) * 100) / 100
};
});
// Trend indicators (simplified)
const trendIndicators = {
satisfactionTrend: averageSatisfaction > 7 ? 'improving' :
averageSatisfaction > 5 ? 'stable' : 'declining',
usageTrend: totalSessions > 10 ? 'increasing' : 'stable',
qualityTrend: averageSatisfaction > 7 ? 'improving' : 'stable'
};
return {
totalSessions,
averageSatisfaction: Math.round(averageSatisfaction * 10) / 10,
satisfactionDistribution,
agentPerformance,
frameworkMetrics,
trendIndicators
};
}
addDifferentialPrivacyNoise(metrics) {
// Add Laplacian noise to numerical values
const epsilon = this.config.epsilonValue;
return {
...metrics,
totalSessions: Math.max(0, metrics.totalSessions + this.generateLaplaceNoise(1 / epsilon)),
averageSatisfaction: Math.max(0, Math.min(10, metrics.averageSatisfaction + this.generateLaplaceNoise(1 / epsilon))),
// Add noise to counts in distributions
satisfactionDistribution: Object.fromEntries(Object.entries(metrics.satisfactionDistribution).map(([key, count]) => [
key, Math.max(0, count + this.generateLaplaceNoise(1 / epsilon))
]))
};
}
generateLaplaceNoise(scale) {
// Generate Laplacian noise for differential privacy
const u = Math.random() - 0.5;
return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
}
hashValue(value) {
return crypto.createHash('sha256').update(value + 'privacy-salt').digest('hex').substring(0, 16);
}
generalizeTextArray(texts) {
// Remove potentially identifying information and generalize
return texts.map(text => {
return text
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[email]')
.replace(/\b(?:https?:\/\/)?(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:\/[^\s]*)?\b/g, '[url]')
.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '[ip]')
.replace(/\b\d{4,}\b/g, '[number]')
.substring(0, 100); // Limit length
}).slice(0, 3); // Limit count
}
getSatisfactionBucket(satisfaction) {
if (satisfaction >= 8)
return 'high';
if (satisfaction >= 6)
return 'medium';
return 'low';
}
groupByAgent(data) {
const groups = {};
data.forEach(entry => {
groups[entry.agentType] = groups[entry.agentType] || [];
groups[entry.agentType].push(entry);
});
return groups;
}
groupByFramework(data) {
const groups = {};
data.forEach(entry => {
const framework = entry.contextualData.framework;
groups[framework] = groups[framework] || [];
groups[framework].push(entry);
});
return groups;
}
getMostCommonComplexity(entries) {
const counts = {};
entries.forEach(e => {
counts[e.contextualData.projectComplexity] = (counts[e.contextualData.projectComplexity] || 0) + 1;
});
return Object.entries(counts).reduce((a, b) => counts[a[0]] > counts[b[0]] ? a : b)[0];
}
extractAnonymizedFeedback(entries) {
const allFeedback = entries.flatMap(e => [
...e.feedback.strengths,
...e.feedback.weaknesses,
...e.feedback.suggestions
]);
return this.generalizeTextArray(allFeedback);
}
determinePrivacyLevel() {
if (this.config.enableDifferentialPrivacy && this.config.kAnonymityThreshold >= 10) {
return 'strict';
}
if (this.config.enableDifferentialPrivacy || this.config.kAnonymityThreshold >= 5) {
return 'standard';
}
return 'minimal';
}
updatePrivacyBudget() {
// Simple privacy budget tracking (in real implementation, this would be more sophisticated)
this.privacyBudgetUsed += this.config.epsilonValue / 10;
}
assessReidentificationRisk(data) {
const risks = [];
// Check for unique combinations of quasi-identifiers
const combinations = new Map();
data.forEach(entry => {
const key = `${entry.contextualData.framework}_${entry.contextualData.projectComplexity}_${entry.agentType}`;
combinations.set(key, (combinations.get(key) || 0) + 1);
});
const uniqueEntries = Array.from(combinations.values()).filter(count => count === 1).length;
const uniqueRatio = uniqueEntries / data.length;
if (uniqueRatio > 0.1) {
risks.push(`High re-identification risk: ${Math.round(uniqueRatio * 100)}% unique entries`);
}
return risks;
}
calculateAnonymizationScore(data) {
let score = 100;
// Deduct points for privacy risks
const kAnonymityMet = this.checkKAnonymity(data);
if (!kAnonymityMet)
score -= 30;
if (!this.config.enableDifferentialPrivacy)
score -= 20;
if (!this.config.enableDataMinimization)
score -= 15;
const risks = this.assessReidentificationRisk(data);
score -= risks.length * 10;
return Math.max(0, score);
}
applyMaximalPrivacy(data) {
return data
.filter((_, index) => index % 2 === 0) // Sample 50% for extreme privacy
.map(entry => ({
timestamp: Math.floor(entry.timestamp / (24 * 60 * 60 * 1000)) * (24 * 60 * 60 * 1000), // Round to day
agentType: entry.agentType,
outcome: entry.outcome,
satisfaction: this.getSatisfactionBucket(entry.userExperience.satisfaction),
framework: entry.contextualData.framework === 'unknown' ? 'unknown' : 'known',
complexity: entry.contextualData.projectComplexity
}));
}
applyResearchPrivacy(data) {
return data.map(entry => ({
sessionId: this.hashValue(entry.sessionId),
timestamp: Math.floor(entry.timestamp / (24 * 60 * 60 * 1000)) * (24 * 60 * 60 * 1000),
agentType: entry.agentType,
outcome: entry.outcome,
userExperience: {
satisfaction: Math.round(entry.userExperience.satisfaction),
efficiency: Math.round(entry.userExperience.efficiency),
clarity: Math.round(entry.userExperience.clarity),
usefulness: Math.round(entry.userExperience.usefulness)
},
contextualData: {
framework: entry.contextualData.framework,
projectComplexity: entry.contextualData.projectComplexity,
userType: entry.contextualData.userType
}
}));
}
applyInternalPrivacy(data) {
return data.map(entry => ({
...entry,
sessionId: this.hashValue(entry.sessionId),
feedback: {
...entry.feedback,
strengths: this.generalizeTextArray(entry.feedback.strengths),
weaknesses: this.generalizeTextArray(entry.feedback.weaknesses),
suggestions: this.generalizeTextArray(entry.feedback.suggestions)
}
}));
}
}
//# sourceMappingURL=ai-feedback-privacy-aggregator.js.map