codereview-aia 0.1.0

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.
Files changed (153) hide show
  1. package/.cr-aia.yml +23 -0
  2. package/.crignore +0 -0
  3. package/dist/index.js +27 -0
  4. package/package.json +85 -0
  5. package/src/analysis/FindingsExtractor.ts +431 -0
  6. package/src/analysis/ai-detection/analyzers/BaseAnalyzer.ts +267 -0
  7. package/src/analysis/ai-detection/analyzers/DocumentationAnalyzer.ts +622 -0
  8. package/src/analysis/ai-detection/analyzers/GitHistoryAnalyzer.ts +430 -0
  9. package/src/analysis/ai-detection/core/AIDetectionEngine.ts +467 -0
  10. package/src/analysis/ai-detection/types/DetectionTypes.ts +406 -0
  11. package/src/analysis/ai-detection/utils/SubmissionConverter.ts +390 -0
  12. package/src/analysis/context/ReviewContext.ts +378 -0
  13. package/src/analysis/context/index.ts +7 -0
  14. package/src/analysis/index.ts +8 -0
  15. package/src/analysis/tokens/TokenAnalysisFormatter.ts +154 -0
  16. package/src/analysis/tokens/TokenAnalyzer.ts +747 -0
  17. package/src/analysis/tokens/index.ts +8 -0
  18. package/src/clients/base/abstractClient.ts +190 -0
  19. package/src/clients/base/httpClient.ts +160 -0
  20. package/src/clients/base/index.ts +12 -0
  21. package/src/clients/base/modelDetection.ts +107 -0
  22. package/src/clients/base/responseProcessor.ts +586 -0
  23. package/src/clients/factory/clientFactory.ts +55 -0
  24. package/src/clients/factory/index.ts +8 -0
  25. package/src/clients/implementations/index.ts +8 -0
  26. package/src/clients/implementations/openRouterClient.ts +411 -0
  27. package/src/clients/openRouterClient.ts +863 -0
  28. package/src/clients/openRouterClientWrapper.ts +44 -0
  29. package/src/clients/utils/directoryStructure.ts +52 -0
  30. package/src/clients/utils/index.ts +11 -0
  31. package/src/clients/utils/languageDetection.ts +44 -0
  32. package/src/clients/utils/promptFormatter.ts +105 -0
  33. package/src/clients/utils/promptLoader.ts +53 -0
  34. package/src/clients/utils/tokenCounter.ts +297 -0
  35. package/src/core/ApiClientSelector.ts +37 -0
  36. package/src/core/ConfigurationService.ts +591 -0
  37. package/src/core/ConsolidationService.ts +423 -0
  38. package/src/core/InteractiveDisplayManager.ts +81 -0
  39. package/src/core/OutputManager.ts +275 -0
  40. package/src/core/ReviewGenerator.ts +140 -0
  41. package/src/core/fileDiscovery.ts +237 -0
  42. package/src/core/handlers/EstimationHandler.ts +104 -0
  43. package/src/core/handlers/FileProcessingHandler.ts +204 -0
  44. package/src/core/handlers/OutputHandler.ts +125 -0
  45. package/src/core/handlers/ReviewExecutor.ts +104 -0
  46. package/src/core/reviewOrchestrator.ts +333 -0
  47. package/src/core/utils/ModelInfoUtils.ts +56 -0
  48. package/src/formatters/outputFormatter.ts +62 -0
  49. package/src/formatters/utils/IssueFormatters.ts +83 -0
  50. package/src/formatters/utils/JsonFormatter.ts +77 -0
  51. package/src/formatters/utils/MarkdownFormatters.ts +609 -0
  52. package/src/formatters/utils/MetadataFormatter.ts +269 -0
  53. package/src/formatters/utils/ModelInfoExtractor.ts +115 -0
  54. package/src/index.ts +27 -0
  55. package/src/plugins/PluginInterface.ts +50 -0
  56. package/src/plugins/PluginManager.ts +126 -0
  57. package/src/prompts/PromptManager.ts +69 -0
  58. package/src/prompts/cache/PromptCache.ts +50 -0
  59. package/src/prompts/promptText/common/variables/css-frameworks.json +33 -0
  60. package/src/prompts/promptText/common/variables/framework-versions.json +45 -0
  61. package/src/prompts/promptText/frameworks/react/comprehensive.hbs +19 -0
  62. package/src/prompts/promptText/languages/css/comprehensive.hbs +18 -0
  63. package/src/prompts/promptText/languages/generic/comprehensive.hbs +20 -0
  64. package/src/prompts/promptText/languages/html/comprehensive.hbs +18 -0
  65. package/src/prompts/promptText/languages/javascript/comprehensive.hbs +18 -0
  66. package/src/prompts/promptText/languages/python/comprehensive.hbs +18 -0
  67. package/src/prompts/promptText/languages/typescript/comprehensive.hbs +18 -0
  68. package/src/runtime/auth/service.ts +58 -0
  69. package/src/runtime/auth/session.ts +103 -0
  70. package/src/runtime/auth/types.ts +11 -0
  71. package/src/runtime/cliEntry.ts +196 -0
  72. package/src/runtime/errors.ts +13 -0
  73. package/src/runtime/fileCollector.ts +188 -0
  74. package/src/runtime/manifest.ts +64 -0
  75. package/src/runtime/openrouterProxy.ts +45 -0
  76. package/src/runtime/proxyConfig.ts +94 -0
  77. package/src/runtime/proxyEnvironment.ts +71 -0
  78. package/src/runtime/reportMerge.ts +102 -0
  79. package/src/runtime/reporting/markdownReportBuilder.ts +138 -0
  80. package/src/runtime/reporting/reportDataCollector.ts +234 -0
  81. package/src/runtime/reporting/summaryGenerator.ts +86 -0
  82. package/src/runtime/reviewPipeline.ts +155 -0
  83. package/src/runtime/runAiCodeReview.ts +153 -0
  84. package/src/runtime/runtimeConfig.ts +5 -0
  85. package/src/runtime/ui/Layout.tsx +57 -0
  86. package/src/runtime/ui/RuntimeApp.tsx +150 -0
  87. package/src/runtime/ui/inkModules.ts +73 -0
  88. package/src/runtime/ui/screens/AuthScreen.tsx +128 -0
  89. package/src/runtime/ui/screens/ModeSelection.tsx +52 -0
  90. package/src/runtime/ui/screens/ProgressScreen.tsx +55 -0
  91. package/src/runtime/ui/screens/ResultsScreen.tsx +76 -0
  92. package/src/strategies/ArchitecturalReviewStrategy.ts +54 -0
  93. package/src/strategies/CodingTestReviewStrategy.ts +920 -0
  94. package/src/strategies/ConsolidatedReviewStrategy.ts +59 -0
  95. package/src/strategies/ExtractPatternsReviewStrategy.ts +64 -0
  96. package/src/strategies/MultiPassReviewStrategy.ts +785 -0
  97. package/src/strategies/ReviewStrategy.ts +64 -0
  98. package/src/strategies/StrategyFactory.ts +79 -0
  99. package/src/strategies/index.ts +14 -0
  100. package/src/tokenizers/baseTokenizer.ts +61 -0
  101. package/src/tokenizers/gptTokenizer.ts +27 -0
  102. package/src/tokenizers/index.ts +8 -0
  103. package/src/types/apiResponses.ts +40 -0
  104. package/src/types/cli.ts +24 -0
  105. package/src/types/common.ts +39 -0
  106. package/src/types/configuration.ts +201 -0
  107. package/src/types/handlebars.d.ts +5 -0
  108. package/src/types/patch.d.ts +25 -0
  109. package/src/types/review.ts +294 -0
  110. package/src/types/reviewContext.d.ts +65 -0
  111. package/src/types/reviewSchema.ts +181 -0
  112. package/src/types/structuredReview.ts +167 -0
  113. package/src/types/tokenAnalysis.ts +56 -0
  114. package/src/utils/FileReader.ts +93 -0
  115. package/src/utils/FileWriter.ts +76 -0
  116. package/src/utils/PathGenerator.ts +97 -0
  117. package/src/utils/api/apiUtils.ts +14 -0
  118. package/src/utils/api/index.ts +1 -0
  119. package/src/utils/apiErrorHandler.ts +287 -0
  120. package/src/utils/ciDataCollector.ts +252 -0
  121. package/src/utils/codingTestConfigLoader.ts +466 -0
  122. package/src/utils/dependencies/aiDependencyAnalyzer.ts +454 -0
  123. package/src/utils/detection/frameworkDetector.ts +879 -0
  124. package/src/utils/detection/index.ts +10 -0
  125. package/src/utils/detection/projectTypeDetector.ts +518 -0
  126. package/src/utils/diagramGenerator.ts +206 -0
  127. package/src/utils/errorLogger.ts +60 -0
  128. package/src/utils/estimationUtils.ts +407 -0
  129. package/src/utils/fileFilters.ts +373 -0
  130. package/src/utils/fileSystem.ts +57 -0
  131. package/src/utils/index.ts +36 -0
  132. package/src/utils/logger.ts +240 -0
  133. package/src/utils/pathValidator.ts +98 -0
  134. package/src/utils/priorityFilter.ts +59 -0
  135. package/src/utils/projectDocs.ts +189 -0
  136. package/src/utils/promptPaths.ts +29 -0
  137. package/src/utils/promptTemplateManager.ts +157 -0
  138. package/src/utils/review/consolidateReview.ts +553 -0
  139. package/src/utils/review/fixDisplay.ts +100 -0
  140. package/src/utils/review/fixImplementation.ts +61 -0
  141. package/src/utils/review/index.ts +36 -0
  142. package/src/utils/review/interactiveProcessing.ts +294 -0
  143. package/src/utils/review/progressTracker.ts +296 -0
  144. package/src/utils/review/reviewExtraction.ts +382 -0
  145. package/src/utils/review/types.ts +46 -0
  146. package/src/utils/reviewActionHandler.ts +18 -0
  147. package/src/utils/reviewParser.ts +253 -0
  148. package/src/utils/sanitizer.ts +238 -0
  149. package/src/utils/smartFileSelector.ts +255 -0
  150. package/src/utils/templateLoader.ts +514 -0
  151. package/src/utils/treeGenerator.ts +153 -0
  152. package/tsconfig.build.json +14 -0
  153. package/tsconfig.json +59 -0
