edsger 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,11 +3,13 @@
3
3
  * Verifies that all PR review comments have been addressed and resolves them
4
4
  * Uses GitHub GraphQL API to accurately detect unresolved review threads
5
5
  */
6
+ import { EdsgerConfig } from '../../types/index.js';
6
7
  export interface CodeRefineVerificationOptions {
7
8
  featureId: string;
8
9
  mcpServerUrl: string;
9
10
  mcpToken: string;
10
11
  githubToken: string;
12
+ config: EdsgerConfig;
11
13
  verbose?: boolean;
12
14
  }
13
15
  export interface CodeRefineVerificationData {
@@ -42,7 +44,7 @@ export interface CodeRefineVerificationResult {
42
44
  data: CodeRefineVerificationData;
43
45
  }
44
46
  /**
45
- * Verify and resolve PR review comments using GraphQL API
47
+ * Verify and resolve PR review comments using GraphQL API and LLM analysis
46
48
  */
47
49
  export declare function verifyAndResolveComments(options: CodeRefineVerificationOptions): Promise<CodeRefineVerificationResult>;
48
50
  export declare function checkCodeRefineVerificationRequirements(): Promise<boolean>;
@@ -4,9 +4,42 @@
4
4
  * Uses GitHub GraphQL API to accurately detect unresolved review threads
5
5
  */
6
6
  import { Octokit } from '@octokit/rest';
7
+ import { query } from '@anthropic-ai/claude-code';
7
8
  import { logInfo, logError } from '../../utils/logger.js';
8
9
  import { parsePullRequestUrl, fetchPRReviews, } from '../code-refine/context-fetcher.js';
9
10
  import { getFeature } from '../../api/features/get-feature.js';
11
+ /**
12
+ * Fetch PR file changes (diff information)
13
+ */
14
+ async function fetchPRFileChanges(octokit, owner, repo, prNumber, verbose) {
15
+ try {
16
+ if (verbose) {
17
+ logInfo('📂 Fetching PR file changes...');
18
+ }
19
+ const { data: files } = await octokit.pulls.listFiles({
20
+ owner,
21
+ repo,
22
+ pull_number: prNumber,
23
+ per_page: 100,
24
+ });
25
+ const fileChanges = files.map((file) => ({
26
+ filename: file.filename,
27
+ status: file.status,
28
+ additions: file.additions,
29
+ deletions: file.deletions,
30
+ changes: file.changes,
31
+ patch: file.patch,
32
+ }));
33
+ if (verbose) {
34
+ logInfo(`✅ Found ${fileChanges.length} changed files`);
35
+ }
36
+ return fileChanges;
37
+ }
38
+ catch (error) {
39
+ logError(`Failed to fetch PR file changes: ${error}`);
40
+ return [];
41
+ }
42
+ }
10
43
  /**
11
44
  * Fetch unresolved review threads using GitHub GraphQL API
12
45
  * This provides accurate resolution status unlike REST API
@@ -78,22 +111,152 @@ async function fetchUnresolvedReviewThreads(octokit, owner, repo, prNumber, verb
78
111
  }
79
112
  }
80
113
  /**
81
- * Analyze why a comment thread is unresolved and provide specific reason
82
- * Note: Outdated threads are auto-resolved, so this only analyzes truly unresolved threads
114
+ * Analyze whether a review thread has been addressed by examining code changes
115
+ * Uses LLM to intelligently determine if the feedback was addressed
83
116
  */
84
- function analyzeUnresolvedThread(thread) {
117
+ async function analyzeThreadWithLLM(thread, fileChange, config, verbose) {
85
118
  const firstComment = thread.comments.nodes[0];
86
119
  if (!firstComment) {
87
- return 'Comment thread exists but has no comments';
120
+ return {
121
+ isAddressed: false,
122
+ reason: 'Comment thread exists but has no comments',
123
+ };
88
124
  }
89
- const reasons = [];
90
- // Since outdated threads are filtered out, remaining threads need code changes
91
- reasons.push('Code at this location has not been modified to address the feedback');
92
- // Check if there are multiple comments (discussion ongoing)
93
- if (thread.comments.totalCount > 1) {
94
- reasons.push(`Discussion ongoing (${thread.comments.totalCount} comments in thread)`);
125
+ // If file was not changed at all, feedback definitely not addressed
126
+ if (!fileChange || !fileChange.patch) {
127
+ return {
128
+ isAddressed: false,
129
+ reason: `File ${firstComment.path} has not been modified in this PR`,
130
+ };
131
+ }
132
+ // If file was deleted, thread should be resolved as obsolete
133
+ if (fileChange.status === 'removed') {
134
+ return {
135
+ isAddressed: true,
136
+ reason: 'File has been removed',
137
+ };
138
+ }
139
+ try {
140
+ if (verbose) {
141
+ logInfo(`🤖 Using LLM to analyze if comment in ${firstComment.path}:${firstComment.line} has been addressed...`);
142
+ }
143
+ const analysisPrompt = createThreadAnalysisPrompt(thread, fileChange, firstComment);
144
+ let lastResponse = '';
145
+ let analysisResult = null;
146
+ function* userMessage() {
147
+ yield {
148
+ type: 'user',
149
+ message: { role: 'user', content: analysisPrompt },
150
+ };
151
+ }
152
+ for await (const message of query({
153
+ prompt: userMessage(),
154
+ options: {
155
+ model: config.claude.model || 'sonnet',
156
+ maxTurns: 10,
157
+ permissionMode: 'bypassPermissions',
158
+ },
159
+ })) {
160
+ if (message.type === 'assistant' && message.message?.content) {
161
+ for (const content of message.message.content) {
162
+ if (content.type === 'text') {
163
+ lastResponse += content.text + '\n';
164
+ }
165
+ }
166
+ }
167
+ if (message.type === 'result') {
168
+ if (message.subtype === 'success') {
169
+ const responseText = message.result || lastResponse;
170
+ // Try to extract JSON from response
171
+ const jsonMatch = responseText.match(/```json\s*\n([\s\S]*?)\n\s*```/);
172
+ if (jsonMatch) {
173
+ analysisResult = JSON.parse(jsonMatch[1]);
174
+ }
175
+ else {
176
+ // Try to parse directly
177
+ try {
178
+ analysisResult = JSON.parse(responseText);
179
+ }
180
+ catch {
181
+ // Fallback: look for isAddressed boolean in text
182
+ const isAddressedMatch = /isAddressed["']?\s*:\s*(true|false)/i.exec(responseText);
183
+ if (isAddressedMatch) {
184
+ analysisResult = {
185
+ isAddressed: isAddressedMatch[1].toLowerCase() === 'true',
186
+ reason: responseText.split('\n').find((line) => line.trim()) ||
187
+ 'Analysis completed',
188
+ };
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+ if (analysisResult) {
196
+ if (verbose) {
197
+ logInfo(` ${analysisResult.isAddressed ? '✅' : '❌'} ${analysisResult.reason}`);
198
+ }
199
+ return analysisResult;
200
+ }
201
+ // Fallback if LLM analysis failed
202
+ return {
203
+ isAddressed: false,
204
+ reason: 'Unable to analyze - please review manually',
205
+ };
206
+ }
207
+ catch (error) {
208
+ if (verbose) {
209
+ logError(`LLM analysis failed: ${error}`);
210
+ }
211
+ return {
212
+ isAddressed: false,
213
+ reason: 'Analysis failed - please review manually',
214
+ };
95
215
  }
96
- return reasons.join('; ');
216
+ }
217
+ /**
218
+ * Create prompt for LLM to analyze if a comment thread has been addressed
219
+ */
220
+ function createThreadAnalysisPrompt(thread, fileChange, firstComment) {
221
+ const allComments = thread.comments.nodes
222
+ .map((c, idx) => `Comment ${idx + 1} by @${c.author.login}:\n${c.body}`)
223
+ .join('\n\n');
224
+ return `You are analyzing whether a code review comment has been addressed by subsequent code changes.
225
+
226
+ **Review Thread Information:**
227
+ - File: ${firstComment.path}
228
+ - Line: ${firstComment.line || 'N/A'}
229
+ - Thread has ${thread.comments.totalCount} comment(s)
230
+
231
+ **Review Comments:**
232
+ ${allComments}
233
+
234
+ **Code Changes in This File:**
235
+ \`\`\`diff
236
+ ${fileChange.patch || '(No patch available)'}
237
+ \`\`\`
238
+
239
+ **Your Task:**
240
+ Analyze whether the code changes adequately address the feedback in the review comments. Consider:
241
+ 1. Does the feedback request a specific code change?
242
+ 2. Have those changes (or equivalent changes) been made?
243
+ 3. If the feedback points to a specific line, have related areas of code been modified to address the concern?
244
+
245
+ **Important:** The comment may point to a specific line, but the fix might be in nearby code. Focus on whether the **underlying issue** was addressed, not just whether that exact line was modified.
246
+
247
+ Return your analysis in this JSON format:
248
+ \`\`\`json
249
+ {
250
+ "isAddressed": true or false,
251
+ "reason": "Brief explanation of why you determined the feedback was or was not addressed"
252
+ }
253
+ \`\`\`
254
+
255
+ Example responses:
256
+ - If a comment says "add null check" and the diff shows a null check was added nearby: {"isAddressed": true, "reason": "Null check added in related code"}
257
+ - If a comment suggests a refactor but no relevant changes are visible: {"isAddressed": false, "reason": "No refactoring changes visible in diff"}
258
+ - If code was modified in related areas addressing the concern: {"isAddressed": true, "reason": "Related code modified to address the concern"}
259
+ `;
97
260
  }
98
261
  /**
99
262
  * Mark review threads as resolved using GraphQL API
@@ -132,10 +295,10 @@ async function resolveReviewThreads(octokit, threads, verbose) {
132
295
  return markedCount;
133
296
  }
134
297
  /**
135
- * Verify and resolve PR review comments using GraphQL API
298
+ * Verify and resolve PR review comments using GraphQL API and LLM analysis
136
299
  */
137
300
  export async function verifyAndResolveComments(options) {
138
- const { featureId, mcpServerUrl, mcpToken, githubToken, verbose } = options;
301
+ const { featureId, mcpServerUrl, mcpToken, githubToken, config, verbose } = options;
139
302
  if (verbose) {
140
303
  logInfo(`Starting code refine verification for feature ID: ${featureId}`);
141
304
  }
@@ -155,10 +318,11 @@ export async function verifyAndResolveComments(options) {
155
318
  const octokit = new Octokit({
156
319
  auth: githubToken,
157
320
  });
158
- // Fetch unresolved review threads using GraphQL API
159
- const [unresolvedThreads, reviews] = await Promise.all([
321
+ // Fetch unresolved review threads, reviews, and file changes
322
+ const [unresolvedThreads, reviews, fileChanges] = await Promise.all([
160
323
  fetchUnresolvedReviewThreads(octokit, owner, repo, prNumber, verbose),
161
324
  fetchPRReviews(octokit, owner, repo, prNumber, verbose),
325
+ fetchPRFileChanges(octokit, owner, repo, prNumber, verbose),
162
326
  ]);
163
327
  // Check if there's anything to verify
164
328
  if (reviews.length === 0 && unresolvedThreads.length === 0) {
@@ -178,16 +342,46 @@ export async function verifyAndResolveComments(options) {
178
342
  },
179
343
  };
180
344
  }
345
+ // Use LLM to intelligently analyze unresolved threads
346
+ if (verbose && unresolvedThreads.length > 0) {
347
+ logInfo(`🔍 Analyzing ${unresolvedThreads.length} unresolved threads with LLM...`);
348
+ }
349
+ // Analyze each thread with LLM to determine if it's truly unresolved
350
+ const threadAnalysisResults = await Promise.all(unresolvedThreads.map(async (thread) => {
351
+ const firstComment = thread.comments.nodes[0];
352
+ const fileChange = fileChanges.find((fc) => fc.filename === firstComment?.path);
353
+ const analysis = await analyzeThreadWithLLM(thread, fileChange, config, verbose);
354
+ return {
355
+ thread,
356
+ analysis,
357
+ };
358
+ }));
359
+ // Separate threads that LLM determined are addressed vs not addressed
360
+ const addressedThreads = threadAnalysisResults.filter((result) => result.analysis.isAddressed);
361
+ const trulyUnresolvedThreads = threadAnalysisResults.filter((result) => !result.analysis.isAddressed);
362
+ if (verbose) {
363
+ logInfo(`📊 LLM Analysis: ${addressedThreads.length} threads addressed, ${trulyUnresolvedThreads.length} still need attention`);
364
+ }
365
+ // Auto-resolve threads that LLM determined are addressed
366
+ if (addressedThreads.length > 0) {
367
+ if (verbose) {
368
+ logInfo(`✅ Auto-resolving ${addressedThreads.length} threads that have been addressed...`);
369
+ }
370
+ const resolvedCount = await resolveReviewThreads(octokit, addressedThreads.map((r) => r.thread), verbose);
371
+ if (verbose) {
372
+ logInfo(`✅ Successfully resolved ${resolvedCount} threads`);
373
+ }
374
+ }
181
375
  // Check reviews - they need to be dismissed or re-reviewed
182
376
  const unresolvedReviews = reviews.filter((review) => review.state === 'CHANGES_REQUESTED');
183
377
  if (verbose) {
184
- logInfo(`📊 Review Threads: ${unresolvedThreads.length} unresolved`);
378
+ logInfo(`📊 Review Threads: ${trulyUnresolvedThreads.length} still unresolved (after LLM analysis)`);
185
379
  if (reviews.length > 0) {
186
380
  logInfo(`📊 Reviews: ${reviews.length - unresolvedReviews.length} addressed, ${unresolvedReviews.length} still requesting changes`);
187
381
  }
188
382
  }
189
- // If all threads are resolved AND no reviews requesting changes, success
190
- if (unresolvedThreads.length === 0 && unresolvedReviews.length === 0) {
383
+ // If all threads are truly resolved (after LLM analysis) AND no reviews requesting changes, success
384
+ if (trulyUnresolvedThreads.length === 0 && unresolvedReviews.length === 0) {
191
385
  if (verbose) {
192
386
  logInfo('✅ All comments have been addressed! All review threads are resolved.');
193
387
  }
@@ -202,13 +396,14 @@ export async function verifyAndResolveComments(options) {
202
396
  totalReviews: reviews.length,
203
397
  unresolvedReviews: 0,
204
398
  totalComments: unresolvedThreads.length,
205
- resolvedComments: 0,
399
+ resolvedComments: addressedThreads.length,
206
400
  unresolvedComments: 0,
401
+ commentsMarkedResolved: addressedThreads.length,
207
402
  },
208
403
  };
209
404
  }
210
405
  else {
211
- // Verification failed - build detailed info with specific failure reasons
406
+ // Verification failed - build detailed info with specific failure reasons from LLM analysis
212
407
  if (verbose) {
213
408
  if (unresolvedReviews.length > 0) {
214
409
  logInfo(`⚠️ ${unresolvedReviews.length} reviews still requesting changes`);
@@ -219,27 +414,26 @@ export async function verifyAndResolveComments(options) {
219
414
  }
220
415
  });
221
416
  }
222
- if (unresolvedThreads.length > 0) {
223
- logInfo(`⚠️ ${unresolvedThreads.length} review threads still need to be addressed`);
224
- unresolvedThreads.forEach((thread) => {
225
- const firstComment = thread.comments.nodes[0];
417
+ if (trulyUnresolvedThreads.length > 0) {
418
+ logInfo(`⚠️ ${trulyUnresolvedThreads.length} review threads still need to be addressed`);
419
+ trulyUnresolvedThreads.forEach((result) => {
420
+ const firstComment = result.thread.comments.nodes[0];
226
421
  if (firstComment) {
227
422
  logInfo(` - Comment by @${firstComment.author.login}`);
228
423
  logInfo(` File: ${firstComment.path}:${firstComment.line || '?'}`);
229
424
  logInfo(` ${firstComment.body.substring(0, 100)}...`);
230
- logInfo(` Reason: ${analyzeUnresolvedThread(thread)}`);
425
+ logInfo(` LLM Analysis: ${result.analysis.reason}`);
231
426
  }
232
427
  });
233
428
  }
234
429
  }
235
- // Build targeted suggestions based on specific unresolved issues
430
+ // Build targeted suggestions based on LLM analysis
236
431
  const suggestions = [];
237
- // Create specific suggestions for each unresolved comment
238
- unresolvedThreads.forEach((thread, index) => {
239
- const firstComment = thread.comments.nodes[0];
432
+ // Create specific suggestions for each truly unresolved comment (based on LLM analysis)
433
+ trulyUnresolvedThreads.forEach((result, index) => {
434
+ const firstComment = result.thread.comments.nodes[0];
240
435
  if (firstComment) {
241
- const reason = analyzeUnresolvedThread(thread);
242
- suggestions.push(`${index + 1}. [${firstComment.path}:${firstComment.line || '?'}] by @${firstComment.author.login}: ${reason}`);
436
+ suggestions.push(`${index + 1}. [${firstComment.path}:${firstComment.line || '?'}] by @${firstComment.author.login}: ${result.analysis.reason}`);
243
437
  }
244
438
  });
245
439
  // Add review-specific suggestions if any
@@ -249,16 +443,16 @@ export async function verifyAndResolveComments(options) {
249
443
  suggestions.push(` - @${review.user.login}: ${review.body ? review.body.substring(0, 150) : 'No details provided'}${review.body && review.body.length > 150 ? '...' : ''}`);
250
444
  });
251
445
  }
252
- // Build detailed unresolved info with failure reasons
253
- const unresolvedCommentDetails = unresolvedThreads.map((thread) => {
254
- const firstComment = thread.comments.nodes[0];
446
+ // Build detailed unresolved info with LLM-analyzed failure reasons
447
+ const unresolvedCommentDetails = trulyUnresolvedThreads.map((result) => {
448
+ const firstComment = result.thread.comments.nodes[0];
255
449
  return {
256
450
  commentId: firstComment.id,
257
451
  author: firstComment.author.login,
258
452
  file: firstComment.path,
259
453
  line: firstComment.line,
260
454
  body: firstComment.body,
261
- failureReason: analyzeUnresolvedThread(thread),
455
+ failureReason: result.analysis.reason, // Use LLM analysis result
262
456
  url: firstComment.url,
263
457
  };
264
458
  });
@@ -270,14 +464,14 @@ export async function verifyAndResolveComments(options) {
270
464
  submittedAt: review.submitted_at,
271
465
  }));
272
466
  let errorMessage = '';
273
- if (unresolvedReviews.length > 0 && unresolvedThreads.length > 0) {
274
- errorMessage = `${unresolvedReviews.length} reviews and ${unresolvedThreads.length} review threads still need to be addressed`;
467
+ if (unresolvedReviews.length > 0 && trulyUnresolvedThreads.length > 0) {
468
+ errorMessage = `${unresolvedReviews.length} reviews and ${trulyUnresolvedThreads.length} review threads still need to be addressed (based on LLM analysis)`;
275
469
  }
276
470
  else if (unresolvedReviews.length > 0) {
277
471
  errorMessage = `${unresolvedReviews.length} reviews still requesting changes`;
278
472
  }
279
473
  else {
280
- errorMessage = `${unresolvedThreads.length} review comments still need to be addressed`;
474
+ errorMessage = `${trulyUnresolvedThreads.length} review comments still need to be addressed (based on LLM analysis)`;
281
475
  }
282
476
  return {
283
477
  status: 'error',
@@ -286,9 +480,10 @@ export async function verifyAndResolveComments(options) {
286
480
  featureId,
287
481
  totalReviews: reviews.length,
288
482
  unresolvedReviews: unresolvedReviews.length,
289
- totalComments: unresolvedThreads.length,
290
- resolvedComments: 0,
291
- unresolvedComments: unresolvedThreads.length,
483
+ totalComments: unresolvedThreads.length, // Original count before LLM analysis
484
+ resolvedComments: addressedThreads.length, // LLM determined these are addressed
485
+ unresolvedComments: trulyUnresolvedThreads.length, // LLM determined these still need work
486
+ commentsMarkedResolved: addressedThreads.length,
292
487
  suggestions,
293
488
  unresolvedReviewDetails,
294
489
  unresolvedCommentDetails,
@@ -42,6 +42,7 @@ const executeCodeRefineVerification = async (options, config) => {
42
42
  mcpServerUrl: options.mcpServerUrl,
43
43
  mcpToken: options.mcpToken,
44
44
  githubToken,
45
+ config, // Add config parameter for LLM analysis
45
46
  verbose: options.verbose,
46
47
  });
47
48
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {