edsger 0.4.2 → 0.4.4

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Context fetcher for code refine phase
3
- * Fetches GitHub PR review comments that request changes
3
+ * Fetches GitHub PR review comments and reviews using GraphQL API
4
4
  */
5
5
  import { Octokit } from '@octokit/rest';
6
6
  export interface PRReviewComment {
@@ -16,6 +16,7 @@ export interface PRReviewComment {
16
16
  original_position: number | null;
17
17
  diff_hunk: string | null;
18
18
  in_reply_to_id?: number;
19
+ url?: string;
19
20
  }
20
21
  export interface PRReview {
21
22
  id: number;
@@ -26,6 +27,17 @@ export interface PRReview {
26
27
  state: string;
27
28
  submitted_at: string | null;
28
29
  }
30
+ export interface PRReviewThread {
31
+ id: string;
32
+ isResolved: boolean;
33
+ isOutdated: boolean;
34
+ line: number | null;
35
+ originalLine: number | null;
36
+ startLine: number | null;
37
+ originalStartLine: number | null;
38
+ diffSide: string;
39
+ comments: PRReviewComment[];
40
+ }
29
41
  export interface CodeRefineContext {
30
42
  featureId: string;
31
43
  featureName: string;
@@ -36,6 +48,7 @@ export interface CodeRefineContext {
36
48
  repo: string;
37
49
  reviews: PRReview[];
38
50
  reviewComments: PRReviewComment[];
51
+ reviewThreads?: PRReviewThread[];
39
52
  technicalDesign?: string;
40
53
  userStories?: any[];
41
54
  testCases?: any[];
@@ -56,6 +69,13 @@ export declare function fetchPRReviews(octokit: Octokit, owner: string, repo: st
56
69
  * Fetch PR review comments
57
70
  */
58
71
  export declare function fetchPRReviewComments(octokit: Octokit, owner: string, repo: string, prNumber: number, verbose?: boolean): Promise<PRReviewComment[]>;
72
+ /**
73
+ * Fetch PR data using GraphQL API including review threads and reviews
74
+ */
75
+ export declare function fetchPRDataGraphQL(octokit: Octokit, owner: string, repo: string, prNumber: number, verbose?: boolean): Promise<{
76
+ reviewThreads: PRReviewThread[];
77
+ reviews: PRReview[];
78
+ }>;
59
79
  /**
60
80
  * Fetch user stories via MCP
61
81
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Context fetcher for code refine phase
3
- * Fetches GitHub PR review comments that request changes
3
+ * Fetches GitHub PR review comments and reviews using GraphQL API
4
4
  */
5
5
  import { Octokit } from '@octokit/rest';
6
6
  import { getFeature } from '../../api/features/get-feature.js';
@@ -54,6 +54,114 @@ export async function fetchPRReviewComments(octokit, owner, repo, prNumber, verb
54
54
  }
55
55
  return comments;
56
56
  }
57
+ /**
58
+ * Fetch PR data using GraphQL API including review threads and reviews
59
+ */
60
+ export async function fetchPRDataGraphQL(octokit, owner, repo, prNumber, verbose) {
61
+ if (verbose) {
62
+ console.log(`🔍 Fetching PR data via GraphQL for ${owner}/${repo}#${prNumber}...`);
63
+ }
64
+ const query = `
65
+ query($owner: String!, $repo: String!, $prNumber: Int!) {
66
+ repository(owner: $owner, name: $repo) {
67
+ pullRequest(number: $prNumber) {
68
+ reviews(first: 100) {
69
+ nodes {
70
+ id
71
+ databaseId
72
+ author {
73
+ login
74
+ }
75
+ body
76
+ state
77
+ submittedAt
78
+ }
79
+ }
80
+ reviewThreads(first: 100) {
81
+ nodes {
82
+ id
83
+ isResolved
84
+ isOutdated
85
+ line
86
+ originalLine
87
+ startLine
88
+ originalStartLine
89
+ diffSide
90
+ comments(first: 100) {
91
+ nodes {
92
+ id
93
+ databaseId
94
+ body
95
+ path
96
+ originalPosition
97
+ diffHunk
98
+ createdAt
99
+ url
100
+ author {
101
+ login
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ `;
111
+ const result = await octokit.graphql(query, {
112
+ owner,
113
+ repo,
114
+ prNumber,
115
+ });
116
+ const pullRequest = result.repository.pullRequest;
117
+ // Transform GraphQL reviews to our interface
118
+ const reviews = (pullRequest.reviews.nodes || []).map((review) => ({
119
+ id: review.databaseId,
120
+ user: {
121
+ login: review.author?.login || 'unknown',
122
+ },
123
+ body: review.body,
124
+ state: review.state,
125
+ submitted_at: review.submittedAt,
126
+ }));
127
+ // Transform GraphQL review threads to our interface
128
+ const reviewThreads = (pullRequest.reviewThreads.nodes || []).map((thread) => ({
129
+ id: thread.id,
130
+ isResolved: thread.isResolved,
131
+ isOutdated: thread.isOutdated,
132
+ line: thread.line,
133
+ originalLine: thread.originalLine,
134
+ startLine: thread.startLine,
135
+ originalStartLine: thread.originalStartLine,
136
+ diffSide: thread.diffSide,
137
+ comments: (thread.comments.nodes || []).map((comment) => ({
138
+ id: comment.databaseId,
139
+ body: comment.body,
140
+ path: comment.path,
141
+ line: thread.line, // Use thread's line for consistency
142
+ user: {
143
+ login: comment.author?.login || 'unknown',
144
+ },
145
+ created_at: comment.createdAt,
146
+ position: comment.originalPosition,
147
+ original_position: comment.originalPosition,
148
+ diff_hunk: comment.diffHunk,
149
+ url: comment.url,
150
+ })),
151
+ }));
152
+ // Filter for reviews requesting changes
153
+ const changesRequestedReviews = reviews.filter((review) => review.state === 'CHANGES_REQUESTED');
154
+ // Filter for unresolved threads
155
+ const unresolvedThreads = reviewThreads.filter((thread) => !thread.isResolved);
156
+ if (verbose) {
157
+ console.log(`✅ Found ${changesRequestedReviews.length} reviews requesting changes`);
158
+ console.log(`✅ Found ${unresolvedThreads.length} unresolved review threads`);
159
+ }
160
+ return {
161
+ reviewThreads: unresolvedThreads,
162
+ reviews: changesRequestedReviews,
163
+ };
164
+ }
57
165
  /**
58
166
  * Fetch user stories via MCP
59
167
  */
@@ -163,13 +271,17 @@ export async function fetchCodeRefineContext(mcpServerUrl, mcpToken, featureId,
163
271
  const octokit = new Octokit({
164
272
  auth: githubToken,
165
273
  });
166
- // Fetch PR reviews and comments
167
- const [reviews, reviewComments, userStories, testCases] = await Promise.all([
168
- fetchPRReviews(octokit, owner, repo, prNumber, verbose),
169
- fetchPRReviewComments(octokit, owner, repo, prNumber, verbose),
274
+ // Fetch PR data using GraphQL API and additional context
275
+ const [prData, userStories, testCases] = await Promise.all([
276
+ fetchPRDataGraphQL(octokit, owner, repo, prNumber, verbose),
170
277
  fetchUserStories(mcpServerUrl, mcpToken, featureId, verbose),
171
278
  fetchTestCases(mcpServerUrl, mcpToken, featureId, verbose),
172
279
  ]);
280
+ // Extract review comments from unresolved threads
281
+ const reviewComments = prData.reviewThreads.flatMap((thread) => thread.comments);
282
+ if (verbose) {
283
+ console.log(`📊 Summary: ${prData.reviews.length} reviews requesting changes, ${prData.reviewThreads.length} unresolved threads, ${reviewComments.length} total comments`);
284
+ }
173
285
  return {
174
286
  featureId,
175
287
  featureName: feature.name,
@@ -178,8 +290,9 @@ export async function fetchCodeRefineContext(mcpServerUrl, mcpToken, featureId,
178
290
  pullRequestNumber: prNumber,
179
291
  owner,
180
292
  repo,
181
- reviews,
293
+ reviews: prData.reviews,
182
294
  reviewComments,
295
+ reviewThreads: prData.reviewThreads,
183
296
  technicalDesign: feature.technical_design,
184
297
  userStories,
185
298
  testCases,
@@ -213,8 +326,45 @@ export function formatContextForPrompt(context) {
213
326
  sections.push('');
214
327
  });
215
328
  }
216
- // Review comments
217
- if (context.reviewComments.length > 0) {
329
+ // Review threads (unresolved conversations)
330
+ if (context.reviewThreads && context.reviewThreads.length > 0) {
331
+ sections.push(`## Unresolved Review Threads`);
332
+ sections.push(`*These conversations must be resolved before the PR can be merged.*`);
333
+ sections.push('');
334
+ context.reviewThreads.forEach((thread, idx) => {
335
+ sections.push(`### Thread ${idx + 1} ${thread.isOutdated ? '(Outdated)' : ''}`);
336
+ // Show thread location
337
+ if (thread.line !== null) {
338
+ sections.push(`**Line**: ${thread.line}`);
339
+ }
340
+ if (thread.startLine !== null && thread.startLine !== thread.line) {
341
+ sections.push(`**Line Range**: ${thread.startLine} - ${thread.line || thread.originalLine}`);
342
+ }
343
+ sections.push(`**Status**: ❌ Unresolved`);
344
+ sections.push('');
345
+ // Show all comments in this thread
346
+ thread.comments.forEach((comment, commentIdx) => {
347
+ sections.push(`#### ${commentIdx === 0 ? 'Original Comment' : `Reply ${commentIdx}`} by @${comment.user.login}`);
348
+ if (comment.path) {
349
+ sections.push(`**File**: ${comment.path}`);
350
+ }
351
+ if (comment.url) {
352
+ sections.push(`**URL**: ${comment.url}`);
353
+ }
354
+ if (commentIdx === 0 && comment.diff_hunk) {
355
+ sections.push(`**Context**:`);
356
+ sections.push('```diff');
357
+ sections.push(comment.diff_hunk);
358
+ sections.push('```');
359
+ }
360
+ sections.push(`**Comment**:`);
361
+ sections.push(comment.body);
362
+ sections.push('');
363
+ });
364
+ });
365
+ }
366
+ else if (context.reviewComments.length > 0) {
367
+ // Fallback to old format if no thread data available
218
368
  sections.push(`## Review Comments`);
219
369
  sections.push('');
220
370
  context.reviewComments.forEach((comment, idx) => {
@@ -50,10 +50,24 @@ async function fetchUnresolvedReviewThreads(octokit, owner, repo, prNumber, verb
50
50
  prNumber,
51
51
  });
52
52
  const allThreads = result?.repository?.pullRequest?.reviewThreads?.nodes || [];
53
- // Filter for unresolved threads
54
- const unresolvedThreads = allThreads.filter((thread) => !thread.isResolved);
53
+ // Filter for truly unresolved threads
54
+ // - Exclude resolved threads (isResolved = true)
55
+ // - Exclude outdated threads (isOutdated = true) - these mean code has changed, should auto-resolve
56
+ const unresolvedThreads = allThreads.filter((thread) => !thread.isResolved && !thread.isOutdated);
57
+ // Separate outdated threads that should be auto-resolved
58
+ const outdatedThreads = allThreads.filter((thread) => !thread.isResolved && thread.isOutdated);
55
59
  if (verbose) {
56
60
  logInfo(`📊 Found ${unresolvedThreads.length} unresolved review threads (out of ${allThreads.length} total)`);
61
+ if (outdatedThreads.length > 0) {
62
+ logInfo(`📊 Found ${outdatedThreads.length} outdated threads (code changed, will auto-resolve)`);
63
+ }
64
+ }
65
+ // Auto-resolve outdated threads
66
+ if (outdatedThreads.length > 0) {
67
+ const markedCount = await resolveReviewThreads(octokit, outdatedThreads, verbose);
68
+ if (verbose) {
69
+ logInfo(`✅ Auto-resolved ${markedCount} outdated threads`);
70
+ }
57
71
  }
58
72
  return unresolvedThreads;
59
73
  }
@@ -65,6 +79,7 @@ async function fetchUnresolvedReviewThreads(octokit, owner, repo, prNumber, verb
65
79
  }
66
80
  /**
67
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
68
83
  */
69
84
  function analyzeUnresolvedThread(thread) {
70
85
  const firstComment = thread.comments.nodes[0];
@@ -72,13 +87,8 @@ function analyzeUnresolvedThread(thread) {
72
87
  return 'Comment thread exists but has no comments';
73
88
  }
74
89
  const reasons = [];
75
- // Check if thread is outdated (code has changed but thread not resolved)
76
- if (thread.isOutdated) {
77
- reasons.push('Code has changed since comment was made, but thread not marked as resolved');
78
- }
79
- else {
80
- reasons.push('Code at this location has not been modified to address the feedback');
81
- }
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');
82
92
  // Check if there are multiple comments (discussion ongoing)
83
93
  if (thread.comments.totalCount > 1) {
84
94
  reasons.push(`Discussion ongoing (${thread.comments.totalCount} comments in thread)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {