"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.feedbackCapture = exports.FeedbackCapture = void 0; const octokit_1 = require("./octokit"); const context_1 = require("./context"); const feedback_tracker_1 = require("./feedback-tracker"); const lru_cache_1 = require("lru-cache"); const pino_1 = __importDefault(require("pino")); const logger = (0, pino_1.default)({ level: process.env.LOG_LEVEL || 'info' }); const FEEDBACK_TAG = ''; class FeedbackCapture { // SECURITY: LRU caches prevent unbounded memory growth (Issue #5) commentMetadata; pendingFeedback; constructor() { // Limit to 1000 entries per PR with 1 hour TTL this.commentMetadata = new lru_cache_1.LRUCache({ max: 1000, ttl: 1000 * 60 * 60, // 1 hour updateAgeOnGet: true, dispose: (value, key) => { logger.debug({ key, pullNumber: value.pullNumber }, 'Comment metadata evicted'); } }); this.pendingFeedback = new lru_cache_1.LRUCache({ max: 1000, ttl: 1000 * 60 * 60, // 1 hour updateAgeOnGet: true }); } getCommentKey(pullNumber, path, startLine) { return `${pullNumber}:${path}:${startLine}`; } trackComment(pullNumber, path, startLine, patternType, severity, confidence, commentId) { const key = this.getCommentKey(pullNumber, path, startLine); this.commentMetadata.set(key, { commentId, pullNumber, path, patternType, severity, confidence }); } async checkSuggestionApplied(pullNumber, path, startLine) { const key = this.getCommentKey(pullNumber, path, startLine); const metadata = this.commentMetadata.get(key); if (!metadata || !metadata.commentId) return false; try { const comments = await octokit_1.octokit.rest.pulls.listReviewComments({ owner: context_1.als.getStore()?.repo.owner || '', repo: context_1.als.getStore()?.repo.repo || '', // eslint-disable-next-line camelcase pull_number: pullNumber }); const originalComment = comments.data.find((c) => c.id === metadata.commentId); if (!originalComment) return false; const diff = await this.getFileDiffAtComment(pullNumber, path, startLine, originalComment.original_line || startLine); const hasAppliedSuggestion = this.detectSuggestionApplication(originalComment.body || '', diff); if (hasAppliedSuggestion) { this.recordFeedback(key, 'accepted'); } return hasAppliedSuggestion; } catch (e) { logger.warn({ error: e }, 'Failed to check suggestion application'); return false; } } async getFileDiffAtComment(pullNumber, path, startLine, originalLine) { try { const diff = await octokit_1.octokit.rest.repos.compareCommits({ owner: context_1.als.getStore()?.repo.owner || '', repo: context_1.als.getStore()?.repo.repo || '', base: `${pullNumber}-base`, head: `${pullNumber}-head` }); const file = diff.data.files?.find((f) => f.filename === path); if (!file?.patch) return ''; const hunk = file.patch .split('\n') .slice(Math.max(0, originalLine - startLine - 1), originalLine - startLine + 5) .join('\n'); return hunk; } catch (e) { return ''; } } detectSuggestionApplication(originalBody, currentDiff) { if (!originalBody.includes('```suggestion')) return false; if (!currentDiff) return false; const suggestionMatch = originalBody.match(/```suggestion\n([\s\S]*?)```/); if (!suggestionMatch) return false; const suggestedCode = suggestionMatch[1].trim(); const hasApplied = currentDiff.includes(suggestedCode.substring(0, Math.min(50, suggestedCode.length))); return hasApplied; } recordFeedback(key, type) { const metadata = this.commentMetadata.get(key); if (!metadata) return; this.pendingFeedback.set(key, type); const ctx = context_1.als.getStore(); const teamId = ctx?.repo?.owner || 'default'; const repoSlug = ctx?.repo?.repo || 'default'; feedback_tracker_1.feedbackTracker.recordFeedback({ patternType: metadata.patternType, severity: metadata.severity, filenamePattern: metadata.path, repoSlug, teamId, confidence: metadata.confidence, feedbackType: type }); logger.info({ key, type }, 'Feedback recorded'); } async checkReactionFeedback(pullNumber) { const ctx = context_1.als.getStore(); const teamId = ctx?.repo?.owner || 'default'; const repoSlug = ctx?.repo?.repo || 'default'; try { const comments = await octokit_1.octokit.rest.pulls.listReviewComments({ owner: teamId, repo: repoSlug, // eslint-disable-next-line camelcase pull_number: pullNumber }); for (const comment of comments.data) { if (!comment.body?.includes(FEEDBACK_TAG)) continue; const reactions = await octokit_1.octokit.rest.reactions.listForIssueComment({ owner: teamId, repo: repoSlug, // eslint-disable-next-line camelcase comment_id: comment.id }); for (const reaction of reactions.data) { const key = this.getCommentKey(pullNumber, comment.path || '', comment.line || comment.original_line || 0); if (reaction.content === '+1' || reaction.content === 'heart') { this.recordFeedback(key, 'accepted'); } else if (reaction.content === '-1' || reaction.content === 'eyes') { this.recordFeedback(key, 'rejected'); } } } } catch (e) { logger.warn({ error: e }, 'Failed to check reaction feedback'); } } async trackReviewSubmission(pullNumber, reviewId, commitId) { const ctx = context_1.als.getStore(); const teamId = ctx?.repo?.owner || 'default'; const repoSlug = ctx?.repo?.repo || 'default'; logger.info({ pullNumber, reviewId }, 'Tracking review submission'); try { const comments = await octokit_1.octokit.rest.pulls.listReviewComments({ owner: teamId, repo: repoSlug, // eslint-disable-next-line camelcase pull_number: pullNumber }); for (const comment of comments.data) { if (!comment.body?.includes(FEEDBACK_TAG)) continue; if (comment.user?.login !== 'github-actions[bot]') continue; const metadataMatch = comment.body.match(/data-pattern="([^"]+)"/); const severityMatch = comment.body.match(/data-severity="([^"]+)"/); const confidenceMatch = comment.body.match(/data-confidence="([^"]+)"/); if (metadataMatch && severityMatch && confidenceMatch) { const key = this.getCommentKey(pullNumber, comment.path || '', comment.line || comment.original_line || 0); this.commentMetadata.set(key, { commentId: comment.id, pullNumber, path: comment.path || '', patternType: metadataMatch[1], severity: severityMatch[1], confidence: parseFloat(confidenceMatch[1]) }); } } } catch (e) { logger.warn({ error: e }, 'Failed to track review submission'); } } getFeedbackStats(teamId, repoSlug) { return feedback_tracker_1.feedbackTracker.getTeamInsights(teamId, repoSlug); } shouldSuppressFinding(patternType, filename, teamId, repoSlug) { return feedback_tracker_1.feedbackTracker.shouldSuppressPattern(patternType, teamId, repoSlug); } } exports.FeedbackCapture = FeedbackCapture; exports.feedbackCapture = new FeedbackCapture(); //# sourceMappingURL=feedback-capture.js.map