@probelabs/visor 0.1.17 → 0.1.19

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 (68) hide show
  1. package/README.md +78 -0
  2. package/defaults/.visor.yaml +53 -1
  3. package/dist/ai-review-service.js +2 -2
  4. package/dist/ai-review-service.js.map +1 -1
  5. package/dist/check-execution-engine.d.ts +32 -1
  6. package/dist/check-execution-engine.d.ts.map +1 -1
  7. package/dist/check-execution-engine.js +289 -19
  8. package/dist/check-execution-engine.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +18 -3
  11. package/dist/config.js.map +1 -1
  12. package/dist/event-mapper.d.ts.map +1 -1
  13. package/dist/event-mapper.js +1 -1
  14. package/dist/event-mapper.js.map +1 -1
  15. package/dist/failure-condition-evaluator.d.ts +8 -0
  16. package/dist/failure-condition-evaluator.d.ts.map +1 -1
  17. package/dist/failure-condition-evaluator.js +25 -3
  18. package/dist/failure-condition-evaluator.js.map +1 -1
  19. package/dist/github-check-service.d.ts.map +1 -1
  20. package/dist/github-check-service.js +26 -39
  21. package/dist/github-check-service.js.map +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +331 -690
  24. package/dist/index.js.map +1 -1
  25. package/dist/licenses.txt +2300 -0
  26. package/dist/output/code-review/schema.json +84 -0
  27. package/dist/output/code-review/template.liquid +32 -0
  28. package/dist/output/plain/schema.json +14 -0
  29. package/dist/output/plain/template.liquid +1 -0
  30. package/dist/output-formatters.d.ts.map +1 -1
  31. package/dist/output-formatters.js +14 -14
  32. package/dist/output-formatters.js.map +1 -1
  33. package/dist/pr-analyzer.d.ts +2 -1
  34. package/dist/pr-analyzer.d.ts.map +1 -1
  35. package/dist/pr-analyzer.js +2 -1
  36. package/dist/pr-analyzer.js.map +1 -1
  37. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  38. package/dist/providers/ai-check-provider.js +4 -0
  39. package/dist/providers/ai-check-provider.js.map +1 -1
  40. package/dist/providers/check-provider-registry.js +2 -2
  41. package/dist/providers/check-provider-registry.js.map +1 -1
  42. package/dist/providers/check-provider.interface.d.ts +2 -0
  43. package/dist/providers/check-provider.interface.d.ts.map +1 -1
  44. package/dist/providers/check-provider.interface.js.map +1 -1
  45. package/dist/providers/index.d.ts +1 -1
  46. package/dist/providers/index.d.ts.map +1 -1
  47. package/dist/providers/index.js +3 -3
  48. package/dist/providers/index.js.map +1 -1
  49. package/dist/providers/noop-check-provider.d.ts +25 -0
  50. package/dist/providers/noop-check-provider.d.ts.map +1 -0
  51. package/dist/providers/noop-check-provider.js +55 -0
  52. package/dist/providers/noop-check-provider.js.map +1 -0
  53. package/dist/providers/tool-check-provider.d.ts +4 -1
  54. package/dist/providers/tool-check-provider.d.ts.map +1 -1
  55. package/dist/providers/tool-check-provider.js +64 -15
  56. package/dist/providers/tool-check-provider.js.map +1 -1
  57. package/dist/reviewer.d.ts +19 -37
  58. package/dist/reviewer.d.ts.map +1 -1
  59. package/dist/reviewer.js +83 -593
  60. package/dist/reviewer.js.map +1 -1
  61. package/dist/tiktoken_bg.wasm +0 -0
  62. package/dist/types/config.d.ts +52 -5
  63. package/dist/types/config.d.ts.map +1 -1
  64. package/package.json +3 -3
  65. package/dist/providers/script-check-provider.d.ts +0 -23
  66. package/dist/providers/script-check-provider.d.ts.map +0 -1
  67. package/dist/providers/script-check-provider.js +0 -163
  68. package/dist/providers/script-check-provider.js.map +0 -1
package/dist/index.js CHANGED
@@ -41,11 +41,8 @@ const commands_1 = require("./commands");
41
41
  const pr_analyzer_1 = require("./pr-analyzer");
42
42
  const reviewer_1 = require("./reviewer");
43
43
  const action_cli_bridge_1 = require("./action-cli-bridge");
44
- const github_comments_1 = require("./github-comments");
45
44
  const config_1 = require("./config");
46
- const pr_detector_1 = require("./pr-detector");
47
45
  const github_check_service_1 = require("./github-check-service");
48
- const failure_condition_evaluator_1 = require("./failure-condition-evaluator");
49
46
  /**
50
47
  * Create an authenticated Octokit instance using either GitHub App or token authentication
51
48
  */
