edsger 0.6.10 → 0.6.12
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.
- package/dist/phases/code-review/index.js +165 -6
- package/package.json +1 -1
|
@@ -17,6 +17,100 @@ async function* prompt(reviewPrompt) {
|
|
|
17
17
|
yield userMessage(reviewPrompt);
|
|
18
18
|
await new Promise((res) => setTimeout(res, 10000));
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse unified diff patch to build a mapping from file line numbers to diff positions
|
|
22
|
+
*
|
|
23
|
+
* GitHub API uses "position" parameter which is the line number in the unified diff:
|
|
24
|
+
* - Position starts at 1 for the first line AFTER the @@ hunk header
|
|
25
|
+
* - Increments for every line (context, additions, deletions)
|
|
26
|
+
* - Continues through all hunks in the file
|
|
27
|
+
*
|
|
28
|
+
* Example unified diff:
|
|
29
|
+
* ```
|
|
30
|
+
* @@ -1,3 +1,4 @@ <- not counted in position
|
|
31
|
+
* context line <- position 1, file line 1
|
|
32
|
+
* -old line <- position 2, (deleted line, no file line number)
|
|
33
|
+
* +new line <- position 3, file line 2
|
|
34
|
+
* +another new <- position 4, file line 3
|
|
35
|
+
* context <- position 5, file line 4
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Returns: Map<file line number, diff position>
|
|
39
|
+
*/
|
|
40
|
+
function buildLineToPositionMap(patch) {
|
|
41
|
+
const lineToPosition = new Map();
|
|
42
|
+
const lines = patch.split('\n');
|
|
43
|
+
let position = 0; // Position in the diff (starts at 1 after first line after @@)
|
|
44
|
+
let currentNewLine = 0; // Current line number in the new file (RIGHT side)
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
// Parse hunk headers like @@ -1,7 +1,7 @@
|
|
47
|
+
// This extracts the starting line number for the new file (+side)
|
|
48
|
+
const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
|
|
49
|
+
if (hunkMatch) {
|
|
50
|
+
currentNewLine = parseInt(hunkMatch[1], 10);
|
|
51
|
+
// Don't increment position for the @@ line itself
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Every line after @@ increments the position counter
|
|
55
|
+
position++;
|
|
56
|
+
// Map line numbers based on line type
|
|
57
|
+
if (line.startsWith('+')) {
|
|
58
|
+
// Addition: this line exists in the new file at currentNewLine
|
|
59
|
+
lineToPosition.set(currentNewLine, position);
|
|
60
|
+
currentNewLine++;
|
|
61
|
+
}
|
|
62
|
+
else if (line.startsWith('-')) {
|
|
63
|
+
// Deletion: this line existed in old file, not in new file
|
|
64
|
+
// Position still increments, but currentNewLine does not
|
|
65
|
+
// We can't comment on this line using file line numbers
|
|
66
|
+
}
|
|
67
|
+
else if (!line.startsWith('\\')) {
|
|
68
|
+
// Context line (no prefix): exists in both old and new file
|
|
69
|
+
lineToPosition.set(currentNewLine, position);
|
|
70
|
+
currentNewLine++;
|
|
71
|
+
}
|
|
72
|
+
// Lines starting with '\' (like "") are metadata
|
|
73
|
+
}
|
|
74
|
+
return lineToPosition;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Find the diff position for a target line number, or the closest nearby position
|
|
78
|
+
*
|
|
79
|
+
* @param targetLine - The file line number we want to comment on
|
|
80
|
+
* @param lineToPosition - Map of file line numbers to diff positions
|
|
81
|
+
* @returns Object with position and actual line number, or null if not found
|
|
82
|
+
*/
|
|
83
|
+
function findClosestPosition(targetLine, lineToPosition) {
|
|
84
|
+
// Check if exact line exists in the diff
|
|
85
|
+
if (lineToPosition.has(targetLine)) {
|
|
86
|
+
return {
|
|
87
|
+
position: lineToPosition.get(targetLine),
|
|
88
|
+
actualLine: targetLine,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Try to find a nearby line within ±10 lines range
|
|
92
|
+
const range = 10;
|
|
93
|
+
for (let offset = 1; offset <= range; offset++) {
|
|
94
|
+
// Try below first (more likely to be relevant for review comments)
|
|
95
|
+
const lineBelow = targetLine + offset;
|
|
96
|
+
if (lineToPosition.has(lineBelow)) {
|
|
97
|
+
return {
|
|
98
|
+
position: lineToPosition.get(lineBelow),
|
|
99
|
+
actualLine: lineBelow,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Try above
|
|
103
|
+
const lineAbove = targetLine - offset;
|
|
104
|
+
if (lineToPosition.has(lineAbove)) {
|
|
105
|
+
return {
|
|
106
|
+
position: lineToPosition.get(lineAbove),
|
|
107
|
+
actualLine: lineAbove,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// No valid position found within range
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
20
114
|
/**
|
|
21
115
|
* Main code review function
|
|
22
116
|
*/
|
|
@@ -179,16 +273,81 @@ export const reviewPullRequest = async (options, config) => {
|
|
|
179
273
|
if (verbose) {
|
|
180
274
|
logInfo(`Creating GitHub review with ${comments.length} comments...`);
|
|
181
275
|
}
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
276
|
+
// Build a map of file paths to their line-to-position mappings
|
|
277
|
+
const fileLineToPosition = new Map();
|
|
278
|
+
for (const file of context.files) {
|
|
279
|
+
if (file.patch) {
|
|
280
|
+
fileLineToPosition.set(file.filename, buildLineToPositionMap(file.patch));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Get the latest commit SHA from the PR for the review
|
|
284
|
+
const commitId = context.prData.head.sha;
|
|
285
|
+
// Create review with inline comments using unified diff positions
|
|
286
|
+
const reviewComments = [];
|
|
287
|
+
for (const comment of comments) {
|
|
288
|
+
const filePath = comment.file || comment.path;
|
|
289
|
+
const requestedLine = comment.line;
|
|
290
|
+
const lineToPosition = fileLineToPosition.get(filePath);
|
|
291
|
+
if (!lineToPosition) {
|
|
292
|
+
if (verbose) {
|
|
293
|
+
logInfo(`⚠️ Skipping comment for ${filePath}:${requestedLine} - file has no diff`);
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// Find the diff position for this line number
|
|
298
|
+
const positionResult = findClosestPosition(requestedLine, lineToPosition);
|
|
299
|
+
if (positionResult === null) {
|
|
300
|
+
if (verbose) {
|
|
301
|
+
logInfo(`⚠️ Skipping comment for ${filePath}:${requestedLine} - line not in diff range`);
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
let commentBody = comment.comment || comment.body;
|
|
306
|
+
// If we had to adjust the line number, add a note
|
|
307
|
+
if (positionResult.actualLine !== requestedLine) {
|
|
308
|
+
commentBody = `**Note**: Comment originally for line ${requestedLine}, adjusted to line ${positionResult.actualLine} (nearest line in diff).\n\n${commentBody}`;
|
|
309
|
+
if (verbose) {
|
|
310
|
+
logInfo(` Adjusted ${filePath}:${requestedLine} → line ${positionResult.actualLine} (position ${positionResult.position})`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
reviewComments.push({
|
|
314
|
+
path: filePath,
|
|
315
|
+
position: positionResult.position,
|
|
316
|
+
body: commentBody,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// If all comments were filtered out, just create a review with the summary
|
|
320
|
+
if (reviewComments.length === 0) {
|
|
321
|
+
if (verbose) {
|
|
322
|
+
logInfo(`⚠️ All ${comments.length} comments were filtered out (invalid line numbers)`);
|
|
323
|
+
logInfo('Creating review with summary only...');
|
|
324
|
+
}
|
|
325
|
+
const review = await octokit.pulls.createReview({
|
|
326
|
+
owner: context.owner,
|
|
327
|
+
repo: context.repo,
|
|
328
|
+
pull_number: context.pullRequestNumber,
|
|
329
|
+
event: 'COMMENT',
|
|
330
|
+
body: (summary || overall_assessment || 'Code review completed.') +
|
|
331
|
+
'\n\n**Note**: Some review comments could not be posted because they referenced lines not present in the diff.',
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
featureId,
|
|
335
|
+
status: 'success',
|
|
336
|
+
message: 'Code review completed - comments filtered due to invalid line numbers',
|
|
337
|
+
reviewId: review.data.id,
|
|
338
|
+
reviewUrl: review.data.html_url,
|
|
339
|
+
commentsCount: 0,
|
|
340
|
+
summary: summary || 'Code review completed (comments filtered)',
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (verbose) {
|
|
344
|
+
logInfo(`📝 Posting ${reviewComments.length} validated comments (${comments.length - reviewComments.length} filtered out)`);
|
|
345
|
+
}
|
|
188
346
|
const review = await octokit.pulls.createReview({
|
|
189
347
|
owner: context.owner,
|
|
190
348
|
repo: context.repo,
|
|
191
349
|
pull_number: context.pullRequestNumber,
|
|
350
|
+
commit_id: commitId, // Required for position-based comments
|
|
192
351
|
event: 'COMMENT',
|
|
193
352
|
body: summary ||
|
|
194
353
|
overall_assessment ||
|