Spaces:
Paused
Paused
| ; | |
| 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 = '<!-- prix_feedback_tracking -->'; | |
| 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 |