| "use strict"; |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.feedbackTracker = exports.FeedbackTracker = void 0; |
| const fs_1 = require("fs"); |
| const path_1 = require("path"); |
| const minimatch_1 = require("minimatch"); |
| const info = console.log; |
| function getPersistBaseDir() { |
| if (process.env.PRIX_DATA_DIR) |
| return process.env.PRIX_DATA_DIR; |
| if (process.env.HF_HOME) |
| return (0, path_1.join)(process.env.HF_HOME, 'prix-data'); |
| if (process.env.HUGGING_FACE_HUB_TOKEN) |
| return '/tmp/prix-hf'; |
| return '/tmp/prix'; |
| } |
| class FeedbackTracker { |
| feedbackStore = []; |
| teamInsights = new Map(); |
| persistPath; |
| maxStoreSize = 10000; |
| initialized = false; |
| constructor(persistDir) { |
| const baseDir = persistDir || getPersistBaseDir(); |
| this.persistPath = (0, path_1.join)(baseDir, 'feedback'); |
| this.ensureStorageDir(); |
| this.loadPersisted(); |
| } |
| ensureStorageDir() { |
| try { |
| if (!(0, fs_1.existsSync)(this.persistPath)) { |
| (0, fs_1.mkdirSync)(this.persistPath, { recursive: true }); |
| } |
| } |
| catch (e) { |
| info(`Failed to create feedback storage dir: ${e}`); |
| } |
| } |
| loadPersisted() { |
| try { |
| const feedbackFile = (0, path_1.join)(this.persistPath, 'feedback.json'); |
| if ((0, fs_1.existsSync)(feedbackFile)) { |
| const data = JSON.parse((0, fs_1.readFileSync)(feedbackFile, 'utf8')); |
| if (Array.isArray(data)) { |
| this.feedbackStore = data.slice(-this.maxStoreSize); |
| this.rebuildTeamInsights(); |
| this.initialized = true; |
| info(`Loaded ${this.feedbackStore.length} feedback entries`); |
| } |
| } |
| } |
| catch (e) { |
| info(`Failed to load feedback data: ${e}`); |
| } |
| } |
| persist() { |
| try { |
| const feedbackFile = (0, path_1.join)(this.persistPath, 'feedback.json'); |
| (0, fs_1.writeFileSync)(feedbackFile, JSON.stringify(this.feedbackStore.slice(-this.maxStoreSize))); |
| } |
| catch (e) { |
| info(`Failed to persist feedback: ${e}`); |
| } |
| } |
| rebuildTeamInsights() { |
| const teamMap = new Map(); |
| for (const fb of this.feedbackStore) { |
| const key = `${fb.teamId}:${fb.repoSlug}`; |
| if (!teamMap.has(key)) { |
| teamMap.set(key, []); |
| } |
| teamMap.get(key).push(fb); |
| } |
| for (const [key, feedbacks] of teamMap) { |
| const [teamId, repoSlug] = key.split(':'); |
| const insights = this.computeInsights(teamId, repoSlug, feedbacks); |
| this.teamInsights.set(key, insights); |
| } |
| } |
| computeInsights(teamId, repoSlug, feedbacks) { |
| const patternMap = new Map(); |
| for (const fb of feedbacks) { |
| if (!patternMap.has(fb.patternType)) { |
| patternMap.set(fb.patternType, []); |
| } |
| patternMap.get(fb.patternType).push(fb); |
| } |
| const patternStats = new Map(); |
| for (const [patternType, pFeedbacks] of patternMap) { |
| const accepted = pFeedbacks.filter(f => f.feedbackType === 'accepted').length; |
| const rejected = pFeedbacks.filter(f => f.feedbackType === 'rejected').length; |
| const ignored = pFeedbacks.filter(f => f.feedbackType === 'ignored').length; |
| const total = pFeedbacks.length; |
| const avgConfidence = pFeedbacks.reduce((sum, f) => sum + f.confidence, 0) / total; |
| patternStats.set(patternType, { |
| patternType, |
| total, |
| accepted, |
| rejected, |
| ignored, |
| acceptanceRate: total > 0 ? accepted / total : 0, |
| avgConfidence |
| }); |
| } |
| const allAccepted = feedbacks.filter(f => f.feedbackType === 'accepted').length; |
| const overallAcceptanceRate = feedbacks.length > 0 ? allAccepted / feedbacks.length : 0; |
| return { |
| teamId, |
| repoSlug, |
| patternStats, |
| overallAcceptanceRate, |
| lastUpdated: Date.now() |
| }; |
| } |
| recordFeedback(feedback) { |
| const id = `fb_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; |
| const fullFeedback = { |
| ...feedback, |
| id, |
| timestamp: Date.now() |
| }; |
| this.feedbackStore.push(fullFeedback); |
| if (this.feedbackStore.length > this.maxStoreSize) { |
| this.feedbackStore = this.feedbackStore.slice(-this.maxStoreSize); |
| } |
| const key = `${feedback.teamId}:${feedback.repoSlug}`; |
| const existing = this.teamInsights.get(key); |
| if (existing) { |
| const updatedFeedbacks = [ |
| ...this.feedbackStore.filter(f => f.teamId === feedback.teamId && f.repoSlug === feedback.repoSlug) |
| ]; |
| const insights = this.computeInsights(feedback.teamId, feedback.repoSlug, updatedFeedbacks); |
| this.teamInsights.set(key, insights); |
| } |
| this.persist(); |
| return id; |
| } |
| recordAcceptance(patternType, severity, filename, repoSlug, teamId, confidence, commentId, reviewId) { |
| return this.recordFeedback({ |
| patternType, |
| severity, |
| filenamePattern: filename, |
| repoSlug, |
| teamId, |
| confidence, |
| feedbackType: 'accepted', |
| commentId, |
| reviewId |
| }); |
| } |
| recordRejection(patternType, severity, filename, repoSlug, teamId, confidence, commentId, reviewId) { |
| return this.recordFeedback({ |
| patternType, |
| severity, |
| filenamePattern: filename, |
| repoSlug, |
| teamId, |
| confidence, |
| feedbackType: 'rejected', |
| commentId, |
| reviewId |
| }); |
| } |
| recordIgnored(patternType, severity, filename, repoSlug, teamId, confidence, commentId, reviewId) { |
| return this.recordFeedback({ |
| patternType, |
| severity, |
| filenamePattern: filename, |
| repoSlug, |
| teamId, |
| confidence, |
| feedbackType: 'ignored', |
| commentId, |
| reviewId |
| }); |
| } |
| getPatternAcceptanceRate(patternType, teamId, repoSlug) { |
| const key = `${teamId}:${repoSlug}`; |
| const insights = this.teamInsights.get(key); |
| if (!insights) |
| return 0.5; |
| const stats = insights.patternStats.get(patternType); |
| if (!stats) |
| return 0.5; |
| if (stats.total < 5) |
| return 0.5; |
| return stats.acceptanceRate; |
| } |
| getFilenameAcceptanceRate(filenamePattern, teamId, repoSlug) { |
| const feedbacks = this.feedbackStore.filter(f => f.teamId === teamId && |
| f.repoSlug === repoSlug && |
| f.feedbackType !== 'ignored'); |
| if (feedbacks.length === 0) |
| return 0.5; |
| const matching = feedbacks.filter(f => (0, minimatch_1.minimatch)(f.filenamePattern, filenamePattern) || |
| (0, minimatch_1.minimatch)(filenamePattern, f.filenamePattern)); |
| if (matching.length === 0) |
| return 0.5; |
| const accepted = matching.filter(f => f.feedbackType === 'accepted').length; |
| return accepted / matching.length; |
| } |
| getTeamInsights(teamId, repoSlug) { |
| const key = `${teamId}:${repoSlug}`; |
| return this.teamInsights.get(key) || null; |
| } |
| getAdjustedConfidence(baseConfidence, patternType, filename, teamId, repoSlug) { |
| const patternRate = this.getPatternAcceptanceRate(patternType, teamId, repoSlug); |
| const filenameRate = this.getFilenameAcceptanceRate(filename, teamId, repoSlug); |
| const feedbackWeight = 0.3; |
| const teamRate = patternRate * 0.6 + filenameRate * 0.4; |
| const adjustedConfidence = baseConfidence * (1 - feedbackWeight) + teamRate * 100 * feedbackWeight; |
| return Math.max(0, Math.min(100, adjustedConfidence)); |
| } |
| shouldSuppressPattern(patternType, teamId, repoSlug, threshold = 0.2, minSamples = 10) { |
| const key = `${teamId}:${repoSlug}`; |
| const insights = this.teamInsights.get(key); |
| if (!insights) |
| return false; |
| const stats = insights.patternStats.get(patternType); |
| if (!stats || stats.total < minSamples) |
| return false; |
| return stats.acceptanceRate < threshold; |
| } |
| getQuery(query) { |
| let results = [...this.feedbackStore]; |
| if (query.teamId) { |
| results = results.filter(f => f.teamId === query.teamId); |
| } |
| if (query.repoSlug) { |
| results = results.filter(f => f.repoSlug === query.repoSlug); |
| } |
| if (query.patternType) { |
| results = results.filter(f => f.patternType === query.patternType); |
| } |
| if (query.filenamePattern) { |
| results = results.filter(f => (0, minimatch_1.minimatch)(f.filenamePattern, query.filenamePattern)); |
| } |
| if (query.since) { |
| results = results.filter(f => f.timestamp >= query.since); |
| } |
| if (query.limit) { |
| results = results.slice(-query.limit); |
| } |
| return results; |
| } |
| getSummary(teamId, repoSlug) { |
| let feedbacks = [...this.feedbackStore]; |
| if (teamId) { |
| feedbacks = feedbacks.filter(f => f.teamId === teamId); |
| } |
| if (repoSlug) { |
| feedbacks = feedbacks.filter(f => f.repoSlug === repoSlug); |
| } |
| const patternMap = new Map(); |
| const repoMap = new Map(); |
| const teamMap = new Map(); |
| for (const fb of feedbacks) { |
| if (!patternMap.has(fb.patternType)) { |
| patternMap.set(fb.patternType, []); |
| } |
| patternMap.get(fb.patternType).push(fb); |
| repoMap.set(fb.repoSlug, (repoMap.get(fb.repoSlug) || 0) + 1); |
| teamMap.set(fb.teamId, (teamMap.get(fb.teamId) || 0) + 1); |
| } |
| const byPattern = new Map(); |
| for (const [patternType, pFeedbacks] of patternMap) { |
| const accepted = pFeedbacks.filter(f => f.feedbackType === 'accepted').length; |
| const rejected = pFeedbacks.filter(f => f.feedbackType === 'rejected').length; |
| const ignored = pFeedbacks.filter(f => f.feedbackType === 'ignored').length; |
| const total = pFeedbacks.length; |
| const avgConfidence = pFeedbacks.reduce((sum, f) => sum + f.confidence, 0) / total; |
| byPattern.set(patternType, { |
| patternType, |
| total, |
| accepted, |
| rejected, |
| ignored, |
| acceptanceRate: total > 0 ? accepted / total : 0, |
| avgConfidence |
| }); |
| } |
| const recentWindow = 30 * 24 * 60 * 60 * 1000; |
| const recentFeedbacks = feedbacks.filter(f => f.timestamp > Date.now() - recentWindow); |
| const olderFeedbacks = feedbacks.filter(f => f.timestamp <= Date.now() - recentWindow); |
| const recentAcceptance = recentFeedbacks.filter(f => f.feedbackType === 'accepted').length / |
| (recentFeedbacks.length || 1); |
| const olderAcceptance = olderFeedbacks.filter(f => f.feedbackType === 'accepted').length / |
| (olderFeedbacks.length || 1); |
| let recentTrend = 'stable'; |
| if (recentAcceptance > olderAcceptance + 0.05) { |
| recentTrend = 'improving'; |
| } |
| else if (recentAcceptance < olderAcceptance - 0.05) { |
| recentTrend = 'declining'; |
| } |
| return { |
| totalFeedback: feedbacks.length, |
| byPattern, |
| byRepo: repoMap, |
| byTeam: teamMap, |
| recentTrend |
| }; |
| } |
| exportForTraining() { |
| return this.feedbackStore |
| .filter(f => f.feedbackType !== 'ignored') |
| .map(f => ({ |
| patternType: f.patternType, |
| severity: f.severity, |
| filenamePattern: f.filenamePattern, |
| confidence: f.confidence, |
| accepted: f.feedbackType === 'accepted', |
| repoSlug: f.repoSlug |
| })); |
| } |
| } |
| exports.FeedbackTracker = FeedbackTracker; |
| exports.feedbackTracker = new FeedbackTracker(); |
| |