@@ -131,7 +128,6 @@ async function run() {
131
128
  'github-token': token,
132
129
  owner: (0, core_1.getInput)('owner') || process.env.GITHUB_REPOSITORY_OWNER,
133
130
  repo: (0, core_1.getInput)('repo') || process.env.GITHUB_REPOSITORY?.split('/')[1],
134
- 'auto-review': (0, core_1.getInput)('auto-review'),
135
131
  debug: (0, core_1.getInput)('debug'),
136
132
  // GitHub App authentication inputs
137
133
  'app-id': (0, core_1.getInput)('app-id') || undefined,
@@ -153,7 +149,6 @@ async function run() {
153
149
  'visor-checks': (0, core_1.getInput)('visor-checks') || undefined,
154
150
  };
155
151
  const eventName = process.env.GITHUB_EVENT_NAME;
156
- const autoReview = inputs['auto-review'] === 'true';
157
152
  // Create GitHub context for CLI bridge
158
153
  const context = {
159
154
  event_name: eventName || 'unknown',
@@ -176,26 +171,35 @@ async function run() {
176
171
  console.log('Debug: inputs.visor-config-path =', inputs['visor-config-path']);
177
172
  if (cliBridge.shouldUseVisor(inputs)) {
178
173
  console.log('🔍 Using Visor CLI mode');
179
- // ENHANCED FIX: For PR auto-reviews, detect PR context across all event types
180
- const isAutoReview = inputs['auto-review'] === 'true';
181
- if (isAutoReview) {
182
- console.log('🔄 Auto-review enabled - attempting to detect PR context across all event types');
183
- // Try to detect if we're in a PR context (works for push, pull_request, issue_comment, etc.)
184
- const prDetected = await detectPRContext(inputs, context, octokit);
185
- if (prDetected) {
186
- console.log('✅ PR context detected - using GitHub API for PR analysis');
187
- await handlePullRequestVisorMode(inputs, context, octokit, authType);
188
- return;
189
- }
190
- else {
191
- console.log('ℹ️ No PR context detected - proceeding with CLI mode for general analysis');
192
- }
193
- }
194
174
  await handleVisorMode(cliBridge, inputs, context, octokit);
195
175
  return;
196
176
  }
197
- console.log('🤖 Using legacy GitHub Action mode');
198
- await handleLegacyMode(octokit, inputs, eventName, autoReview);
177
+ // Default behavior: Use Visor config to determine what to run
178
+ console.log('🤖 Using config-driven mode');
179
+ // Load config to determine which checks should run for this event
180
+ const configManager = new config_1.ConfigManager();
181
+ let config;
182
+ try {
183
+ config = await configManager.findAndLoadConfig();
184
+ console.log('📋 Loaded Visor config');
185
+ }
186
+ catch {
187
+ // Use default config if none found
188
+ config = {
189
+ version: '1.0',
190
+ checks: {},
191
+ output: {
192
+ pr_comment: {
193
+ format: 'markdown',
194
+ group_by: 'check',
195
+ collapse: false,
196
+ },
197
+ },
198
+ };
199
+ console.log('📋 Using default configuration');
200
+ }
201
+ // Determine which event we're handling and run appropriate checks
202
+ await handleEvent(octokit, inputs, eventName, context, config);
199
203
  }
200
204
  catch (error) {
201
205
  (0, core_1.setFailed)(error instanceof Error ? error.message : 'Unknown error');
@@ -253,231 +257,103 @@ async function handleVisorMode(cliBridge, inputs, _context, _octokit) {
253
257
  (0, core_1.setFailed)(error instanceof Error ? error.message : 'Visor mode failed');
254
258
  }
255
259
  }
256
- /**
257
- * Post CLI review results as PR comment with robust PR detection
258
- */
259
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
260
- async function postCliReviewComment(cliOutput, inputs, octokit) {
261
- try {
262
- const owner = inputs.owner || process.env.GITHUB_REPOSITORY_OWNER;
263
- const repo = inputs.repo || process.env.GITHUB_REPOSITORY?.split('/')[1];
264
- if (!owner || !repo) {
265
- console.log('⚠️ Missing required parameters for PR comment creation');
266
- return;
267
- }
268
- // Use the provided authenticated Octokit instance
269
- const prDetector = new pr_detector_1.PRDetector(octokit, inputs.debug === 'true');
270
- // Convert GitHub context to our format
271
- const eventContext = {
272
- event_name: process.env.GITHUB_EVENT_NAME || 'unknown',
273
- repository: {
274
- owner: { login: owner },
275
- name: repo,
276
- },
277
- event: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT).event : undefined,
278
- payload: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT) : {},
279
- };
280
- // Use robust PR detection
281
- const prResult = await prDetector.detectPRNumber(eventContext, owner, repo);
282
- if (!prResult.prNumber) {
283
- console.log(`⚠️ No PR found using any detection strategy: ${prResult.details || 'Unknown reason'}`);
284
- if (inputs.debug === 'true') {
285
- console.log('Available detection strategies:');
286
- prDetector.getDetectionStrategies().forEach(strategy => console.log(` ${strategy}`));
287
- }
288
- return;
289
- }
290
- console.log(`✅ Found PR #${prResult.prNumber} using ${prResult.source} (confidence: ${prResult.confidence})`);
291
- if (prResult.details) {
292
- console.log(` Details: ${prResult.details}`);
293
- }
294
- const commentManager = new github_comments_1.CommentManager(octokit);
295
- // Create Visor-formatted comment from CLI output
296
- let comment = `# 🔍 Visor Code Review Results\n\n`;
297
- comment += `## 📊 Summary\n`;
298
- comment += `- **Overall Score**: ${cliOutput.overallScore || 0}/100\n`;
299
- comment += `- **Issues Found**: ${cliOutput.totalIssues || 0} (${cliOutput.criticalIssues || 0} Critical)\n`;
300
- comment += `- **Files Analyzed**: ${cliOutput.filesAnalyzed || 'N/A'}\n\n`;
301
- // Add category scores if available
302
- if (cliOutput.securityScore ||
303
- cliOutput.performanceScore ||
304
- cliOutput.styleScore ||
305
- cliOutput.architectureScore) {
306
- comment += `## 📈 Category Scores\n`;
307
- if (cliOutput.securityScore !== undefined)
308
- comment += `- **Security**: ${cliOutput.securityScore}/100\n`;
309
- if (cliOutput.performanceScore !== undefined)
310
- comment += `- **Performance**: ${cliOutput.performanceScore}/100\n`;
311
- if (cliOutput.styleScore !== undefined)
312
- comment += `- **Style**: ${cliOutput.styleScore}/100\n`;
313
- if (cliOutput.architectureScore !== undefined)
314
- comment += `- **Architecture**: ${cliOutput.architectureScore}/100\n`;
315
- comment += '\n';
316
- }
317
- // Load config to determine grouping method
318
- const { ConfigManager } = await Promise.resolve().then(() => __importStar(require('./config')));
319
- const configManager = new ConfigManager();
320
- const config = await configManager.findAndLoadConfig();
321
- // Add issues grouped by check or category based on config
322
- if (cliOutput.issues && cliOutput.issues.length > 0) {
323
- // Always use check-based grouping when configured
324
- const useCheckGrouping = config.output?.pr_comment?.group_by === 'check';
325
- const groupedIssues = useCheckGrouping
326
- ? groupIssuesByCheck(cliOutput.issues)
327
- : groupIssuesByCategory(cliOutput.issues);
328
- // Get configured checks for filtering
329
- const configuredChecks = config.checks ? Object.keys(config.checks) : [];
330
- for (const [groupKey, issues] of Object.entries(groupedIssues)) {
331
- if (issues.length === 0)
332
- continue;
333
- // When using check-based grouping, only show configured checks
334
- if (useCheckGrouping && configuredChecks.length > 0) {
335
- // Skip if not a configured check (unless it's uncategorized)
336
- if (!configuredChecks.includes(groupKey) && groupKey !== 'uncategorized') {
337
- continue;
338
- }
339
- }
340
- const title = `${groupKey.charAt(0).toUpperCase() + groupKey.slice(1)} Issues (${issues.length})`;
341
- let sectionContent = '';
342
- for (const issue of issues.slice(0, 5)) {
343
- // Limit to 5 issues per category
344
- sectionContent += `- **${issue.severity?.toUpperCase() || 'UNKNOWN'}**: ${issue.message}\n`;
345
- sectionContent += ` - **File**: \`${issue.file}:${issue.line}\`\n\n`;
346
- }
347
- if (issues.length > 5) {
348
- sectionContent += `*...and ${issues.length - 5} more issues in this category.*\n\n`;
349
- }
350
- comment += commentManager.createCollapsibleSection(title, sectionContent, true);
351
- comment += '\n\n';
352
- }
353
- }
354
- // Add suggestions if any
355
- if (cliOutput.suggestions && cliOutput.suggestions.length > 0) {
356
- const suggestionsContent = cliOutput.suggestions.map((s) => `- ${s}`).join('\n') + '\n';
357
- comment += commentManager.createCollapsibleSection('💡 Recommendations', suggestionsContent, true);
358
- comment += '\n\n';
359
- }
360
- // Add debug information if available
361
- if (cliOutput.debug) {
362
- const debugContent = formatDebugInfo(cliOutput.debug);
363
- comment +=
364
- '\n\n' +
365
- commentManager.createCollapsibleSection('🐛 Debug Information', debugContent, false);
366
- comment += '\n\n';
367
- }
368
- // Fetch fresh PR data to get the latest commit SHA
369
- let latestCommitSha;
370
- try {
371
- const { data: pullRequest } = await octokit.rest.pulls.get({
372
- owner,
373
- repo,
374
- pull_number: prResult.prNumber,
375
- });
376
- latestCommitSha = pullRequest.head.sha;
377
- console.log(`📝 Latest commit SHA: ${latestCommitSha.substring(0, 7)}`);
378
- }
379
- catch (error) {
380
- console.warn('⚠️ Could not fetch latest PR data:', error);
381
- // Fallback to environment or event data
382
- latestCommitSha =
383
- eventContext.event?.pull_request?.head?.sha ||
384
- eventContext.payload?.pull_request?.head
385
- ?.sha ||
386
- process.env.GITHUB_SHA;
387
- }
388
- // Use smart comment updating with unique ID
389
- const commentId = `visor-cli-review-${prResult.prNumber}`;
390
- await commentManager.updateOrCreateComment(owner, repo, prResult.prNumber, comment, {
391
- commentId,
392
- triggeredBy: 'visor-cli',
393
- allowConcurrentUpdates: true,
394
- commitSha: latestCommitSha,
395
- });
396
- console.log(`✅ Posted CLI review comment to PR #${prResult.prNumber}`);
397
- }
398
- catch (error) {
399
- console.error('❌ Failed to post CLI review comment:', error);
400
- }
401
- }
402
- function groupIssuesByCategory(issues) {
403
- const grouped = {
404
- security: [],
405
- performance: [],
406
- style: [],
407
- logic: [],
408
- documentation: [],
409
- architecture: [],
410
- };
411
- for (const issue of issues) {
412
- const category = issue.category || 'logic';
413
- if (!grouped[category])
414
- grouped[category] = [];
415
- grouped[category].push(issue);
416
- }
417
- return grouped;
418
- }
419
- /**
420
- * Group issues by the check that found them (extracted from ruleId prefix)
421
- */
422
- function groupIssuesByCheck(issues) {
423
- const grouped = {};
424
- for (const issue of issues) {
425
- // Extract check name from ruleId prefix
426
- // Format: "checkName/specific-rule" -> "checkName"
427
- let checkName = 'uncategorized';
428
- if (issue.ruleId && issue.ruleId.includes('/')) {
429
- const parts = issue.ruleId.split('/');
430
- checkName = parts[0];
431
- }
432
- // No fallback to category - only use ruleId prefix
433
- if (!grouped[checkName]) {
434
- grouped[checkName] = [];
435
- }
436
- grouped[checkName].push(issue);
260
+ function mapGitHubEventToTrigger(eventName, action) {
261
+ if (!eventName)
262
+ return 'pr_updated';
263
+ switch (eventName) {
264
+ case 'pull_request':
265
+ if (action === 'opened')
266
+ return 'pr_opened';
267
+ if (action === 'synchronize' || action === 'edited')
268
+ return 'pr_updated';
269
+ return 'pr_updated';
270
+ case 'issues':
271
+ if (action === 'opened')
272
+ return 'issue_opened';
273
+ return 'issue_opened';
274
+ case 'issue_comment':
275
+ return 'issue_comment';
276
+ default:
277
+ return 'pr_updated';
437
278
  }
438
- return grouped;
439
- }
440
- function formatDebugInfo(debug) {
441
- let content = '';
442
- if (debug.provider)
443
- content += `**Provider:** ${debug.provider}\n`;
444
- if (debug.model)
445
- content += `**Model:** ${debug.model}\n`;
446
- if (debug.processingTime)
447
- content += `**Processing Time:** ${debug.processingTime}ms\n`;
448
- if (debug.parallelExecution !== undefined)
449
- content += `**Parallel Execution:** ${debug.parallelExecution ? '✅' : '❌'}\n`;
450
- if (debug.checksExecuted)
451
- content += `**Checks Executed:** ${debug.checksExecuted.join(', ')}\n`;
452
- content += '\n';
453
- return content;
454
279
  }
455
280
  /**
456
- * Handle legacy GitHub Action mode (backward compatibility)
281
+ * Handle events based on config
457
282
  */
458
- async function handleLegacyMode(octokit, inputs, eventName, autoReview) {
283
+ async function handleEvent(octokit, inputs, eventName, context, config) {
459
284
  const owner = inputs.owner;
460
285
  const repo = inputs.repo;
461
286
  if (!owner || !repo) {
462
287
  throw new Error('Owner and repo are required');
463
288
  }
464
289
  console.log(`Event: ${eventName}, Owner: ${owner}, Repo: ${repo}`);
290
+ // Map GitHub event to our event trigger format
291
+ const eventType = mapGitHubEventToTrigger(eventName, context.event?.action);
292
+ // Find checks that should run for this event
293
+ const checksToRun = [];
294
+ for (const [checkName, checkConfig] of Object.entries(config.checks || {})) {
295
+ // Check if this check should run for this event
296
+ const checkEvents = checkConfig.on || ['pr_opened', 'pr_updated'];
297
+ if (checkEvents.includes(eventType)) {
298
+ checksToRun.push(checkName);
299
+ }
300
+ }
301
+ if (checksToRun.length === 0) {
302
+ console.log(`ℹ️ No checks configured to run for event: ${eventType}`);
303
+ return;
304
+ }
305
+ console.log(`🔧 Checks to run for ${eventType}: ${checksToRun.join(', ')}`);
465
306
  // Handle different GitHub events
466
307
  switch (eventName) {
467
308
  case 'issue_comment':
468
309
  await handleIssueComment(octokit, owner, repo);
469
310
  break;
470
311
  case 'pull_request':
471
- if (autoReview) {
472
- await handlePullRequestEvent(octokit, owner, repo, inputs);
473
- }
312
+ // Run the checks that are configured for this event
313
+ await handlePullRequestWithConfig(octokit, owner, repo, inputs, config, checksToRun);
314
+ break;
315
+ case 'push':
316
+ // Could handle push events that are associated with PRs
317
+ console.log('Push event detected - checking for associated PR');
474
318
  break;
475
319
  default:
476
- // Fallback to original repo info functionality
320
+ // Fallback to repo info for unknown events
321
+ console.log(`Unknown event: ${eventName}`);
477
322
  await handleRepoInfo(octokit, owner, repo);
478
323
  break;
479
324
  }
480
325
  }
326
+ /**
327
+ * Recursively resolve dependencies for a set of check IDs
328
+ */
329
+ function resolveDependencies(checkIds, config, resolved = new Set(), visiting = new Set()) {
330
+ const result = [];
331
+ for (const checkId of checkIds) {
332
+ if (resolved.has(checkId)) {
333
+ continue;
334
+ }
335
+ if (visiting.has(checkId)) {
336
+ console.warn(`Circular dependency detected involving check: ${checkId}`);
337
+ continue;
338
+ }
339
+ visiting.add(checkId);
340
+ // Get dependencies for this check
341
+ const checkConfig = config?.checks?.[checkId];
342
+ const dependencies = checkConfig?.depends_on || [];
343
+ // Recursively resolve dependencies first
344
+ if (dependencies.length > 0) {
345
+ const resolvedDeps = resolveDependencies(dependencies, config, resolved, visiting);
346
+ result.push(...resolvedDeps.filter(dep => !result.includes(dep)));
347
+ }
348
+ // Add the current check if not already added
349
+ if (!result.includes(checkId)) {
350
+ result.push(checkId);
351
+ }
352
+ resolved.add(checkId);
353
+ visiting.delete(checkId);
354
+ }
355
+ return result;
356
+ }
481
357
  async function handleIssueComment(octokit, owner, repo) {
482
358
  const context = JSON.parse(process.env.GITHUB_CONTEXT || '{}');
483
359
  const comment = context.event?.comment;
@@ -486,17 +362,21 @@ async function handleIssueComment(octokit, owner, repo) {
486
362
  console.log('No comment or issue found in context');
487
363
  return;
488
364
  }
489
- // Only process PR comments (issues with pull_request key are PRs)
490
- if (!issue.pull_request) {
491
- console.log('Comment is not on a pull request');
365
+ // Prevent recursion: skip if comment is from visor itself
366
+ if (comment.body &&
367
+ (comment.body.includes('<!-- visor-comment-id:') ||
368
+ comment.body.includes('*Powered by [Visor]'))) {
369
+ console.log('Skipping visor comment to prevent recursion');
492
370
  return;
493
371
  }
372
+ // Process comments on both issues and PRs
373
+ // (issue.pull_request exists for PR comments, doesn't exist for issue comments)
494
374
  // Load configuration to get available commands
495
375
  const configManager = new config_1.ConfigManager();
496
376
  let config;
497
377
  const commandRegistry = {};
498
378
  try {
499
- config = await configManager.loadConfig('.visor.yaml');
379
+ config = await configManager.findAndLoadConfig();
500
380
  // Build command registry from config
501
381
  if (config.checks) {
502
382
  // Add 'review' command that runs all checks
@@ -517,7 +397,7 @@ async function handleIssueComment(octokit, owner, repo) {
517
397
  }
518
398
  catch {
519
399
  console.log('Could not load config, using defaults');
520
- config = null;
400
+ config = undefined;
521
401
  // Default commands when no config is available
522
402
  commandRegistry['review'] = ['security', 'performance', 'style', 'architecture'];
523
403
  }
@@ -534,7 +414,7 @@ async function handleIssueComment(octokit, owner, repo) {
534
414
  const reviewer = new reviewer_1.PRReviewer(octokit);
535
415
  switch (command.type) {
536
416
  case 'status':
537
- const statusPrInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
417
+ const statusPrInfo = await analyzer.fetchPRDiff(owner, repo, prNumber, undefined, 'issue_comment');
538
418
  const statusComment = `## 📊 PR Status\n\n` +
539
419
  `**Title:** ${statusPrInfo.title}\n` +
540
420
  `**Author:** ${statusPrInfo.author}\n` +
@@ -562,9 +442,11 @@ async function handleIssueComment(octokit, owner, repo) {
562
442
  default:
563
443
  // Handle custom commands from config
564
444
  if (commandRegistry[command.type]) {
565
- const checkIds = commandRegistry[command.type];
566
- console.log(`Running checks for command /${command.type}: ${checkIds.join(', ')}`);
567
- const prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
445
+ const initialCheckIds = commandRegistry[command.type];
446
+ // Resolve all dependencies recursively
447
+ const checkIds = resolveDependencies(initialCheckIds, config);
448
+ console.log(`Running checks for command /${command.type} (initial: ${initialCheckIds.join(', ')}, resolved: ${checkIds.join(', ')})`);
449
+ const prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber, undefined, 'issue_comment');
568
450
  // Extract common arguments
569
451
  const focus = command.args?.find(arg => arg.startsWith('--focus='))?.split('=')[1];
570
452
  const format = command.args?.find(arg => arg.startsWith('--format='))?.split('=')[1];
@@ -576,23 +458,25 @@ async function handleIssueComment(octokit, owner, repo) {
576
458
  }
577
459
  }
578
460
  }
579
- const review = await reviewer.reviewPR(owner, repo, prNumber, prInfo, {
461
+ const groupedResults = await reviewer.reviewPR(owner, repo, prNumber, prInfo, {
580
462
  focus,
581
463
  format,
582
464
  config: config,
583
465
  checks: checkIds,
584
466
  parallelExecution: false,
585
467
  });
586
- await reviewer.postReviewComment(owner, repo, prNumber, review, {
468
+ await reviewer.postReviewComment(owner, repo, prNumber, groupedResults, {
587
469
  focus,
588
470
  format,
589
471
  });
590
- (0, core_1.setOutput)('issues-found', (0, reviewer_1.calculateTotalIssues)(review.issues).toString());
472
+ // Calculate total check results from grouped results
473
+ const totalChecks = Object.values(groupedResults).flat().length;
474
+ (0, core_1.setOutput)('checks-executed', totalChecks.toString());
591
475
  }
592
476
  break;
593
477
  }
594
478
  }
595
- async function handlePullRequestEvent(octokit, owner, repo, inputs) {
479
+ async function handlePullRequestWithConfig(octokit, owner, repo, inputs, config, checksToRun) {
596
480
  const context = JSON.parse(process.env.GITHUB_CONTEXT || '{}');
597
481
  const pullRequest = context.event?.pull_request;
598
482
  const action = context.event?.action;
@@ -600,150 +484,62 @@ async function handlePullRequestEvent(octokit, owner, repo, inputs) {
600
484
  console.log('No pull request found in context');
601
485
  return;
602
486
  }
603
- // Handle multiple PR actions: opened, synchronize, edited
604
- const supportedActions = ['opened', 'synchronize', 'edited'];
605
- if (!supportedActions.includes(action)) {
606
- console.log(`Unsupported PR action: ${action}. Supported actions: ${supportedActions.join(', ')}`);
607
- return;
608
- }
609
- console.log(`Auto-reviewing PR #${pullRequest.number} (action: ${action})`);
487
+ console.log(`Reviewing PR #${pullRequest.number} with checks: ${checksToRun.join(', ')}`);
610
488
  const prNumber = pullRequest.number;
611
489
  const analyzer = new pr_analyzer_1.PRAnalyzer(octokit);
612
490
  const reviewer = new reviewer_1.PRReviewer(octokit);
613
- const commentManager = new github_comments_1.CommentManager(octokit);
614
- // Generate comment ID for this PR to enable smart updating
491
+ // Generate comment ID for this PR
615
492
  const commentId = `pr-review-${prNumber}`;
616
- let prInfo;
617
- let reviewContext = '';
618
- // For synchronize (new commits), get the latest commit SHA for incremental analysis
619
- if (action === 'synchronize') {
620
- const latestCommitSha = pullRequest.head?.sha;
621
- if (latestCommitSha) {
622
- console.log(`Analyzing incremental changes from commit: ${latestCommitSha}`);
623
- prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber, latestCommitSha);
624
- reviewContext =
625
- '## 🔄 Updated PR Analysis\n\nThis review has been updated to include the latest changes.\n\n';
626
- }
627
- else {
628
- // Fallback to full analysis if no commit SHA available
629
- prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
630
- reviewContext = '## 🔄 Updated PR Analysis\n\nAnalyzing all changes in this PR.\n\n';
631
- }
632
- }
633
- else {
634
- // For opened and edited events, do full PR analysis
635
- prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
636
- if (action === 'opened') {
637
- reviewContext =
638
- '## 🚀 Welcome to Automated PR Review!\n\nThis PR has been automatically analyzed. Use `/help` to see available commands.\n\n';
639
- }
640
- else {
641
- reviewContext =
642
- '## ✏️ PR Analysis Updated\n\nThis review has been updated based on PR changes.\n\n';
643
- }
644
- }
645
- // Load config for the review
646
- const configManager = new config_1.ConfigManager();
647
- let config;
648
- try {
649
- config = await configManager.loadConfig('.visor.yaml');
493
+ // Map the action to event type
494
+ const eventType = mapGitHubEventToTrigger('pull_request', action);
495
+ // Fetch PR diff
496
+ const prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber, undefined, eventType);
497
+ if (prInfo.files.length === 0) {
498
+ console.log('⚠️ No files changed in this PR - skipping review');
499
+ (0, core_1.setOutput)('review-completed', 'true');
500
+ (0, core_1.setOutput)('issues-found', '0');
501
+ return;
650
502
  }
651
- catch {
652
- // Fall back to a basic configuration for PR auto-review
653
- config = {
654
- version: '1.0',
655
- output: {
656
- pr_comment: {
657
- format: 'markdown',
658
- group_by: 'check',
659
- collapse: false,
660
- },
661
- },
662
- checks: {
663
- 'auto-review': {
664
- type: 'ai',
665
- on: ['pr_opened', 'pr_updated'],
666
- prompt: `Review this pull request comprehensively. Look for security issues, performance problems, code quality, bugs, and suggest improvements. Action: ${action}`,
667
- },
668
- },
669
- };
503
+ // Filter checks based on conditions
504
+ const checksToExecute = await filterChecksToExecute(checksToRun, config, prInfo);
505
+ if (checksToExecute.length === 0) {
506
+ console.log('⚠️ No checks meet execution conditions');
507
+ (0, core_1.setOutput)('review-completed', 'true');
508
+ (0, core_1.setOutput)('issues-found', '0');
509
+ return;
670
510
  }
671
- // Create review options, including debug if enabled
511
+ console.log(`📋 Executing checks: ${checksToExecute.join(', ')}`);
512
+ // Create review options
672
513
  const reviewOptions = {
673
514
  debug: inputs?.debug === 'true',
674
515
  config: config,
675
- checks: ['auto-review'],
676
- parallelExecution: false,
516
+ checks: checksToExecute,
517
+ parallelExecution: true,
677
518
  };
678
- // Create GitHub check runs for legacy auto-review
519
+ // Create GitHub check runs if enabled
679
520
  let checkResults = null;
680
- if (inputs && inputs['github-token']) {
681
- checkResults = await createGitHubChecks(octokit, inputs, owner, repo, pullRequest.head?.sha || 'unknown', ['auto-review'], config);
682
- }
683
- // Update checks to in-progress status
684
- if (checkResults) {
685
- await updateChecksInProgress(octokit, owner, repo, checkResults.checkRunMap);
686
- }
687
- // Perform the review with debug options
688
- const review = await reviewer.reviewPR(owner, repo, prNumber, prInfo, reviewOptions);
689
- // Complete GitHub check runs with results
690
- if (checkResults) {
691
- await completeGitHubChecks(octokit, owner, repo, checkResults.checkRunMap, review, config);
692
- }
693
- // If debug mode is enabled, output debug information to console
694
- if (reviewOptions.debug && review.debug) {
695
- console.log('\n========================================');
696
- console.log('🐛 DEBUG INFORMATION');
697
- console.log('========================================');
698
- console.log(`Provider: ${review.debug.provider}`);
699
- console.log(`Model: ${review.debug.model}`);
700
- console.log(`API Key Source: ${review.debug.apiKeySource}`);
701
- console.log(`Processing Time: ${review.debug.processingTime}ms`);
702
- console.log(`Prompt Length: ${review.debug.promptLength} characters`);
703
- console.log(`Response Length: ${review.debug.responseLength} characters`);
704
- console.log(`JSON Parse Success: ${review.debug.jsonParseSuccess ? '✅' : '❌'}`);
705
- if (review.debug.errors && review.debug.errors.length > 0) {
706
- console.log(`\n⚠️ Errors:`);
707
- review.debug.errors.forEach(err => console.log(` - ${err}`));
708
- }
709
- console.log('\n--- AI PROMPT ---');
710
- console.log(review.debug.prompt.substring(0, 500) + '...');
711
- console.log('\n--- RAW RESPONSE ---');
712
- console.log(review.debug.rawResponse.substring(0, 500) + '...');
713
- console.log('========================================\n');
714
- }
715
- const reviewComment = await reviewer['formatReviewCommentWithVisorFormat'](review, reviewOptions);
716
- const fullComment = reviewContext + reviewComment;
717
- // Use smart comment updating - will update existing comment or create new one
718
- try {
719
- const comment = await commentManager.updateOrCreateComment(owner, repo, prNumber, fullComment, {
720
- commentId,
721
- triggeredBy: action,
722
- allowConcurrentUpdates: true, // Allow updates even if comment was modified externally
723
- commitSha: pullRequest.head?.sha,
724
- });
725
- console.log(`✅ ${action === 'opened' ? 'Created' : 'Updated'} PR review comment (ID: ${comment.id})`);
726
- }
727
- catch (error) {
728
- console.error(`❌ Failed to ${action === 'opened' ? 'create' : 'update'} PR review comment:`, error);
729
- // Fallback to creating a new comment without the smart updating
730
- await octokit.rest.issues.createComment({
731
- owner,
732
- repo,
733
- issue_number: prNumber,
734
- body: fullComment,
735
- });
736
- console.log('✅ Created fallback PR review comment');
737
- }
521
+ if (inputs && inputs['create-check'] !== 'false') {
522
+ checkResults = await createGitHubChecks(octokit, inputs, owner, repo, pullRequest.head?.sha || 'unknown', checksToExecute, config);
523
+ if (checkResults?.checkRunMap) {
524
+ await updateChecksInProgress(octokit, owner, repo, checkResults.checkRunMap);
525
+ }
526
+ }
527
+ // Perform the review
528
+ const groupedResults = await reviewer.reviewPR(owner, repo, prNumber, prInfo, reviewOptions);
529
+ // Complete GitHub check runs
530
+ if (checkResults?.checkRunMap) {
531
+ await completeGitHubChecks(octokit, owner, repo, checkResults.checkRunMap, groupedResults, config);
532
+ }
533
+ // Post review comment
534
+ await reviewer.postReviewComment(owner, repo, prNumber, groupedResults, {
535
+ commentId,
536
+ triggeredBy: action,
537
+ commitSha: pullRequest.head?.sha,
538
+ });
738
539
  // Set outputs
739
- (0, core_1.setOutput)('auto-review-completed', 'true');
740
- (0, core_1.setOutput)('issues-found', (0, reviewer_1.calculateTotalIssues)(review.issues).toString());
540
+ (0, core_1.setOutput)('review-completed', 'true');
541
+ (0, core_1.setOutput)('checks-executed', checksToExecute.length.toString());
741
542
  (0, core_1.setOutput)('pr-action', action);
742
- (0, core_1.setOutput)('incremental-analysis', action === 'synchronize' ? 'true' : 'false');
743
- // Set GitHub check run outputs
744
- (0, core_1.setOutput)('checks-api-available', checkResults?.checksApiAvailable.toString() || 'false');
745
- (0, core_1.setOutput)('check-runs-created', checkResults?.checkRunsCreated.toString() || '0');
746
- (0, core_1.setOutput)('check-runs-urls', checkResults?.checkRunUrls.join(',') || '');
747
543
  }
748
544
  async function handleRepoInfo(octokit, owner, repo) {
749
545
  const { data: repoData } = await octokit.rest.repos.get({
@@ -757,6 +553,57 @@ async function handleRepoInfo(octokit, owner, repo) {
757
553
  console.log(`Description: ${repoData.description || 'No description'}`);
758
554
  console.log(`Stars: ${repoData.stargazers_count}`);
759
555
  }
556
+ /**
557
+ * Filter checks based on their if conditions and API requirements
558
+ */
559
+ async function filterChecksToExecute(checksToRun, config, prInfo) {
560
+ const filteredChecks = [];
561
+ // Create a basic context for condition evaluation
562
+ const context = {
563
+ files: prInfo?.files || [],
564
+ filesChanged: prInfo?.files.map(f => f.filename) || [],
565
+ event: 'pull_request',
566
+ environment: process.env,
567
+ metadata: {
568
+ filesCount: prInfo?.files.length || 0,
569
+ additions: prInfo?.totalAdditions || 0,
570
+ deletions: prInfo?.totalDeletions || 0,
571
+ },
572
+ };
573
+ for (const checkName of checksToRun) {
574
+ const checkConfig = config?.checks?.[checkName];
575
+ if (!checkConfig) {
576
+ // If no config, include the check by default
577
+ filteredChecks.push(checkName);
578
+ continue;
579
+ }
580
+ // Check if the check has an if condition
581
+ if (checkConfig.if) {
582
+ try {
583
+ // Import the failure condition evaluator
584
+ const { FailureConditionEvaluator } = await Promise.resolve().then(() => __importStar(require('./failure-condition-evaluator')));
585
+ const evaluator = new FailureConditionEvaluator();
586
+ const shouldRun = await evaluator.evaluateIfCondition(checkName, checkConfig.if, context);
587
+ if (shouldRun) {
588
+ filteredChecks.push(checkName);
589
+ }
590
+ else {
591
+ console.log(`⚠️ Skipping check '${checkName}' - if condition not met`);
592
+ }
593
+ }
594
+ catch (error) {
595
+ console.warn(`Warning: Could not evaluate if condition for ${checkName}:`, error);
596
+ // Include the check if we can't evaluate the condition
597
+ filteredChecks.push(checkName);
598
+ }
599
+ }
600
+ else {
601
+ // No condition, include the check
602
+ filteredChecks.push(checkName);
603
+ }
604
+ }
605
+ return filteredChecks;
606
+ }
760
607
  /**
761
608
  * Create GitHub check runs for individual checks if enabled
762
609
  */
@@ -887,7 +734,7 @@ async function updateChecksInProgress(octokit, owner, repo, checkRunMap) {
887
734
  /**
888
735
  * Complete GitHub check runs with results
889
736
  */
890
- async function completeGitHubChecks(octokit, owner, repo, checkRunMap, reviewSummary, config) {
737
+ async function completeGitHubChecks(octokit, owner, repo, checkRunMap, groupedResults, config) {
891
738
  if (!checkRunMap)
892
739
  return;
893
740
  const checkService = new github_check_service_1.GitHubCheckService(octokit);
@@ -895,40 +742,113 @@ async function completeGitHubChecks(octokit, owner, repo, checkRunMap, reviewSum
895
742
  console.log(`🏁 Completing ${checkRunMap.size} GitHub check runs...`);
896
743
  if (perCheckMode && !checkRunMap.has('combined')) {
897
744
  // Per-check mode: complete individual check runs
898
- await completeIndividualChecks(checkService, owner, repo, checkRunMap, reviewSummary, config);
745
+ await completeIndividualChecks(checkService, owner, repo, checkRunMap, groupedResults, config);
899
746
  }
900
747
  else {
901
748
  // Combined mode: complete single check run with all results
902
- await completeCombinedCheck(checkService, owner, repo, checkRunMap, reviewSummary, config);
749
+ await completeCombinedCheck(checkService, owner, repo, checkRunMap, groupedResults, config);
750
+ }
751
+ }
752
+ /**
753
+ * Extract ReviewIssue[] from GroupedCheckResults content by parsing the rendered text
754
+ * This function parses the structured content created by CheckExecutionEngine.convertReviewSummaryToGroupedResults()
755
+ */
756
+ function extractIssuesFromGroupedResults(groupedResults) {
757
+ const issues = [];
758
+ for (const [groupName, checkResults] of Object.entries(groupedResults)) {
759
+ for (const checkResult of checkResults) {
760
+ const { checkName, content } = checkResult;
761
+ // First, check if structured issues are available
762
+ if (checkResult.issues && checkResult.issues.length > 0) {
763
+ // Use structured issues directly - they're already properly formatted
764
+ issues.push(...checkResult.issues);
765
+ continue;
766
+ }
767
+ // Fall back to parsing issues from content (legacy support)
768
+ // Parse issues from content - look for lines like:
769
+ // - **CRITICAL**: message (file:line)
770
+ // - **ERROR**: message (file:line)
771
+ // - **WARNING**: message (file:line)
772
+ // - **INFO**: message (file:line)
773
+ const issueRegex = /^- \*\*([A-Z]+)\*\*: (.+?) \(([^:]+):(\d+)\)$/gm;
774
+ let match;
775
+ while ((match = issueRegex.exec(content)) !== null) {
776
+ const [, severityUpper, message, file, lineStr] = match;
777
+ const severity = severityUpper.toLowerCase();
778
+ const line = parseInt(lineStr, 10);
779
+ // Create ReviewIssue with proper format for GitHub annotations
780
+ const issue = {
781
+ file,
782
+ line,
783
+ ruleId: `${checkName}/content-parsed`,
784
+ message: message.trim(),
785
+ severity,
786
+ category: 'logic', // Default category since we can't parse this from content
787
+ group: groupName,
788
+ timestamp: Date.now(),
789
+ };
790
+ issues.push(issue);
791
+ }
792
+ }
903
793
  }
794
+ return issues;
904
795
  }
905
796
  /**
906
797
  * Complete individual GitHub check runs
907
798
  */
908
- async function completeIndividualChecks(checkService, owner, repo, checkRunMap, reviewSummary, config) {
909
- // Group issues by check name
799
+ async function completeIndividualChecks(checkService, owner, repo, checkRunMap, groupedResults, config) {
800
+ // Create failure condition evaluator
801
+ const { FailureConditionEvaluator } = await Promise.resolve().then(() => __importStar(require('./failure-condition-evaluator')));
802
+ const failureEvaluator = new FailureConditionEvaluator();
803
+ // Extract all issues once and group by check name for O(N) complexity
804
+ const allIssues = extractIssuesFromGroupedResults(groupedResults);
910
805
  const issuesByCheck = new Map();
911
806
  // Initialize empty arrays for all checks
912
807
  for (const checkName of checkRunMap.keys()) {
913
808
  issuesByCheck.set(checkName, []);
914
809
  }
915
- // Group issues by their check name (extracted from ruleId prefix)
916
- for (const issue of reviewSummary.issues || []) {
917
- if (issue.ruleId && issue.ruleId.includes('/')) {
918
- const checkName = issue.ruleId.split('/')[0];
919
- if (issuesByCheck.has(checkName)) {
920
- issuesByCheck.get(checkName).push(issue);
921
- }
810
+ // Group issues by check name
811
+ for (const issue of allIssues) {
812
+ const checkName = issue.ruleId?.split('/')[0];
813
+ if (checkName && issuesByCheck.has(checkName)) {
814
+ issuesByCheck.get(checkName).push(issue);
922
815
  }
923
816
  }
924
817
  for (const [checkName, checkRun] of checkRunMap) {
925
818
  try {
819
+ // Get pre-grouped issues for this check - O(1) lookup
926
820
  const checkIssues = issuesByCheck.get(checkName) || [];
927
- const checkConfig = config.checks?.[checkName];
928
- // Evaluate failure conditions for this specific check
929
- const failureResults = await evaluateCheckFailureConditions(config, checkConfig, checkName, checkIssues);
930
- await checkService.completeCheckRun(owner, repo, checkRun.id, checkName, failureResults, checkIssues);
931
- console.log(`✅ Completed ${checkName} check with ${checkIssues.length} issues`);
821
+ // Evaluate failure conditions based on fail_if configuration
822
+ const failureResults = [];
823
+ // Get global and check-specific fail_if conditions
824
+ const globalFailIf = config?.fail_if;
825
+ const checkFailIf = config?.checks?.[checkName]?.fail_if;
826
+ // Create a ReviewSummary for this check's issues
827
+ const checkReviewSummary = {
828
+ issues: checkIssues,
829
+ suggestions: [],
830
+ };
831
+ // Determine which fail_if to use: check-specific overrides global
832
+ const effectiveFailIf = checkFailIf || globalFailIf;
833
+ if (effectiveFailIf) {
834
+ const failed = await failureEvaluator.evaluateSimpleCondition(checkName, config?.checks?.[checkName]?.schema || 'plain', config?.checks?.[checkName]?.group || 'default', checkReviewSummary, effectiveFailIf);
835
+ if (failed) {
836
+ const isCheckSpecific = checkFailIf !== undefined;
837
+ failureResults.push({
838
+ conditionName: isCheckSpecific ? `${checkName}_fail_if` : 'global_fail_if',
839
+ expression: effectiveFailIf,
840
+ failed: true,
841
+ severity: 'error',
842
+ message: isCheckSpecific
843
+ ? `Check ${checkName} failure condition met`
844
+ : 'Global failure condition met',
845
+ haltExecution: false,
846
+ });
847
+ }
848
+ }
849
+ await checkService.completeCheckRun(owner, repo, checkRun.id, checkName, failureResults, checkIssues // Pass extracted issues for GitHub annotations
850
+ );
851
+ console.log(`✅ Completed ${checkName} check with ${checkIssues.length} issues, ${failureResults.length} failure conditions evaluated`);
932
852
  }
933
853
  catch (error) {
934
854
  console.error(`❌ Failed to complete ${checkName} check:`, error);
@@ -939,133 +859,46 @@ async function completeIndividualChecks(checkService, owner, repo, checkRunMap,
939
859
  /**
940
860
  * Complete combined GitHub check run
941
861
  */
942
- async function completeCombinedCheck(checkService, owner, repo, checkRunMap, reviewSummary, config) {
862
+ async function completeCombinedCheck(checkService, owner, repo, checkRunMap, groupedResults, config) {
943
863
  const combinedCheckRun = checkRunMap.get('combined');
944
864
  if (!combinedCheckRun)
945
865
  return;
866
+ // Create failure condition evaluator
867
+ const { FailureConditionEvaluator } = await Promise.resolve().then(() => __importStar(require('./failure-condition-evaluator')));
868
+ const failureEvaluator = new FailureConditionEvaluator();
946
869
  try {
947
- // Use all issues for the combined check
948
- const allIssues = reviewSummary.issues || [];
949
- // Evaluate global failure conditions
950
- const failureResults = await evaluateGlobalFailureConditions(config, allIssues);
951
- await checkService.completeCheckRun(owner, repo, combinedCheckRun.id, 'Code Review', failureResults, allIssues);
952
- console.log(`✅ Completed combined check with ${allIssues.length} issues`);
953
- }
954
- catch (error) {
955
- console.error(`❌ Failed to complete combined check:`, error);
956
- await markCheckAsFailed(checkService, owner, repo, combinedCheckRun.id, 'Code Review', error);
957
- }
958
- }
959
- /**
960
- * Evaluate failure conditions for a specific check
961
- */
962
- async function evaluateCheckFailureConditions(config, checkConfig, checkName, checkIssues) {
963
- const failureResults = [];
964
- const criticalIssues = checkIssues.filter(issue => issue.severity === 'critical').length;
965
- const errorIssues = checkIssues.filter(issue => issue.severity === 'error').length;
966
- // Check global fail_if condition
967
- if (config.fail_if) {
968
- try {
969
- const evaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
970
- const reviewSummary = {
971
- issues: [],
972
- suggestions: [],
973
- metadata: {
974
- totalIssues: checkIssues.length,
975
- criticalIssues,
976
- errorIssues,
977
- warningIssues: 0,
978
- infoIssues: 0,
979
- },
980
- };
981
- const shouldFail = await evaluator.evaluateSimpleCondition(checkName, 'legacy', 'legacy', reviewSummary, config.fail_if);
982
- if (shouldFail) {
870
+ // Extract all issues from the grouped results for the combined check
871
+ const allIssues = extractIssuesFromGroupedResults(groupedResults);
872
+ // Evaluate failure conditions for combined check
873
+ const failureResults = [];
874
+ // Create a combined ReviewSummary with all issues
875
+ const combinedReviewSummary = {
876
+ issues: allIssues,
877
+ suggestions: [],
878
+ };
879
+ // Evaluate global fail_if for the combined check
880
+ const globalFailIf = config?.fail_if;
881
+ if (globalFailIf) {
882
+ const failed = await failureEvaluator.evaluateSimpleCondition('combined', 'plain', 'combined', combinedReviewSummary, globalFailIf);
883
+ if (failed) {
983
884
  failureResults.push({
984
885
  conditionName: 'global_fail_if',
886
+ expression: globalFailIf,
985
887
  failed: true,
986
888
  severity: 'error',
987
- expression: config.fail_if,
988
889
  message: 'Global failure condition met',
989
890
  haltExecution: false,
990
891
  });
991
892
  }
992
893
  }
993
- catch (error) {
994
- console.error('❌ Failed to evaluate global fail_if condition:', config.fail_if, error);
995
- }
894
+ await checkService.completeCheckRun(owner, repo, combinedCheckRun.id, 'Code Review', failureResults, allIssues // Pass all extracted issues for GitHub annotations
895
+ );
896
+ console.log(`✅ Completed combined check with ${allIssues.length} issues, ${failureResults.length} failure conditions evaluated`);
996
897
  }
997
- // Check check-specific fail_if condition
998
- if (checkConfig?.fail_if) {
999
- try {
1000
- const evaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
1001
- const reviewSummary = {
1002
- issues: [],
1003
- suggestions: [],
1004
- metadata: {
1005
- totalIssues: checkIssues.length,
1006
- criticalIssues,
1007
- errorIssues,
1008
- warningIssues: 0,
1009
- infoIssues: 0,
1010
- },
1011
- };
1012
- const shouldFail = await evaluator.evaluateSimpleCondition(checkName, 'legacy', 'legacy', reviewSummary, checkConfig.fail_if);
1013
- if (shouldFail) {
1014
- failureResults.push({
1015
- conditionName: `${checkName}_fail_if`,
1016
- failed: true,
1017
- severity: 'error',
1018
- expression: checkConfig.fail_if,
1019
- message: `Check ${checkName} failure condition met`,
1020
- haltExecution: false,
1021
- });
1022
- }
1023
- }
1024
- catch (error) {
1025
- console.error('❌ Failed to evaluate check-specific fail_if condition:', checkConfig.fail_if, error);
1026
- }
1027
- }
1028
- return failureResults;
1029
- }
1030
- /**
1031
- * Evaluate global failure conditions for combined check
1032
- */
1033
- async function evaluateGlobalFailureConditions(config, allIssues) {
1034
- const failureResults = [];
1035
- const criticalIssues = allIssues.filter(issue => issue.severity === 'critical').length;
1036
- const errorIssues = allIssues.filter(issue => issue.severity === 'error').length;
1037
- // Check global fail_if condition
1038
- if (config.fail_if) {
1039
- try {
1040
- const evaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
1041
- const reviewSummary = {
1042
- issues: [],
1043
- suggestions: [],
1044
- metadata: {
1045
- totalIssues: allIssues.length,
1046
- criticalIssues,
1047
- errorIssues,
1048
- warningIssues: 0,
1049
- infoIssues: 0,
1050
- },
1051
- };
1052
- const shouldFail = await evaluator.evaluateSimpleCondition('combined', 'legacy', 'legacy', reviewSummary, config.fail_if);
1053
- if (shouldFail) {
1054
- failureResults.push({
1055
- conditionName: 'global_fail_if',
1056
- failed: true,
1057
- severity: 'error',
1058
- expression: config.fail_if,
1059
- message: 'Global failure condition met',
1060
- haltExecution: false,
1061
- });
1062
- }
1063
- }
1064
- catch (error) {
1065
- console.error('❌ Failed to evaluate global fail_if condition:', config.fail_if, error);
1066
- }
898
+ catch (error) {
899
+ console.error(`❌ Failed to complete combined check:`, error);
900
+ await markCheckAsFailed(checkService, owner, repo, combinedCheckRun.id, 'Code Review', error);
1067
901
  }
1068
- return failureResults;
1069
902
  }
1070
903
  /**
1071
904
  * Mark a check as failed due to execution error
@@ -1078,198 +911,6 @@ async function markCheckAsFailed(checkService, owner, repo, checkRunId, checkNam
1078
911
  console.error(`❌ Failed to mark ${checkName} check as failed:`, finalError);
1079
912
  }
1080
913
  }
1081
- /**
1082
- * Handle PR review using Visor config but with proper GitHub API PR diff analysis
1083
- */
1084
- async function handlePullRequestVisorMode(inputs, _context, octokit, _authType) {
1085
- const owner = inputs.owner || process.env.GITHUB_REPOSITORY_OWNER;
1086
- const repo = inputs.repo || process.env.GITHUB_REPOSITORY?.split('/')[1];
1087
- if (!owner || !repo) {
1088
- console.error('❌ Missing required GitHub parameters for PR analysis');
1089
- (0, core_1.setFailed)('Missing required GitHub parameters');
1090
- return;
1091
- }
1092
- // Use the provided authenticated Octokit instance
1093
- const prDetector = new pr_detector_1.PRDetector(octokit, inputs.debug === 'true');
1094
- // Convert GitHub context to our format
1095
- const eventContext = {
1096
- event_name: process.env.GITHUB_EVENT_NAME || 'unknown',
1097
- repository: {
1098
- owner: { login: owner },
1099
- name: repo,
1100
- },
1101
- event: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT).event : undefined,
1102
- payload: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT) : {},
1103
- };
1104
- // Use robust PR detection
1105
- const prResult = await prDetector.detectPRNumber(eventContext, owner, repo);
1106
- const action = eventContext.event?.action;
1107
- if (!prResult.prNumber) {
1108
- console.error(`❌ No PR found using any detection strategy: ${prResult.details || 'Unknown reason'}`);
1109
- if (inputs.debug === 'true') {
1110
- console.error('Available detection strategies:');
1111
- prDetector.getDetectionStrategies().forEach(strategy => console.error(` ${strategy}`));
1112
- }
1113
- (0, core_1.setFailed)('No PR number found');
1114
- return;
1115
- }
1116
- const prNumber = prResult.prNumber;
1117
- console.log(`✅ Found PR #${prNumber} using ${prResult.source} (confidence: ${prResult.confidence})`);
1118
- if (prResult.details) {
1119
- console.log(` Details: ${prResult.details}`);
1120
- }
1121
- console.log(`🔍 Analyzing PR #${prNumber} using Visor config (action: ${action})`);
1122
- try {
1123
- // Use the existing PR analysis infrastructure but with Visor config
1124
- const analyzer = new pr_analyzer_1.PRAnalyzer(octokit);
1125
- const reviewer = new reviewer_1.PRReviewer(octokit);
1126
- // Load Visor config
1127
- const configManager = new config_1.ConfigManager();
1128
- let config;
1129
- const configPath = inputs['config-path'];
1130
- if (configPath) {
1131
- try {
1132
- config = await configManager.loadConfig(configPath);
1133
- console.log(`📋 Loaded Visor config from: ${configPath}`);
1134
- }
1135
- catch (error) {
1136
- console.error(`⚠️ Could not load config from ${configPath}:`, error);
1137
- config = await configManager.findAndLoadConfig();
1138
- }
1139
- }
1140
- else {
1141
- // Try to find and load config from default locations (.visor.yaml)
1142
- config = await configManager.findAndLoadConfig();
1143
- const hasCustomConfig = config.checks && Object.keys(config.checks).length > 0;
1144
- if (hasCustomConfig) {
1145
- console.log(`📋 Loaded Visor config from default location (.visor.yaml)`);
1146
- }
1147
- else {
1148
- console.log(`📋 Using default Visor configuration`);
1149
- }
1150
- }
1151
- // Extract checks from config
1152
- const configChecks = Object.keys(config.checks || {});
1153
- const checksToRun = configChecks.length > 0 ? configChecks : ['security', 'performance', 'style', 'architecture'];
1154
- console.log(`🔧 Running checks: ${checksToRun.join(', ')}`);
1155
- // Fetch PR diff using GitHub API
1156
- const prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
1157
- console.log(`📄 Found ${prInfo.files.length} changed files`);
1158
- if (prInfo.files.length === 0) {
1159
- console.log('⚠️ No files changed in this PR - skipping review');
1160
- // Set basic outputs
1161
- (0, core_1.setOutput)('auto-review-completed', 'true');
1162
- (0, core_1.setOutput)('issues-found', '0');
1163
- (0, core_1.setOutput)('pr-action', action || 'unknown');
1164
- (0, core_1.setOutput)('incremental-analysis', action === 'synchronize' ? 'true' : 'false');
1165
- return;
1166
- }
1167
- // Create a custom review options with Visor config
1168
- const reviewOptions = {
1169
- debug: inputs.debug === 'true',
1170
- config: config,
1171
- checks: checksToRun,
1172
- parallelExecution: true,
1173
- };
1174
- // Fetch PR info to get commit SHA for metadata
1175
- const { data: pullRequest } = await octokit.rest.pulls.get({
1176
- owner,
1177
- repo,
1178
- pull_number: prNumber,
1179
- });
1180
- // Create GitHub check runs for each configured check
1181
- const checkResults = await createGitHubChecks(octokit, inputs, owner, repo, pullRequest.head.sha, checksToRun, config);
1182
- // Update checks to in-progress status
1183
- await updateChecksInProgress(octokit, owner, repo, checkResults.checkRunMap);
1184
- // Perform the review
1185
- console.log('🤖 Starting parallel AI review with Visor config...');
1186
- const review = await reviewer.reviewPR(owner, repo, prNumber, prInfo, reviewOptions);
1187
- // Update the review summary to show correct checks executed
1188
- if (review.debug) {
1189
- review.debug.checksExecuted = checksToRun;
1190
- review.debug.parallelExecution = true;
1191
- }
1192
- // Complete GitHub check runs with results
1193
- if (checkResults) {
1194
- await completeGitHubChecks(octokit, owner, repo, checkResults.checkRunMap, review, config);
1195
- }
1196
- // Post comment using group-based comment separation
1197
- const commentId = `visor-config-review-${prNumber}`;
1198
- await reviewer.postReviewComment(owner, repo, prNumber, review, {
1199
- ...reviewOptions,
1200
- commentId,
1201
- triggeredBy: `visor-config-${action}`,
1202
- commitSha: pullRequest.head?.sha,
1203
- });
1204
- console.log('✅ Posted Visor config-based review comment');
1205
- // Check for API errors in the review issues
1206
- const apiErrors = review.issues.filter(issue => issue.file === 'system' &&
1207
- issue.severity === 'critical' &&
1208
- (issue.message.includes('API rate limit') ||
1209
- issue.message.includes('403') ||
1210
- issue.message.includes('401') ||
1211
- issue.message.includes('authentication') ||
1212
- issue.message.includes('API key')));
1213
- if (apiErrors.length > 0) {
1214
- console.error('🚨 Critical API errors detected in review:');
1215
- apiErrors.forEach(error => {
1216
- console.error(` - ${error.message}`);
1217
- });
1218
- // Check if we should fail on API errors
1219
- const failOnApiError = inputs['fail-on-api-error'] === 'true';
1220
- if (failOnApiError) {
1221
- (0, core_1.setFailed)(`Critical API errors detected: ${apiErrors.length} authentication/rate limit issues found. Please check your API credentials.`);
1222
- return;
1223
- }
1224
- }
1225
- // Set outputs
1226
- (0, core_1.setOutput)('auto-review-completed', 'true');
1227
- (0, core_1.setOutput)('issues-found', (0, reviewer_1.calculateTotalIssues)(review.issues).toString());
1228
- (0, core_1.setOutput)('pr-action', action || 'unknown');
1229
- (0, core_1.setOutput)('incremental-analysis', action === 'synchronize' ? 'true' : 'false');
1230
- (0, core_1.setOutput)('visor-config-used', 'true');
1231
- (0, core_1.setOutput)('checks-executed', checksToRun.join(','));
1232
- (0, core_1.setOutput)('api-errors-found', apiErrors.length.toString());
1233
- // Set GitHub check run outputs
1234
- (0, core_1.setOutput)('checks-api-available', checkResults.checksApiAvailable.toString());
1235
- (0, core_1.setOutput)('check-runs-created', checkResults.checkRunsCreated.toString());
1236
- (0, core_1.setOutput)('check-runs-urls', checkResults.checkRunUrls.join(','));
1237
- }
1238
- catch (error) {
1239
- console.error('❌ Error in Visor PR analysis:', error);
1240
- (0, core_1.setFailed)(error instanceof Error ? error.message : 'Visor PR analysis failed');
1241
- }
1242
- }
1243
- /**
1244
- * Detect if we're in a PR context for any GitHub event type
1245
- */
1246
- async function detectPRContext(inputs, context, octokit) {
1247
- try {
1248
- const owner = inputs.owner || process.env.GITHUB_REPOSITORY_OWNER;
1249
- const repo = inputs.repo || process.env.GITHUB_REPOSITORY?.split('/')[1];
1250
- if (!owner || !repo) {
1251
- return false;
1252
- }
1253
- // Use the provided authenticated Octokit instance
1254
- const prDetector = new pr_detector_1.PRDetector(octokit, inputs.debug === 'true');
1255
- // Convert GitHub context to our format
1256
- const eventContext = {
1257
- event_name: context.event_name,
1258
- repository: {
1259
- owner: { login: owner },
1260
- name: repo,
1261
- },
1262
- event: context.event,
1263
- payload: context.payload || {},
1264
- };
1265
- const prResult = await prDetector.detectPRNumber(eventContext, owner, repo);
1266
- return prResult.prNumber !== null;
1267
- }
1268
- catch (error) {
1269
- console.error('Error detecting PR context:', error);
1270
- return false;
1271
- }
1272
- }
1273
914
  if (require.main === module) {
1274
915
  run();
1275
916
  }