@@ -0,0 +1,785 @@
1
+ /**
2
+ * @fileoverview Multi-pass review strategy implementation.
3
+ *
4
+ * This strategy analyzes large codebases by splitting files into multiple chunks
5
+ * and processing them sequentially, maintaining context between passes to ensure
6
+ * a cohesive review. It's automatically used when token counts exceed model limits.
7
+ */
8
+
9
+ import { ReviewContext } from '../analysis/context';
10
+ import { FindingsExtractor } from '../analysis/FindingsExtractor';
11
+ import { formatTokenAnalysis, type TokenAnalysisResult, TokenAnalyzer } from '../analysis/tokens';
12
+ import type { PassCostInfo } from '../clients/utils/tokenCounter';
13
+ import type { ApiClientConfig } from '../core/ApiClientSelector';
14
+ import { ConsolidationService } from '../core/ConsolidationService';
15
+ import { generateReview } from '../core/ReviewGenerator';
16
+ import type { FileInfo, ReviewOptions, ReviewResult, ReviewType } from '../types/review';
17
+ import logger from '../utils/logger';
18
+ import type { ProjectDocs } from '../utils/projectDocs';
19
+ import { MultiPassProgressTracker } from '../utils/review';
20
+ import { BaseReviewStrategy } from './ReviewStrategy';
21
+
22
+ // Helper function to accommodate the type mismatch with existing formatters
23
+ // const ensureString = (value: string | undefined): string => {
24
+ // return value || 'unknown';
25
+ // };
26
+
27
+ /**
28
+ * Strategy for performing multi-pass reviews of large codebases
29
+ */
30
+ export class MultiPassReviewStrategy extends BaseReviewStrategy {
31
+ private readonly consolidationService: ConsolidationService;
32
+ private readonly findingsExtractor: FindingsExtractor;
33
+
34
+ /**
35
+ * Create a new multi-pass review strategy
36
+ * @param reviewType Type of review to perform
37
+ */
38
+ constructor(reviewType: ReviewType) {
39
+ super(reviewType);
40
+ this.consolidationService = new ConsolidationService();
41
+ this.findingsExtractor = new FindingsExtractor();
42
+ logger.debug('Initialized MultiPassReviewStrategy with services');
43
+ }
44
+
45
+ /**
46
+ * Execute the multi-pass review strategy
47
+ * @param files Files to review
48
+ * @param projectName Project name
49
+ * @param projectDocs Project documentation
50
+ * @param options Review options
51
+ * @param apiClientConfig API client configuration
52
+ * @returns Promise resolving to the review result
53
+ */
54
+ async execute(
55
+ files: FileInfo[],
56
+ projectName: string,
57
+ projectDocs: ProjectDocs | null,
58
+ options: ReviewOptions,
59
+ apiClientConfig: ApiClientConfig,
60
+ ): Promise<ReviewResult> {
61
+ logger.info(`Executing multi-pass ${this.reviewType} review strategy...`);
62
+
63
+ // Make sure API client is initialized
64
+ if (!apiClientConfig.initialized) {
65
+ throw new Error('API client not initialized');
66
+ }
67
+
68
+ // Create a progress tracker
69
+ const progressTracker = new MultiPassProgressTracker(1, files.length, {
70
+ quiet: options.quiet,
71
+ });
72
+
73
+ // Start with analysis phase
74
+ progressTracker.setPhase('analyzing');
75
+
76
+ // Analyze token usage to determine chunking strategy
77
+ const tokenAnalysisOptions = {
78
+ reviewType: this.reviewType,
79
+ modelName: apiClientConfig.modelName,
80
+ batchTokenLimit: options.batchTokenLimit,
81
+ };
82
+
83
+ const tokenAnalysis = TokenAnalyzer.analyzeFiles(files, tokenAnalysisOptions);
84
+
85
+ // Log token analysis results
86
+ logger.info('Token analysis completed:');
87
+ logger.info(formatTokenAnalysis(tokenAnalysis, apiClientConfig.modelName));
88
+
89
+ // Create or get the review context
90
+ const reviewContext = new ReviewContext(projectName, this.reviewType, files);
91
+
92
+ // Determine if we need to use chunking
93
+ if (!tokenAnalysis.chunkingRecommendation.chunkingRecommended) {
94
+ logger.info('Content fits within context window, using standard review');
95
+ progressTracker.complete();
96
+
97
+ // If chunking is not needed, delegate to standard review process
98
+ return generateReview(
99
+ files,
100
+ projectName,
101
+ this.reviewType,
102
+ projectDocs,
103
+ options,
104
+ apiClientConfig,
105
+ );
106
+ }
107
+
108
+ // We need to use chunking
109
+ logger.info(
110
+ `Content exceeds context window (${tokenAnalysis.estimatedTotalTokens} > ${tokenAnalysis.contextWindowSize}), using multi-pass review`,
111
+ );
112
+ logger.info(`Estimated ${tokenAnalysis.estimatedPassesNeeded} passes needed`);
113
+
114
+ // Update progress tracker with actual pass count
115
+ const totalPasses = tokenAnalysis.estimatedPassesNeeded;
116
+ progressTracker.stopProgressUpdates();
117
+ const newProgressTracker = new MultiPassProgressTracker(totalPasses, files.length, {
118
+ quiet: options.quiet,
119
+ });
120
+
121
+ // Create a consolidated result to aggregate findings
122
+ let consolidatedResult: ReviewResult = {
123
+ content: '',
124
+ filePath: 'multi-pass-review',
125
+ files: files,
126
+ reviewType: this.reviewType,
127
+ timestamp: new Date().toISOString(),
128
+ costInfo: {
129
+ inputTokens: 0,
130
+ outputTokens: 0,
131
+ totalTokens: 0,
132
+ estimatedCost: 0,
133
+ formattedCost: '$0.00 USD',
134
+ cost: 0,
135
+ passCount: totalPasses,
136
+ perPassCosts: [],
137
+ contextMaintenanceFactor: options.contextMaintenanceFactor || 0.15,
138
+ },
139
+ totalPasses: totalPasses,
140
+ };
141
+
142
+ // Create filtered subsets of files for each pass
143
+ const chunks = tokenAnalysis.chunkingRecommendation.recommendedChunks;
144
+
145
+ // Process each chunk
146
+ for (let i = 0; i < chunks.length; i++) {
147
+ const chunk = chunks[i];
148
+ const passNumber = i + 1;
149
+ const chunkFiles = files.filter((f) => chunk.files.includes(f.path));
150
+
151
+ // Update progress tracker
152
+ newProgressTracker.startPass(
153
+ passNumber,
154
+ chunkFiles.map((f) => f.path),
155
+ );
156
+
157
+ // Start a new pass in the context
158
+ reviewContext.startPass();
159
+
160
+ // Generate next-pass context
161
+ const chunkContext = reviewContext.generateNextPassContext(chunkFiles.map((f) => f.path));
162
+
163
+ // Append the next-pass context to project docs
164
+ let enhancedProjectDocs = null;
165
+
166
+ if (projectDocs) {
167
+ enhancedProjectDocs = { ...projectDocs };
168
+ } else {
169
+ enhancedProjectDocs = { readme: '' };
170
+ }
171
+
172
+ // Add the review context to the project docs
173
+ enhancedProjectDocs.custom = {
174
+ ...(enhancedProjectDocs.custom || {}),
175
+ 'REVIEW_CONTEXT.md': chunkContext,
176
+ };
177
+
178
+ // Create a modified options object with metadata about the multi-pass process
179
+ const chunkOptions = {
180
+ ...options,
181
+ multiPass: true,
182
+ passNumber,
183
+ totalPasses: chunks.length,
184
+ };
185
+
186
+ // Generate review for this chunk with error handling
187
+ let chunkResult: ReviewResult | undefined;
188
+ let chunkRetryCount = 0;
189
+ const maxChunkRetries = 2;
190
+
191
+ while (chunkRetryCount <= maxChunkRetries) {
192
+ try {
193
+ // Enhanced logging before API call
194
+ logger.debug(
195
+ `Attempting to generate review for pass ${passNumber}, attempt ${chunkRetryCount + 1}/${maxChunkRetries + 1}`,
196
+ );
197
+ logger.debug(` Files in chunk: ${chunkFiles.length}`);
198
+ logger.debug(` API client: ${apiClientConfig.clientType}:${apiClientConfig.modelName}`);
199
+ logger.debug(` Review type: ${this.reviewType}`);
200
+
201
+ chunkResult = await generateReview(
202
+ chunkFiles,
203
+ projectName,
204
+ this.reviewType,
205
+ enhancedProjectDocs,
206
+ chunkOptions,
207
+ apiClientConfig,
208
+ );
209
+
210
+ // Log the result immediately after the API call
211
+ logger.debug(`API call completed for pass ${passNumber}, attempt ${chunkRetryCount + 1}`);
212
+ logger.debug(` Result exists: ${!!chunkResult}`);
213
+ logger.debug(` Content exists: ${!!(chunkResult && chunkResult.content)}`);
214
+ logger.debug(
215
+ ` Content length: ${chunkResult && chunkResult.content ? chunkResult.content.length : 'N/A'}`,
216
+ );
217
+ logger.debug(
218
+ ` Model used: ${chunkResult && chunkResult.modelUsed ? chunkResult.modelUsed : 'N/A'}`,
219
+ );
220
+
221
+ // Validate that we got valid content
222
+ if (!chunkResult || !chunkResult.content || chunkResult.content.trim() === '') {
223
+ // Enhanced error logging for debugging
224
+ logger.error(`Empty or invalid chunk result for pass ${passNumber}:`);
225
+ logger.error(` chunkResult exists: ${!!chunkResult}`);
226
+ logger.error(` chunkResult.content exists: ${!!(chunkResult && chunkResult.content)}`);
227
+ logger.error(
228
+ ` chunkResult.content length: ${chunkResult && chunkResult.content ? chunkResult.content.length : 'N/A'}`,
229
+ );
230
+ logger.error(
231
+ ` chunkResult.modelUsed: ${chunkResult && chunkResult.modelUsed ? chunkResult.modelUsed : 'N/A'}`,
232
+ );
233
+ logger.error(` chunkFiles count: ${chunkFiles.length}`);
234
+ logger.error(` chunkFiles paths: ${chunkFiles.map((f) => f.path).join(', ')}`);
235
+ logger.error(` apiClientConfig: ${JSON.stringify(apiClientConfig)}`);
236
+
237
+ // Log the full chunkResult for debugging (but limit size)
238
+ if (chunkResult) {
239
+ const resultForLogging = {
240
+ ...chunkResult,
241
+ content: chunkResult.content
242
+ ? `[${chunkResult.content.length} chars]: ${chunkResult.content.substring(0, 200)}...`
243
+ : 'null/undefined',
244
+ };
245
+ logger.error(` Full chunkResult: ${JSON.stringify(resultForLogging, null, 2)}`);
246
+ }
247
+
248
+ throw new Error(`Empty or invalid chunk result for pass ${passNumber}`);
249
+ }
250
+
251
+ // Success - break out of retry loop
252
+ break;
253
+ } catch (chunkError) {
254
+ chunkRetryCount++;
255
+ logger.error(
256
+ `Error generating review for pass ${passNumber} (attempt ${chunkRetryCount}/${maxChunkRetries + 1}): ${
257
+ chunkError instanceof Error ? chunkError.message : String(chunkError)
258
+ }`,
259
+ );
260
+
261
+ if (chunkRetryCount > maxChunkRetries) {
262
+ // Create a fallback result for this chunk
263
+ logger.warn(
264
+ `Creating fallback result for pass ${passNumber} after ${maxChunkRetries + 1} attempts`,
265
+ );
266
+ chunkResult = {
267
+ content: `## Error in Pass ${passNumber}\n\nFailed to generate review for ${chunkFiles.length} files after ${maxChunkRetries + 1} attempts.\n\nFiles attempted:\n${chunkFiles.map((f) => `- ${f.path}`).join('\n')}\n\nError: ${chunkError instanceof Error ? chunkError.message : String(chunkError)}`,
268
+ filePath: 'error-pass',
269
+ files: chunkFiles,
270
+ reviewType: this.reviewType,
271
+ timestamp: new Date().toISOString(),
272
+ costInfo: {
273
+ inputTokens: 0,
274
+ outputTokens: 0,
275
+ totalTokens: 0,
276
+ estimatedCost: 0,
277
+ formattedCost: '$0.00 USD',
278
+ cost: 0,
279
+ },
280
+ };
281
+ break;
282
+ }
283
+
284
+ // Wait before retrying
285
+ const waitTime = Math.min(2000 * 2 ** (chunkRetryCount - 1), 8000);
286
+ logger.info(
287
+ `Waiting ${waitTime}ms before retry ${chunkRetryCount + 1}/${maxChunkRetries + 1} for pass ${passNumber}`,
288
+ );
289
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
290
+ }
291
+ }
292
+
293
+ // Ensure we have a valid result
294
+ if (!chunkResult) {
295
+ // This should never happen, but provide a safety fallback
296
+ logger.error(
297
+ `No chunk result generated for pass ${passNumber} - creating emergency fallback`,
298
+ );
299
+ chunkResult = {
300
+ content: `## Error in Pass ${passNumber}\n\nNo result generated for ${chunkFiles.length} files.\n\nFiles attempted:\n${chunkFiles.map((f) => `- ${f.path}`).join('\n')}\n\nEmergency fallback result.`,
301
+ filePath: 'emergency-fallback',
302
+ files: chunkFiles,
303
+ reviewType: this.reviewType,
304
+ timestamp: new Date().toISOString(),
305
+ costInfo: {
306
+ inputTokens: 0,
307
+ outputTokens: 0,
308
+ totalTokens: 0,
309
+ estimatedCost: 0,
310
+ formattedCost: '$0.00 USD',
311
+ cost: 0,
312
+ },
313
+ };
314
+ }
315
+
316
+ // Extract findings from this pass and update the context
317
+ this.updateContextFromReviewResults(reviewContext, chunkResult, chunkFiles);
318
+
319
+ // Accumulate costs
320
+ if (consolidatedResult.costInfo && chunkResult.costInfo) {
321
+ consolidatedResult.costInfo.inputTokens += chunkResult.costInfo.inputTokens || 0;
322
+ consolidatedResult.costInfo.outputTokens += chunkResult.costInfo.outputTokens || 0;
323
+ consolidatedResult.costInfo.totalTokens += chunkResult.costInfo.totalTokens || 0;
324
+ consolidatedResult.costInfo.estimatedCost += chunkResult.costInfo.estimatedCost || 0;
325
+
326
+ // Update formatted cost
327
+ consolidatedResult.costInfo.formattedCost = `$${consolidatedResult.costInfo.estimatedCost.toFixed(6)} USD`;
328
+
329
+ // Add per-pass cost information
330
+ if (
331
+ consolidatedResult.costInfo.perPassCosts &&
332
+ Array.isArray(consolidatedResult.costInfo.perPassCosts)
333
+ ) {
334
+ consolidatedResult.costInfo.perPassCosts.push({
335
+ passNumber: passNumber,
336
+ inputTokens: chunkResult.costInfo.inputTokens || 0,
337
+ outputTokens: chunkResult.costInfo.outputTokens || 0,
338
+ totalTokens: chunkResult.costInfo.totalTokens || 0,
339
+ estimatedCost: chunkResult.costInfo.estimatedCost || 0,
340
+ });
341
+ }
342
+ }
343
+
344
+ // Accumulate content with pass information
345
+ consolidatedResult.content += `\n## Pass ${passNumber}: Review of ${chunkFiles.length} Files\n\n`;
346
+ consolidatedResult.content += chunkResult.content;
347
+ consolidatedResult.content += '\n\n';
348
+
349
+ // Mark the pass as complete
350
+ newProgressTracker.completePass(passNumber);
351
+ }
352
+
353
+ // Set the initial processing phase
354
+ newProgressTracker.setPhase('processing');
355
+
356
+ // Add a summary section based on token analysis
357
+ const initialSummary = this.generateMultiPassSummary(
358
+ consolidatedResult,
359
+ tokenAnalysis,
360
+ reviewContext,
361
+ files,
362
+ apiClientConfig.modelName,
363
+ );
364
+
365
+ // Add the initial summary to the consolidated result
366
+ consolidatedResult.content = initialSummary + consolidatedResult.content;
367
+
368
+ // Set the consolidation phase for the final AI analysis
369
+ newProgressTracker.setPhase('consolidating');
370
+
371
+ // Create a final consolidated report through AI with robust error handling
372
+ logger.info('Generating final consolidated review report with grading...');
373
+
374
+ // Check if we have enough valid content to consolidate
375
+ const validPassCount = consolidatedResult.content.split('## Pass').length - 1;
376
+ const errorPassCount = consolidatedResult.content.split('## Error in Pass').length - 1;
377
+
378
+ if (validPassCount === 0) {
379
+ logger.error('No valid passes to consolidate - all passes failed');
380
+ consolidatedResult = {
381
+ ...consolidatedResult,
382
+ content: `# Review Failed\n\nAll ${totalPasses} passes failed to generate valid reviews. Please check the logs for details.\n\n${consolidatedResult.content}`,
383
+ };
384
+ } else if (errorPassCount > validPassCount * 0.5) {
385
+ logger.warn(`High error rate: ${errorPassCount}/${totalPasses} passes failed`);
386
+ }
387
+
388
+ try {
389
+ logger.debug(
390
+ 'Starting consolidation phase with model provider: ' +
391
+ apiClientConfig.provider +
392
+ ', model: ' +
393
+ apiClientConfig.modelName,
394
+ );
395
+
396
+ // Make sure model information is set in the consolidated result
397
+ // This ensures we use the same model for consolidation
398
+ // Note: apiClientConfig.modelName already contains the full model string (e.g., "openrouter:anthropic/claude-3-haiku")
399
+ consolidatedResult.modelUsed = apiClientConfig.modelName;
400
+
401
+ const finalReport = await this.generateConsolidatedReport(
402
+ consolidatedResult,
403
+ apiClientConfig,
404
+ files,
405
+ projectName,
406
+ projectDocs,
407
+ options,
408
+ );
409
+
410
+ // If the final report was generated successfully, use it instead
411
+ if (finalReport?.content && finalReport.content.trim() !== '') {
412
+ logger.info('Successfully generated consolidated report with grading');
413
+ consolidatedResult = finalReport;
414
+ } else {
415
+ // If the final report wasn't generated (returned undefined), log a more detailed warning
416
+ logger.warn('Consolidation function returned empty or undefined result');
417
+ logger.warn('Creating enhanced fallback consolidated report');
418
+
419
+ // Create an enhanced fallback consolidated report
420
+ const passResults = this.extractPassResults(consolidatedResult);
421
+ const fallbackContent = await this.consolidationService.generateConsolidatedReport(
422
+ consolidatedResult,
423
+ apiClientConfig,
424
+ {
425
+ projectName,
426
+ modelName: apiClientConfig.modelName,
427
+ totalPasses: consolidatedResult.totalPasses || 1,
428
+ passResults,
429
+ },
430
+ );
431
+ const fallbackReport = {
432
+ ...consolidatedResult,
433
+ content: fallbackContent,
434
+ };
435
+ consolidatedResult = fallbackReport;
436
+ }
437
+ } catch (error) {
438
+ logger.error(
439
+ `Failed to generate final consolidated report: ${error instanceof Error ? error.message : String(error)}`,
440
+ );
441
+ logger.error('Error occurred during consolidated report generation. Stack trace:');
442
+ if (error instanceof Error && error.stack) {
443
+ logger.error(error.stack);
444
+ }
445
+ logger.warn('Creating enhanced fallback consolidated report');
446
+
447
+ // Create an enhanced fallback consolidated report even in the case of an exception
448
+ const passResults = this.extractPassResults(consolidatedResult);
449
+ const fallbackContent = await this.consolidationService.generateConsolidatedReport(
450
+ consolidatedResult,
451
+ apiClientConfig,
452
+ {
453
+ projectName,
454
+ modelName: apiClientConfig.modelName,
455
+ totalPasses: consolidatedResult.totalPasses || 1,
456
+ passResults,
457
+ },
458
+ );
459
+ const fallbackReport = {
460
+ ...consolidatedResult,
461
+ content: fallbackContent,
462
+ };
463
+ consolidatedResult = fallbackReport;
464
+ }
465
+
466
+ // Mark the review as complete
467
+ newProgressTracker.complete();
468
+
469
+ return consolidatedResult;
470
+ }
471
+
472
+ /**
473
+ * Generate a summary for the multi-pass review
474
+ * @param result Consolidated review result
475
+ * @param tokenAnalysis Token analysis result
476
+ * @param context Review context
477
+ * @param files All files in the review
478
+ * @param modelName Model name
479
+ * @returns Summary string
480
+ */
481
+ private generateMultiPassSummary(
482
+ result: ReviewResult,
483
+ tokenAnalysis: TokenAnalysisResult,
484
+ context: ReviewContext,
485
+ files: FileInfo[],
486
+ modelName: string,
487
+ ): string {
488
+ const findings = context.getFindings();
489
+ const filesCount = files.length;
490
+ const totalPassesCount = context.getCurrentPass();
491
+
492
+ // Get cost info for detailed reporting
493
+ const costInfo = result.costInfo;
494
+ const costBreakdown = costInfo?.perPassCosts
495
+ ? costInfo.perPassCosts
496
+ .map(
497
+ (passCost: PassCostInfo) =>
498
+ `- Pass ${passCost.passNumber}: ${passCost.inputTokens.toLocaleString()} input + ${passCost.outputTokens.toLocaleString()} output = ${passCost.totalTokens.toLocaleString()} tokens ($${passCost.estimatedCost.toFixed(4)} USD)`,
499
+ )
500
+ .join('\n')
501
+ : 'N/A';
502
+
503
+ return `# Multi-Pass ${this.reviewType.charAt(0).toUpperCase() + this.reviewType.slice(1)} Review Summary
504
+
505
+ This review was conducted in **${totalPassesCount} passes** to analyze **${filesCount} files** (${tokenAnalysis.totalSizeInBytes.toLocaleString()} bytes) due to the large size of the codebase.
506
+
507
+ ## Review Statistics
508
+ - Files analyzed: ${filesCount}
509
+ - Total passes: ${totalPassesCount}
510
+ - Model used: ${modelName}
511
+ - Key findings identified: ${findings.length}
512
+
513
+ ## Token Usage
514
+ - Content tokens: ${tokenAnalysis.totalTokens.toLocaleString()}
515
+ - Context window size: ${tokenAnalysis.contextWindowSize.toLocaleString()}
516
+ - Context utilization: ${((tokenAnalysis.estimatedTotalTokens / tokenAnalysis.contextWindowSize) * 100).toFixed(2)}%
517
+ ${
518
+ costInfo
519
+ ? `- Total tokens used: ${costInfo.totalTokens.toLocaleString()} (input: ${costInfo.inputTokens.toLocaleString()}, output: ${costInfo.outputTokens.toLocaleString()})
520
+ - Estimated cost: ${costInfo.formattedCost}`
521
+ : ''
522
+ }
523
+
524
+ ### Per-Pass Token Usage
525
+ ${costBreakdown}
526
+
527
+ ## Multi-Pass Methodology
528
+ This review used a multi-pass approach with context maintenance between passes to ensure a cohesive analysis despite the large codebase size. Each pass analyzed a subset of files while maintaining awareness of findings and relationships from previous passes.
529
+
530
+ `;
531
+ }
532
+
533
+ /**
534
+ * Update the review context with findings from a review result
535
+ * @param context Review context to update
536
+ * @param result Review result to extract findings from
537
+ * @param files Files included in this pass
538
+ */
539
+ private updateContextFromReviewResult(
540
+ context: ReviewContext,
541
+ result: ReviewResult,
542
+ files: FileInfo[],
543
+ ): void {
544
+ // Add file summaries
545
+ files.forEach((file) => {
546
+ context.addFileSummary({
547
+ path: file.path,
548
+ type: file.path.split('.').pop() || 'unknown',
549
+ description: `File containing ${file.content.length} bytes of code`,
550
+ keyElements: [],
551
+ passNumber: context.getCurrentPass(),
552
+ });
553
+ });
554
+
555
+ // In a real implementation, we would parse the review result to extract:
556
+ // - Code elements (functions, classes, etc.)
557
+ // - Findings (bugs, suggestions, etc.)
558
+ // - Relationships between files
559
+
560
+ // For now, add a general note about the pass
561
+ context.addGeneralNote(
562
+ `Pass ${context.getCurrentPass()} analyzed ${files.length} files and generated a ${result.content.length} character review.`,
563
+ );
564
+ }
565
+
566
+ /**
567
+ * Generate a consolidated report from the multi-pass review results
568
+ * @param multiPassResult Combined result from all passes
569
+ * @param apiClientConfig API client configuration
570
+ * @param files All files included in the review
571
+ * @param projectName Name of the project
572
+ * @param projectDocs Project documentation
573
+ * @param options Review options
574
+ * @returns Promise resolving to a consolidated review result, or undefined if consolidation fails
575
+ */
576
+ private async generateConsolidatedReport(
577
+ multiPassResult: ReviewResult,
578
+ apiClientConfig: ApiClientConfig,
579
+ files: FileInfo[],
580
+ projectName: string,
581
+ projectDocs: ProjectDocs | null,
582
+ options: ReviewOptions,
583
+ ): Promise<ReviewResult | undefined> {
584
+ try {
585
+ // Validate API client configuration for consolidation
586
+ if (!apiClientConfig.modelName) {
587
+ throw new Error('API client configuration is missing a model name for consolidation');
588
+ }
589
+
590
+ // Set the project name in the result for use in consolidation
591
+ multiPassResult.projectName = projectName;
592
+
593
+ // Extract pass results for consolidation
594
+ const passResults = this.extractPassResults(multiPassResult);
595
+
596
+ // Use the ConsolidationService to generate the final report
597
+ const consolidatedContent = await this.consolidationService.generateConsolidatedReport(
598
+ multiPassResult,
599
+ apiClientConfig,
600
+ {
601
+ projectName,
602
+ modelName: apiClientConfig.modelName,
603
+ totalPasses: multiPassResult.totalPasses || 1,
604
+ passResults,
605
+ },
606
+ );
607
+
608
+ // If the consolidation failed (empty content), return undefined
609
+ if (!consolidatedContent || consolidatedContent.trim() === '') {
610
+ logger.warn('Received empty consolidated content');
611
+ return undefined;
612
+ }
613
+
614
+ logger.info('Successfully generated consolidated report with grading!');
615
+
616
+ // Create a new result with the consolidated content
617
+ const consolidatedResult: ReviewResult = {
618
+ ...multiPassResult,
619
+ content: consolidatedContent,
620
+ timestamp: new Date().toISOString(),
621
+ totalPasses: (multiPassResult.totalPasses || 0) + 1,
622
+ };
623
+
624
+ // Return the consolidated result
625
+ return consolidatedResult;
626
+ } catch (error) {
627
+ logger.error(
628
+ `Error generating consolidated report: ${error instanceof Error ? error.message : String(error)}`,
629
+ );
630
+ return undefined;
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Extract individual pass results from the consolidated multi-pass result
636
+ * @param multiPassResult The consolidated result containing all passes
637
+ * @returns Array of individual pass results
638
+ */
639
+ private extractPassResults(multiPassResult: ReviewResult): ReviewResult[] {
640
+ const passResults: ReviewResult[] = [];
641
+ const content = multiPassResult.content || '';
642
+
643
+ // Split content by pass markers
644
+ const passSections = content.split(/## Pass \d+:/);
645
+
646
+ for (let i = 1; i < passSections.length; i++) {
647
+ const passContent = passSections[i].trim();
648
+ if (passContent) {
649
+ passResults.push({
650
+ content: passContent,
651
+ filePath: `pass-${i}`,
652
+ reviewType: multiPassResult.reviewType,
653
+ timestamp: multiPassResult.timestamp,
654
+ files: multiPassResult.files,
655
+ });
656
+ }
657
+ }
658
+
659
+ // If no passes were found, return the original result as a single pass
660
+ if (passResults.length === 0) {
661
+ passResults.push(multiPassResult);
662
+ }
663
+
664
+ return passResults;
665
+ }
666
+
667
+ /**
668
+ * Extract findings from valid passes using the FindingsExtractor service
669
+ */
670
+ private extractFindingsFromPasses(validPasses: Array<{ content: string }>) {
671
+ return this.findingsExtractor.extractFindingsFromPasses(validPasses);
672
+ }
673
+
674
+ /**
675
+ * Calculate overall grade using the FindingsExtractor service
676
+ */
677
+ private calculateOverallGrade(findings: {
678
+ high: Set<string>;
679
+ medium: Set<string>;
680
+ low: Set<string>;
681
+ }): string {
682
+ return this.findingsExtractor.calculateOverallGrade(findings);
683
+ }
684
+
685
+ /**
686
+ * Generate recommendations based on findings using the FindingsExtractor service
687
+ */
688
+ private generateRecommendations(
689
+ findings: { high: Set<string>; medium: Set<string>; low: Set<string> },
690
+ hasErrors: boolean,
691
+ ): string[] {
692
+ return this.findingsExtractor.generateRecommendations(findings, hasErrors);
693
+ }
694
+
695
+ /**
696
+ * Update the review context with findings from multiple review results
697
+ * @param context Review context to update
698
+ * @param result Review result to extract findings from
699
+ * @param files Files included in this pass
700
+ */
701
+ private updateContextFromReviewResults(
702
+ context: ReviewContext,
703
+ result: ReviewResult,
704
+ files: FileInfo[],
705
+ ): void {
706
+ // Add file summaries for all files in this pass
707
+ files.forEach((file) => {
708
+ if (!file.path) return;
709
+
710
+ // Extract the file extension
711
+ const fileExtension = file.path.split('.').pop() || 'unknown';
712
+
713
+ // Create a basic file summary
714
+ context.addFileSummary({
715
+ path: file.path,
716
+ type: fileExtension,
717
+ description: `${fileExtension.toUpperCase()} file with ${file.content.length} bytes`,
718
+ keyElements: [],
719
+ passNumber: context.getCurrentPass(),
720
+ });
721
+ });
722
+
723
+ // Simple heuristic to extract findings from the review content
724
+ // In a real implementation, we would have a more structured approach to extract findings
725
+ const findingPatterns = [
726
+ { type: 'bug', regex: /bug|issue|error|fix needed|incorrect/gi, severity: 8 },
727
+ {
728
+ type: 'security',
729
+ regex: /security|vulnerability|exploit|injection|xss|csrf/gi,
730
+ severity: 9,
731
+ },
732
+ {
733
+ type: 'performance',
734
+ regex: /performance|slow|optimize|efficiency|bottleneck/gi,
735
+ severity: 7,
736
+ },
737
+ {
738
+ type: 'maintainability',
739
+ regex: /maintainability|hard to read|complex|refactor/gi,
740
+ severity: 6,
741
+ },
742
+ ];
743
+
744
+ // Split the review content into paragraphs
745
+ const paragraphs = result.content.split('\n\n');
746
+
747
+ // Examine each paragraph for potential findings
748
+ paragraphs.forEach((paragraph) => {
749
+ findingPatterns.forEach((pattern) => {
750
+ if (pattern.regex.test(paragraph)) {
751
+ // Extract a short description from the paragraph
752
+ let description = paragraph.substring(0, 100);
753
+ if (description.length === 100) {
754
+ description += '...';
755
+ }
756
+
757
+ // Determine which file this finding is about (if mentioned)
758
+ let file: string | undefined;
759
+ files.forEach((f) => {
760
+ if (
761
+ f.path &&
762
+ (paragraph.includes(f.path) || (f.relativePath && paragraph.includes(f.relativePath)))
763
+ ) {
764
+ file = f.path;
765
+ }
766
+ });
767
+
768
+ // Add the finding to the context
769
+ context.addFinding({
770
+ type: pattern.type,
771
+ description,
772
+ file,
773
+ severity: pattern.severity,
774
+ passNumber: context.getCurrentPass(),
775
+ });
776
+ }
777
+ });
778
+ });
779
+
780
+ // Add a general note about this pass
781
+ context.addGeneralNote(
782
+ `Pass ${context.getCurrentPass()} reviewed ${files.length} files and identified approximate findings based on text heuristics.`,
783
+ );
784
+ }
785
+ }