codecritique 2.0.3 → 2.0.5
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/package.json +1 -1
- package/src/index.js +208 -52
- package/src/llm.js +1 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -30,7 +30,9 @@ import { ProjectAnalyzer } from './project-analyzer.js';
|
|
|
30
30
|
import { reviewFile, reviewFiles, reviewPullRequest } from './rag-review.js';
|
|
31
31
|
import { execGitSafe } from './utils/command.js';
|
|
32
32
|
import { ensureBranchExists, findBaseBranch } from './utils/git.js';
|
|
33
|
-
import { verboseLog } from './utils/logging.js';
|
|
33
|
+
import { diagnosticLog, isDebugEnabled, isVerboseEnabled, verboseLog } from './utils/logging.js';
|
|
34
|
+
import { collectPRLevelFindings, getFileLevelIssueCount, hasPRLevelFindings } from './utils/review-results.js';
|
|
35
|
+
import { configureCleanStdoutForDataOutput, isBrokenStdoutPipeError, writeStdout } from './utils/stdout.js';
|
|
34
36
|
|
|
35
37
|
// Create a default embeddings system instance
|
|
36
38
|
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
@@ -212,7 +214,7 @@ else {
|
|
|
212
214
|
|
|
213
215
|
// Register process event handlers for cleanup (embeddings cleanup primarily)
|
|
214
216
|
process.on('SIGINT', async () => {
|
|
215
|
-
|
|
217
|
+
diagnosticLog(chalk.yellow('\nReceived SIGINT. Attempting graceful shutdown...'));
|
|
216
218
|
// Set a timeout to force exit if cleanup hangs
|
|
217
219
|
const forceExitTimeout = setTimeout(() => {
|
|
218
220
|
console.error(chalk.red('Cleanup timed out after 10 seconds. Forcing exit...'));
|
|
@@ -220,23 +222,23 @@ process.on('SIGINT', async () => {
|
|
|
220
222
|
}, 10000); // 10 seconds timeout
|
|
221
223
|
|
|
222
224
|
try {
|
|
223
|
-
|
|
225
|
+
diagnosticLog(chalk.cyan('SIGINT handler: Attempting embeddingsSystem.cleanup()...'));
|
|
224
226
|
await embeddingsSystem.cleanup();
|
|
225
|
-
|
|
227
|
+
diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
|
|
226
228
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
227
|
-
|
|
229
|
+
diagnosticLog(chalk.cyan('SIGINT handler: Exiting normally (code 0).'));
|
|
228
230
|
process.exit(0); // Exit normally
|
|
229
231
|
}
|
|
230
232
|
catch (err) {
|
|
231
233
|
console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
|
|
232
234
|
clearTimeout(forceExitTimeout);
|
|
233
|
-
|
|
235
|
+
diagnosticLog(chalk.cyan('SIGINT handler: Exiting after error (code 1).'));
|
|
234
236
|
process.exit(1); // Exit with error code
|
|
235
237
|
}
|
|
236
238
|
});
|
|
237
239
|
|
|
238
240
|
process.on('SIGTERM', async () => {
|
|
239
|
-
|
|
241
|
+
diagnosticLog(chalk.yellow('\nReceived SIGTERM. Attempting graceful shutdown...'));
|
|
240
242
|
// Set a timeout to force exit if cleanup hangs
|
|
241
243
|
const forceExitTimeout = setTimeout(() => {
|
|
242
244
|
console.error(chalk.red('Cleanup timed out after 10 seconds. Forcing exit...'));
|
|
@@ -244,17 +246,17 @@ process.on('SIGTERM', async () => {
|
|
|
244
246
|
}, 10000);
|
|
245
247
|
|
|
246
248
|
try {
|
|
247
|
-
|
|
249
|
+
diagnosticLog(chalk.cyan('SIGTERM handler: Attempting embeddingsSystem.cleanup()...'));
|
|
248
250
|
await embeddingsSystem.cleanup();
|
|
249
|
-
|
|
251
|
+
diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
|
|
250
252
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
251
|
-
|
|
253
|
+
diagnosticLog(chalk.cyan('SIGTERM handler: Exiting normally (code 0).'));
|
|
252
254
|
process.exit(0); // Exit normally
|
|
253
255
|
}
|
|
254
256
|
catch (err) {
|
|
255
257
|
console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
|
|
256
258
|
clearTimeout(forceExitTimeout);
|
|
257
|
-
|
|
259
|
+
diagnosticLog(chalk.cyan('SIGTERM handler: Exiting after error (code 1).'));
|
|
258
260
|
process.exit(1); // Exit with error code
|
|
259
261
|
}
|
|
260
262
|
});
|
|
@@ -262,7 +264,7 @@ process.on('SIGTERM', async () => {
|
|
|
262
264
|
// Ensure cleanup on normal exit
|
|
263
265
|
process.on('exit', () => {
|
|
264
266
|
// Note: Async cleanup might not fully complete here
|
|
265
|
-
|
|
267
|
+
diagnosticLog(chalk.cyan('Exiting...'));
|
|
266
268
|
});
|
|
267
269
|
|
|
268
270
|
// REMOVED: Old options processing logic for ignore/severity
|
|
@@ -272,6 +274,7 @@ process.on('exit', () => {
|
|
|
272
274
|
|
|
273
275
|
// Main function to run the code review (Refactored to use cag-review.js)
|
|
274
276
|
async function runCodeReview(options) {
|
|
277
|
+
configureCleanStdoutForDataOutput(options);
|
|
275
278
|
let reviewTask = null;
|
|
276
279
|
let operationDescription = '';
|
|
277
280
|
const startTime = Date.now();
|
|
@@ -280,7 +283,7 @@ async function runCodeReview(options) {
|
|
|
280
283
|
// If --directory is specified, use that as the project directory
|
|
281
284
|
// Otherwise, use the current working directory
|
|
282
285
|
const projectPath = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
283
|
-
|
|
286
|
+
diagnosticLog(chalk.gray(`Using project path for analysis: ${projectPath}`));
|
|
284
287
|
|
|
285
288
|
// Parse custom documents
|
|
286
289
|
const customDocs = [];
|
|
@@ -321,12 +324,12 @@ async function runCodeReview(options) {
|
|
|
321
324
|
.trim(); // Remove leading/trailing whitespace
|
|
322
325
|
|
|
323
326
|
customDocs.push({ title, content });
|
|
324
|
-
|
|
327
|
+
diagnosticLog(chalk.gray(`Loaded custom document '${title}' from ${filePath} (${content.length} chars)`));
|
|
325
328
|
|
|
326
329
|
// Debug: Show a clean preview of the content
|
|
327
330
|
const previewLength = 200;
|
|
328
331
|
const preview = content.substring(0, previewLength).replace(/\n/g, '\\n');
|
|
329
|
-
|
|
332
|
+
diagnosticLog(chalk.gray(` Content preview: ${preview}${content.length > previewLength ? '...' : ''}`));
|
|
330
333
|
}
|
|
331
334
|
}
|
|
332
335
|
|
|
@@ -351,7 +354,7 @@ async function runCodeReview(options) {
|
|
|
351
354
|
};
|
|
352
355
|
|
|
353
356
|
try {
|
|
354
|
-
|
|
357
|
+
diagnosticLog(chalk.bold.blue('AI Code Review (RAG Approach) - Starting analysis...'));
|
|
355
358
|
|
|
356
359
|
// Determine the review mode based on options
|
|
357
360
|
// Only support: single file, specific files, or diff with branch
|
|
@@ -360,7 +363,9 @@ async function runCodeReview(options) {
|
|
|
360
363
|
const gitWorkingDir = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
361
364
|
const changedFiles = getChangedFiles(options.diffWith, gitWorkingDir);
|
|
362
365
|
if (changedFiles.length === 0) {
|
|
363
|
-
|
|
366
|
+
const message = `No changed files found compared to branch '${options.diffWith}'. Exiting.`;
|
|
367
|
+
diagnosticLog(chalk.yellow(message));
|
|
368
|
+
await emitEmptyReviewOutputIfNeeded(options, { success: true, results: [], message });
|
|
364
369
|
return;
|
|
365
370
|
}
|
|
366
371
|
operationDescription = `${changedFiles.length} files changed vs ${options.diffWith}`;
|
|
@@ -382,7 +387,9 @@ async function runCodeReview(options) {
|
|
|
382
387
|
else if (options.files && options.files.length > 0) {
|
|
383
388
|
const filesToAnalyze = await expandFilePatterns(options.files);
|
|
384
389
|
if (filesToAnalyze.length === 0) {
|
|
385
|
-
|
|
390
|
+
const message = 'No files found matching the specified patterns. Exiting.';
|
|
391
|
+
diagnosticLog(chalk.yellow(message));
|
|
392
|
+
await emitEmptyReviewOutputIfNeeded(options, { success: true, results: [], message });
|
|
386
393
|
return;
|
|
387
394
|
}
|
|
388
395
|
operationDescription = `${filesToAnalyze.length} specific files/patterns`;
|
|
@@ -407,7 +414,7 @@ async function runCodeReview(options) {
|
|
|
407
414
|
process.exit(1);
|
|
408
415
|
}
|
|
409
416
|
|
|
410
|
-
|
|
417
|
+
diagnosticLog(chalk.cyan(`Starting review for ${operationDescription}...`));
|
|
411
418
|
|
|
412
419
|
// Execute the selected review task
|
|
413
420
|
const reviewResult = await reviewTask;
|
|
@@ -420,60 +427,77 @@ async function runCodeReview(options) {
|
|
|
420
427
|
// Process and output results
|
|
421
428
|
if (reviewResult && reviewResult.success) {
|
|
422
429
|
if (reviewResult.results && reviewResult.results.length > 0) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
// Pass the detailed results array to the output function
|
|
427
|
-
outputFn(reviewResult.results, options);
|
|
428
|
-
console.log(chalk.bold.green(`\nAnalysis complete for ${operationDescription}! (${duration}s)`));
|
|
430
|
+
diagnosticLog(chalk.green(`Found ${reviewResult.results.length} result items to display`));
|
|
431
|
+
await outputReviewResults(reviewResult.results, options, reviewResult);
|
|
432
|
+
diagnosticLog(chalk.bold.green(`\nAnalysis complete for ${operationDescription}! (${duration}s)`));
|
|
429
433
|
}
|
|
430
434
|
else {
|
|
431
435
|
// No results to display (e.g., all files were excluded/skipped)
|
|
432
436
|
const message = reviewResult.message || 'All files were excluded from review (e.g., config files, lock files).';
|
|
433
|
-
|
|
437
|
+
diagnosticLog(chalk.yellow(message));
|
|
434
438
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
console.log(chalk.yellow(`Empty results written to: ${options.outputFile}`));
|
|
439
|
+
if (await emitEmptyReviewOutputIfNeeded(options, reviewResult)) {
|
|
440
|
+
if (options.outputFile) {
|
|
441
|
+
diagnosticLog(chalk.yellow(`Empty results written to: ${options.outputFile}`));
|
|
442
|
+
}
|
|
440
443
|
}
|
|
441
444
|
|
|
442
|
-
|
|
445
|
+
diagnosticLog(chalk.bold.yellow(`\nReview complete for ${operationDescription} - no reviewable files found (${duration}s)`));
|
|
443
446
|
}
|
|
444
447
|
}
|
|
445
448
|
else {
|
|
449
|
+
const error = reviewResult?.error || 'Code review process failed.';
|
|
446
450
|
console.error(chalk.red('\nCode review process failed.'));
|
|
447
|
-
|
|
448
|
-
|
|
451
|
+
console.error(chalk.red(`Error: ${error}`));
|
|
452
|
+
|
|
453
|
+
if (await emitErrorReviewOutputIfNeeded(options, error, reviewResult || {})) {
|
|
454
|
+
if (options.outputFile) {
|
|
455
|
+
diagnosticLog(chalk.yellow(`Error results written to: ${options.outputFile}`));
|
|
456
|
+
}
|
|
449
457
|
}
|
|
458
|
+
// AI review output is advisory; report review-task failures without failing CI.
|
|
450
459
|
}
|
|
451
460
|
|
|
452
461
|
// Clean up resources
|
|
453
|
-
|
|
462
|
+
diagnosticLog(chalk.cyan('Cleaning up resources...'));
|
|
454
463
|
try {
|
|
455
464
|
await embeddingsSystem.cleanup();
|
|
456
465
|
await cleanupClassifier();
|
|
457
|
-
|
|
466
|
+
diagnosticLog(chalk.green('All resources cleaned up successfully'));
|
|
458
467
|
}
|
|
459
468
|
catch (cleanupErr) {
|
|
460
469
|
console.error(chalk.yellow('Error during cleanup:'), cleanupErr.message);
|
|
461
|
-
process.
|
|
470
|
+
process.exitCode = 1;
|
|
471
|
+
return;
|
|
462
472
|
}
|
|
463
473
|
}
|
|
464
474
|
catch (err) {
|
|
465
|
-
|
|
466
|
-
|
|
475
|
+
if (!isBrokenStdoutPipeError(err)) {
|
|
476
|
+
console.error(chalk.red(`\nError during code review (${operationDescription}):`), err.message);
|
|
477
|
+
if (shouldPrintErrorStack(options, err)) {
|
|
478
|
+
console.error(err.stack);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
await emitErrorReviewOutputIfNeeded(options, err.message, { success: false, error: err.message });
|
|
484
|
+
}
|
|
485
|
+
catch (outputErr) {
|
|
486
|
+
if (!isBrokenStdoutPipeError(outputErr)) {
|
|
487
|
+
console.error(chalk.red('Error writing review error output:'), outputErr.message);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
467
491
|
// Clean up resources even on error
|
|
468
492
|
try {
|
|
469
493
|
await embeddingsSystem.cleanup();
|
|
470
494
|
await cleanupClassifier();
|
|
471
|
-
|
|
495
|
+
diagnosticLog(chalk.green('All resources cleaned up successfully'));
|
|
472
496
|
}
|
|
473
497
|
catch (cleanupErr) {
|
|
474
498
|
console.error(chalk.red('Error during cleanup:'), cleanupErr.message);
|
|
475
499
|
}
|
|
476
|
-
process.
|
|
500
|
+
process.exitCode = 1;
|
|
477
501
|
}
|
|
478
502
|
}
|
|
479
503
|
|
|
@@ -960,7 +984,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
960
984
|
try {
|
|
961
985
|
// Get git root directory
|
|
962
986
|
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd: workingDir }).toString().trim();
|
|
963
|
-
|
|
987
|
+
diagnosticLog(chalk.gray(`Git repository: ${gitRoot}`));
|
|
964
988
|
|
|
965
989
|
// Ensure the branch exists locally (fetch if needed)
|
|
966
990
|
ensureBranchExists(branch, workingDir);
|
|
@@ -977,7 +1001,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
977
1001
|
// Continue with the original baseBranch name, it might work with remote refs
|
|
978
1002
|
}
|
|
979
1003
|
|
|
980
|
-
|
|
1004
|
+
diagnosticLog(chalk.gray(`Comparing ${branch} against ${baseBranch}...`));
|
|
981
1005
|
|
|
982
1006
|
// Use three-dot notation to get changes in branch compared to base
|
|
983
1007
|
// This shows commits that are in 'branch' but not in 'baseBranch'
|
|
@@ -991,7 +1015,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
991
1015
|
.map((file) => path.resolve(gitRoot, file)); // Get absolute path
|
|
992
1016
|
|
|
993
1017
|
if (changedFiles.length > 0) {
|
|
994
|
-
|
|
1018
|
+
diagnosticLog(chalk.gray(`Found ${changedFiles.length} changed files in ${branch} vs ${baseBranch}`));
|
|
995
1019
|
}
|
|
996
1020
|
|
|
997
1021
|
return changedFiles;
|
|
@@ -1008,19 +1032,77 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
1008
1032
|
// --- Output Formatting Functions --- //
|
|
1009
1033
|
// These consume the normalized results returned by the RAG review pipeline
|
|
1010
1034
|
|
|
1035
|
+
function getReviewOutputFunction(outputFormat) {
|
|
1036
|
+
if (outputFormat === 'json') {
|
|
1037
|
+
return outputJson;
|
|
1038
|
+
}
|
|
1039
|
+
if (outputFormat === 'markdown') {
|
|
1040
|
+
return outputMarkdown;
|
|
1041
|
+
}
|
|
1042
|
+
return outputText;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function shouldEmitReviewOutput(options = {}) {
|
|
1046
|
+
return Boolean(options.outputFile || options.output === 'json' || options.output === 'markdown');
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function shouldPrintErrorStack(options, error) {
|
|
1050
|
+
return Boolean(error?.stack && (isVerboseEnabled(options) || isDebugEnabled()));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function outputReviewResults(reviewResults, options, aggregateResult = {}) {
|
|
1054
|
+
await getReviewOutputFunction(options.output)(reviewResults, options, aggregateResult);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async function emitEmptyReviewOutputIfNeeded(options, aggregateResult = {}) {
|
|
1058
|
+
if (!shouldEmitReviewOutput(options)) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
await outputReviewResults([], options, aggregateResult);
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function emitErrorReviewOutputIfNeeded(options, error, aggregateResult = {}) {
|
|
1067
|
+
if (!shouldEmitReviewOutput(options)) {
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
await outputReviewResults([createReviewErrorResult(error)], options, {
|
|
1072
|
+
...aggregateResult,
|
|
1073
|
+
success: false,
|
|
1074
|
+
error,
|
|
1075
|
+
});
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function createReviewErrorResult(error) {
|
|
1080
|
+
return {
|
|
1081
|
+
filePath: 'review',
|
|
1082
|
+
success: false,
|
|
1083
|
+
error,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1011
1087
|
/**
|
|
1012
1088
|
* Output results in JSON format
|
|
1013
1089
|
*
|
|
1014
1090
|
* @param {Array<Object>} reviewResults - Array of individual file review results
|
|
1015
1091
|
* @param {Object} options - Command line options
|
|
1092
|
+
* @param {Object} aggregateResult - Full review result with PR-level context
|
|
1016
1093
|
*/
|
|
1017
|
-
function outputJson(reviewResults, options) {
|
|
1094
|
+
async function outputJson(reviewResults, options, aggregateResult = {}) {
|
|
1095
|
+
const prLevelFindings = collectPRLevelFindings(reviewResults, aggregateResult);
|
|
1096
|
+
const fileLevelIssues = getFileLevelIssueCount(reviewResults);
|
|
1097
|
+
|
|
1018
1098
|
// Structure the output to be informative
|
|
1019
1099
|
const output = {
|
|
1020
1100
|
summary: {
|
|
1021
1101
|
totalFilesReviewed: reviewResults.length,
|
|
1022
1102
|
filesWithIssues: reviewResults.filter((r) => r.success && !r.skipped && r.results?.issues?.length > 0).length,
|
|
1023
|
-
totalIssues:
|
|
1103
|
+
totalIssues: fileLevelIssues + prLevelFindings.issues.length,
|
|
1104
|
+
fileLevelIssues,
|
|
1105
|
+
prLevelIssues: prLevelFindings.issues.length,
|
|
1024
1106
|
issuesWithCodeSuggestions: reviewResults.reduce((sum, r) => {
|
|
1025
1107
|
if (!r.success || r.skipped || !r.results?.issues) {
|
|
1026
1108
|
return sum;
|
|
@@ -1049,6 +1131,10 @@ function outputJson(reviewResults, options) {
|
|
|
1049
1131
|
}),
|
|
1050
1132
|
};
|
|
1051
1133
|
|
|
1134
|
+
if (hasPRLevelFindings(prLevelFindings)) {
|
|
1135
|
+
output.prLevelFindings = prLevelFindings;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1052
1138
|
const jsonOutput = JSON.stringify(output, null, 2);
|
|
1053
1139
|
|
|
1054
1140
|
// If output-file is specified, write to file instead of stdout
|
|
@@ -1058,7 +1144,7 @@ function outputJson(reviewResults, options) {
|
|
|
1058
1144
|
}
|
|
1059
1145
|
else {
|
|
1060
1146
|
// Write JSON output to stdout (process.stdout is not buffered)
|
|
1061
|
-
|
|
1147
|
+
await writeStdout(jsonOutput);
|
|
1062
1148
|
}
|
|
1063
1149
|
}
|
|
1064
1150
|
|
|
@@ -1067,13 +1153,15 @@ function outputJson(reviewResults, options) {
|
|
|
1067
1153
|
*
|
|
1068
1154
|
* @param {Array<Object>} reviewResults - Array of individual file review results
|
|
1069
1155
|
* @param {Object} options - Command line options
|
|
1156
|
+
* @param {Object} aggregateResult - Full review result with PR-level context
|
|
1070
1157
|
*/
|
|
1071
|
-
function outputMarkdown(reviewResults, options) {
|
|
1158
|
+
async function outputMarkdown(reviewResults, options, aggregateResult = {}) {
|
|
1072
1159
|
const totalFiles = reviewResults.length;
|
|
1073
1160
|
const filesWithIssues = reviewResults.filter((r) => r.success && !r.skipped && r.results?.issues?.length > 0).length;
|
|
1074
|
-
const totalIssues = reviewResults.reduce((sum, r) => sum + (r.results?.issues?.length || 0), 0);
|
|
1075
1161
|
const skippedFiles = reviewResults.filter((r) => r.skipped).length;
|
|
1076
1162
|
const errorFiles = reviewResults.filter((r) => !r.success).length;
|
|
1163
|
+
const prLevelFindings = collectPRLevelFindings(reviewResults, aggregateResult);
|
|
1164
|
+
const totalIssues = getFileLevelIssueCount(reviewResults) + prLevelFindings.issues.length;
|
|
1077
1165
|
|
|
1078
1166
|
const lines = [
|
|
1079
1167
|
'# AI Code Review Results (RAG Approach)',
|
|
@@ -1092,6 +1180,8 @@ function outputMarkdown(reviewResults, options) {
|
|
|
1092
1180
|
lines.push(`- **Errors:** ${errorFiles}`);
|
|
1093
1181
|
}
|
|
1094
1182
|
|
|
1183
|
+
appendPRLevelFindingsMarkdown(lines, prLevelFindings);
|
|
1184
|
+
|
|
1095
1185
|
lines.push('', '## Detailed Review per File', '');
|
|
1096
1186
|
|
|
1097
1187
|
reviewResults.forEach((fileResult) => {
|
|
@@ -1149,7 +1239,7 @@ function outputMarkdown(reviewResults, options) {
|
|
|
1149
1239
|
return;
|
|
1150
1240
|
}
|
|
1151
1241
|
|
|
1152
|
-
|
|
1242
|
+
await writeStdout(markdownOutput);
|
|
1153
1243
|
}
|
|
1154
1244
|
|
|
1155
1245
|
/**
|
|
@@ -1157,13 +1247,15 @@ function outputMarkdown(reviewResults, options) {
|
|
|
1157
1247
|
*
|
|
1158
1248
|
* @param {Array<Object>} reviewResults - Array of individual file review results
|
|
1159
1249
|
* @param {Object} cliOptions - Command line options
|
|
1250
|
+
* @param {Object} aggregateResult - Full review result with PR-level context
|
|
1160
1251
|
*/
|
|
1161
|
-
function outputText(reviewResults, cliOptions) {
|
|
1252
|
+
function outputText(reviewResults, cliOptions, aggregateResult = {}) {
|
|
1162
1253
|
const totalFiles = reviewResults.length;
|
|
1163
1254
|
const filesWithIssues = reviewResults.filter((r) => r.success && !r.skipped && r.results?.issues?.length > 0).length;
|
|
1164
|
-
const totalIssues = reviewResults.reduce((sum, r) => sum + (r.results?.issues?.length || 0), 0);
|
|
1165
1255
|
const skippedFiles = reviewResults.filter((r) => r.skipped).length;
|
|
1166
1256
|
const errorFiles = reviewResults.filter((r) => !r.success).length;
|
|
1257
|
+
const prLevelFindings = collectPRLevelFindings(reviewResults, aggregateResult);
|
|
1258
|
+
const totalIssues = getFileLevelIssueCount(reviewResults) + prLevelFindings.issues.length;
|
|
1167
1259
|
|
|
1168
1260
|
console.log(chalk.bold.blue('\n===== AI Code Review Summary ====='));
|
|
1169
1261
|
console.log(`Files Analyzed: ${chalk.bold(totalFiles)}`);
|
|
@@ -1177,6 +1269,8 @@ function outputText(reviewResults, cliOptions) {
|
|
|
1177
1269
|
}
|
|
1178
1270
|
console.log(chalk.bold.blue('================================================'));
|
|
1179
1271
|
|
|
1272
|
+
outputPRLevelFindingsText(prLevelFindings);
|
|
1273
|
+
|
|
1180
1274
|
reviewResults.forEach((fileResult) => {
|
|
1181
1275
|
if (!fileResult.success) {
|
|
1182
1276
|
console.log(chalk.bold.red(`\n===== Error reviewing ${fileResult.filePath} =====`));
|
|
@@ -1230,6 +1324,68 @@ function outputText(reviewResults, cliOptions) {
|
|
|
1230
1324
|
});
|
|
1231
1325
|
}
|
|
1232
1326
|
|
|
1327
|
+
function appendPRLevelFindingsMarkdown(lines, prLevelFindings) {
|
|
1328
|
+
if (!hasPRLevelFindings(prLevelFindings)) {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
lines.push('', '## PR-Level Findings', '');
|
|
1333
|
+
|
|
1334
|
+
if (prLevelFindings.summary) {
|
|
1335
|
+
lines.push(`**Summary:** ${prLevelFindings.summary}`, '');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
for (const issue of prLevelFindings.issues) {
|
|
1339
|
+
const severity = issue.severity || 'info';
|
|
1340
|
+
const severityEmoji = getSeverityEmoji(severity);
|
|
1341
|
+
lines.push(`- **[${severity.toUpperCase()}] ${severityEmoji}** ${issue.description}`);
|
|
1342
|
+
if (issue.files?.length > 0) {
|
|
1343
|
+
lines.push(` - Affected files: ${issue.files.join(', ')}`);
|
|
1344
|
+
}
|
|
1345
|
+
if (issue.suggestion) {
|
|
1346
|
+
lines.push(` - Suggestion: ${issue.suggestion}`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
if (prLevelFindings.recommendations.length > 0) {
|
|
1351
|
+
lines.push('', '**Recommendations:**', '');
|
|
1352
|
+
for (const recommendation of prLevelFindings.recommendations) {
|
|
1353
|
+
lines.push(`- ${recommendation}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function outputPRLevelFindingsText(prLevelFindings) {
|
|
1359
|
+
if (!hasPRLevelFindings(prLevelFindings)) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
console.log(chalk.bold.underline('\n===== PR-Level Findings ====='));
|
|
1364
|
+
if (prLevelFindings.summary) {
|
|
1365
|
+
console.log(chalk.bold.cyan(`Summary: ${prLevelFindings.summary}`));
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
for (const issue of prLevelFindings.issues) {
|
|
1369
|
+
const severity = issue.severity || 'info';
|
|
1370
|
+
const severityColor = getSeverityColor(severity);
|
|
1371
|
+
console.log(` ${severityColor(`[${severity.toUpperCase()}]`)} ${issue.description}`);
|
|
1372
|
+
if (issue.files?.length > 0) {
|
|
1373
|
+
console.log(` Affected files: ${issue.files.join(', ')}`);
|
|
1374
|
+
}
|
|
1375
|
+
if (issue.suggestion) {
|
|
1376
|
+
console.log(` ${chalk.green(`Suggestion: ${issue.suggestion}`)}`);
|
|
1377
|
+
}
|
|
1378
|
+
console.log('');
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (prLevelFindings.recommendations.length > 0) {
|
|
1382
|
+
console.log(chalk.bold.yellow('Recommendations:'));
|
|
1383
|
+
for (const recommendation of prLevelFindings.recommendations) {
|
|
1384
|
+
console.log(` - ${recommendation}`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1233
1389
|
// --- Severity Helpers (Remain Unchanged) --- //
|
|
1234
1390
|
|
|
1235
1391
|
/**
|
package/src/llm.js
CHANGED