@probelabs/visor 0.1.10 → 0.1.18

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 (75) hide show
  1. package/README.md +78 -0
  2. package/defaults/.visor.yaml +54 -0
  3. package/dist/ai-review-service.d.ts +13 -0
  4. package/dist/ai-review-service.d.ts.map +1 -1
  5. package/dist/ai-review-service.js +142 -73
  6. package/dist/ai-review-service.js.map +1 -1
  7. package/dist/check-execution-engine.d.ts +41 -1
  8. package/dist/check-execution-engine.d.ts.map +1 -1
  9. package/dist/check-execution-engine.js +376 -19
  10. package/dist/check-execution-engine.js.map +1 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +19 -3
  13. package/dist/config.js.map +1 -1
  14. package/dist/event-mapper.d.ts.map +1 -1
  15. package/dist/event-mapper.js +3 -5
  16. package/dist/event-mapper.js.map +1 -1
  17. package/dist/failure-condition-evaluator.d.ts +11 -3
  18. package/dist/failure-condition-evaluator.d.ts.map +1 -1
  19. package/dist/failure-condition-evaluator.js +41 -5
  20. package/dist/failure-condition-evaluator.js.map +1 -1
  21. package/dist/github-check-service.d.ts.map +1 -1
  22. package/dist/github-check-service.js +26 -39
  23. package/dist/github-check-service.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +332 -681
  26. package/dist/index.js.map +1 -1
  27. package/dist/licenses.txt +2300 -0
  28. package/dist/output/code-review/schema.json +84 -0
  29. package/dist/output/code-review/template.liquid +32 -0
  30. package/dist/output/plain/schema.json +14 -0
  31. package/dist/output/plain/template.liquid +1 -0
  32. package/dist/output-formatters.d.ts.map +1 -1
  33. package/dist/output-formatters.js +14 -14
  34. package/dist/output-formatters.js.map +1 -1
  35. package/dist/pr-analyzer.d.ts +10 -1
  36. package/dist/pr-analyzer.d.ts.map +1 -1
  37. package/dist/pr-analyzer.js +2 -1
  38. package/dist/pr-analyzer.js.map +1 -1
  39. package/dist/pr-detector.d.ts +14 -4
  40. package/dist/pr-detector.d.ts.map +1 -1
  41. package/dist/pr-detector.js.map +1 -1
  42. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  43. package/dist/providers/ai-check-provider.js +27 -23
  44. package/dist/providers/ai-check-provider.js.map +1 -1
  45. package/dist/providers/check-provider-registry.js +2 -2
  46. package/dist/providers/check-provider-registry.js.map +1 -1
  47. package/dist/providers/check-provider.interface.d.ts +3 -1
  48. package/dist/providers/check-provider.interface.d.ts.map +1 -1
  49. package/dist/providers/check-provider.interface.js.map +1 -1
  50. package/dist/providers/index.d.ts +1 -1
  51. package/dist/providers/index.d.ts.map +1 -1
  52. package/dist/providers/index.js +3 -3
  53. package/dist/providers/index.js.map +1 -1
  54. package/dist/providers/noop-check-provider.d.ts +25 -0
  55. package/dist/providers/noop-check-provider.d.ts.map +1 -0
  56. package/dist/providers/noop-check-provider.js +55 -0
  57. package/dist/providers/noop-check-provider.js.map +1 -0
  58. package/dist/providers/tool-check-provider.d.ts +4 -1
  59. package/dist/providers/tool-check-provider.d.ts.map +1 -1
  60. package/dist/providers/tool-check-provider.js +64 -15
  61. package/dist/providers/tool-check-provider.js.map +1 -1
  62. package/dist/providers/webhook-check-provider.js +3 -3
  63. package/dist/providers/webhook-check-provider.js.map +1 -1
  64. package/dist/reviewer.d.ts +19 -37
  65. package/dist/reviewer.d.ts.map +1 -1
  66. package/dist/reviewer.js +85 -596
  67. package/dist/reviewer.js.map +1 -1
  68. package/dist/tiktoken_bg.wasm +0 -0
  69. package/dist/types/config.d.ts +79 -6
  70. package/dist/types/config.d.ts.map +1 -1
  71. package/package.json +3 -3
  72. package/dist/providers/script-check-provider.d.ts +0 -23
  73. package/dist/providers/script-check-provider.d.ts.map +0 -1
  74. package/dist/providers/script-check-provider.js +0 -163
  75. 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,230 +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?.event?.pull_request?.head?.sha ||
385
- process.env.GITHUB_SHA;
386
- }
387
- // Use smart comment updating with unique ID
388
- const commentId = `visor-cli-review-${prResult.prNumber}`;
389
- await commentManager.updateOrCreateComment(owner, repo, prResult.prNumber, comment, {
390
- commentId,
391
- triggeredBy: 'visor-cli',
392
- allowConcurrentUpdates: true,
393
- commitSha: latestCommitSha,
394
- });
395
- console.log(`✅ Posted CLI review comment to PR #${prResult.prNumber}`);
396
- }
397
- catch (error) {
398
- console.error('❌ Failed to post CLI review comment:', error);
399
- }
400
- }
401
- function groupIssuesByCategory(issues) {
402
- const grouped = {
403
- security: [],
404
- performance: [],
405
- style: [],
406
- logic: [],
407
- documentation: [],
408
- architecture: [],
409
- };
410
- for (const issue of issues) {
411
- const category = issue.category || 'logic';
412
- if (!grouped[category])
413
- grouped[category] = [];
414
- grouped[category].push(issue);
415
- }
416
- return grouped;
417
- }
418
- /**
419
- * Group issues by the check that found them (extracted from ruleId prefix)
420
- */
421
- function groupIssuesByCheck(issues) {
422
- const grouped = {};
423
- for (const issue of issues) {
424
- // Extract check name from ruleId prefix
425
- // Format: "checkName/specific-rule" -> "checkName"
426
- let checkName = 'uncategorized';
427
- if (issue.ruleId && issue.ruleId.includes('/')) {
428
- const parts = issue.ruleId.split('/');
429
- checkName = parts[0];
430
- }
431
- // No fallback to category - only use ruleId prefix
432
- if (!grouped[checkName]) {
433
- grouped[checkName] = [];
434
- }
435
- 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';
436
278
  }
437
- return grouped;
438
- }
439
- function formatDebugInfo(debug) {
440
- let content = '';
441
- if (debug.provider)
442
- content += `**Provider:** ${debug.provider}\n`;
443
- if (debug.model)
444
- content += `**Model:** ${debug.model}\n`;
445
- if (debug.processingTime)
446
- content += `**Processing Time:** ${debug.processingTime}ms\n`;
447
- if (debug.parallelExecution !== undefined)
448
- content += `**Parallel Execution:** ${debug.parallelExecution ? '✅' : '❌'}\n`;
449
- if (debug.checksExecuted)
450
- content += `**Checks Executed:** ${debug.checksExecuted.join(', ')}\n`;
451
- content += '\n';
452
- return content;
453
279
  }
454
280
  /**
455
- * Handle legacy GitHub Action mode (backward compatibility)
281
+ * Handle events based on config
456
282
  */
457
- async function handleLegacyMode(octokit, inputs, eventName, autoReview) {
283
+ async function handleEvent(octokit, inputs, eventName, context, config) {
458
284
  const owner = inputs.owner;
459
285
  const repo = inputs.repo;
460
286
  if (!owner || !repo) {
461
287
  throw new Error('Owner and repo are required');
462
288
  }
463
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(', ')}`);
464
306
  // Handle different GitHub events
465
307
  switch (eventName) {
466
308
  case 'issue_comment':
467
309
  await handleIssueComment(octokit, owner, repo);
468
310
  break;
469
311
  case 'pull_request':
470
- if (autoReview) {
471
- await handlePullRequestEvent(octokit, owner, repo, inputs);
472
- }
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');
473
318
  break;
474
319
  default:
475
- // Fallback to original repo info functionality
320
+ // Fallback to repo info for unknown events
321
+ console.log(`Unknown event: ${eventName}`);
476
322
  await handleRepoInfo(octokit, owner, repo);
477
323
  break;
478
324
  }
479
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
+ }
480
357
  async function handleIssueComment(octokit, owner, repo) {
481
358
  const context = JSON.parse(process.env.GITHUB_CONTEXT || '{}');
482
359
  const comment = context.event?.comment;
@@ -485,17 +362,21 @@ async function handleIssueComment(octokit, owner, repo) {
485
362
  console.log('No comment or issue found in context');
486
363
  return;
487
364
  }
488
- // Only process PR comments (issues with pull_request key are PRs)
489
- if (!issue.pull_request) {
490
- 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');
491
370
  return;
492
371
  }
372
+ // Process comments on both issues and PRs
373
+ // (issue.pull_request exists for PR comments, doesn't exist for issue comments)
493
374
  // Load configuration to get available commands
494
375
  const configManager = new config_1.ConfigManager();
495
376
  let config;
496
377
  const commandRegistry = {};
497
378
  try {
498
- config = await configManager.loadConfig('.visor.yaml');
379
+ config = await configManager.findAndLoadConfig();
499
380
  // Build command registry from config
500
381
  if (config.checks) {
501
382
  // Add 'review' command that runs all checks
@@ -516,7 +397,7 @@ async function handleIssueComment(octokit, owner, repo) {
516
397
  }
517
398
  catch {
518
399
  console.log('Could not load config, using defaults');
519
- config = null;
400
+ config = undefined;
520
401
  // Default commands when no config is available
521
402
  commandRegistry['review'] = ['security', 'performance', 'style', 'architecture'];
522
403
  }
@@ -533,7 +414,7 @@ async function handleIssueComment(octokit, owner, repo) {
533
414
  const reviewer = new reviewer_1.PRReviewer(octokit);
534
415
  switch (command.type) {
535
416
  case 'status':
536
- const statusPrInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
417
+ const statusPrInfo = await analyzer.fetchPRDiff(owner, repo, prNumber, undefined, 'issue_comment');
537
418
  const statusComment = `## 📊 PR Status\n\n` +
538
419
  `**Title:** ${statusPrInfo.title}\n` +
539
420
  `**Author:** ${statusPrInfo.author}\n` +
@@ -561,9 +442,11 @@ async function handleIssueComment(octokit, owner, repo) {
561
442
  default:
562
443
  // Handle custom commands from config
563
444
  if (commandRegistry[command.type]) {
564
- const checkIds = commandRegistry[command.type];
565
- console.log(`Running checks for command /${command.type}: ${checkIds.join(', ')}`);
566
- 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');
567
450
  // Extract common arguments
568
451
  const focus = command.args?.find(arg => arg.startsWith('--focus='))?.split('=')[1];
569
452
  const format = command.args?.find(arg => arg.startsWith('--format='))?.split('=')[1];
@@ -575,23 +458,25 @@ async function handleIssueComment(octokit, owner, repo) {
575
458
  }
576
459
  }
577
460
  }
578
- const review = await reviewer.reviewPR(owner, repo, prNumber, prInfo, {
461
+ const groupedResults = await reviewer.reviewPR(owner, repo, prNumber, prInfo, {
579
462
  focus,
580
463
  format,
581
464
  config: config,
582
465
  checks: checkIds,
583
466
  parallelExecution: false,
584
467
  });
585
- await reviewer.postReviewComment(owner, repo, prNumber, review, {
468
+ await reviewer.postReviewComment(owner, repo, prNumber, groupedResults, {
586
469
  focus,
587
470
  format,
588
471
  });
589
- (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());
590
475
  }
591
476
  break;
592
477
  }
593
478
  }
594
- async function handlePullRequestEvent(octokit, owner, repo, inputs) {
479
+ async function handlePullRequestWithConfig(octokit, owner, repo, inputs, config, checksToRun) {
595
480
  const context = JSON.parse(process.env.GITHUB_CONTEXT || '{}');
596
481
  const pullRequest = context.event?.pull_request;
597
482
  const action = context.event?.action;
@@ -599,144 +484,62 @@ async function handlePullRequestEvent(octokit, owner, repo, inputs) {
599
484
  console.log('No pull request found in context');
600
485
  return;
601
486
  }
602
- // Handle multiple PR actions: opened, synchronize, edited
603
- const supportedActions = ['opened', 'synchronize', 'edited'];
604
- if (!supportedActions.includes(action)) {
605
- console.log(`Unsupported PR action: ${action}. Supported actions: ${supportedActions.join(', ')}`);
606
- return;
607
- }
608
- console.log(`Auto-reviewing PR #${pullRequest.number} (action: ${action})`);
487
+ console.log(`Reviewing PR #${pullRequest.number} with checks: ${checksToRun.join(', ')}`);
609
488
  const prNumber = pullRequest.number;
610
489
  const analyzer = new pr_analyzer_1.PRAnalyzer(octokit);
611
490
  const reviewer = new reviewer_1.PRReviewer(octokit);
612
- const commentManager = new github_comments_1.CommentManager(octokit);
613
- // Generate comment ID for this PR to enable smart updating
491
+ // Generate comment ID for this PR
614
492
  const commentId = `pr-review-${prNumber}`;
615
- let prInfo;
616
- let reviewContext = '';
617
- // For synchronize (new commits), get the latest commit SHA for incremental analysis
618
- if (action === 'synchronize') {
619
- const latestCommitSha = pullRequest.head?.sha;
620
- if (latestCommitSha) {
621
- console.log(`Analyzing incremental changes from commit: ${latestCommitSha}`);
622
- prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber, latestCommitSha);
623
- reviewContext =
624
- '## 🔄 Updated PR Analysis\n\nThis review has been updated to include the latest changes.\n\n';
625
- }
626
- else {
627
- // Fallback to full analysis if no commit SHA available
628
- prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
629
- reviewContext = '## 🔄 Updated PR Analysis\n\nAnalyzing all changes in this PR.\n\n';
630
- }
631
- }
632
- else {
633
- // For opened and edited events, do full PR analysis
634
- prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
635
- if (action === 'opened') {
636
- reviewContext =
637
- '## 🚀 Welcome to Automated PR Review!\n\nThis PR has been automatically analyzed. Use `/help` to see available commands.\n\n';
638
- }
639
- else {
640
- reviewContext =
641
- '## ✏️ PR Analysis Updated\n\nThis review has been updated based on PR changes.\n\n';
642
- }
643
- }
644
- // Load config for the review
645
- const configManager = new config_1.ConfigManager();
646
- let config;
647
- try {
648
- 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;
649
502
  }
650
- catch {
651
- // Fall back to a basic configuration for PR auto-review
652
- config = {
653
- version: '1.0',
654
- output: {},
655
- checks: {
656
- 'auto-review': {
657
- type: 'ai',
658
- on: ['pr_opened', 'pr_updated'],
659
- prompt: `Review this pull request comprehensively. Look for security issues, performance problems, code quality, bugs, and suggest improvements. Action: ${action}`,
660
- },
661
- },
662
- };
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;
663
510
  }
664
- // Create review options, including debug if enabled
511
+ console.log(`📋 Executing checks: ${checksToExecute.join(', ')}`);
512
+ // Create review options
665
513
  const reviewOptions = {
666
514
  debug: inputs?.debug === 'true',
667
515
  config: config,
668
- checks: ['auto-review'],
669
- parallelExecution: false,
516
+ checks: checksToExecute,
517
+ parallelExecution: true,
670
518
  };
671
- // Create GitHub check runs for legacy auto-review
519
+ // Create GitHub check runs if enabled
672
520
  let checkResults = null;
673
- if (inputs && inputs['github-token']) {
674
- checkResults = await createGitHubChecks(octokit, inputs, owner, repo, pullRequest.head?.sha || 'unknown', ['auto-review'], config);
675
- }
676
- // Update checks to in-progress status
677
- if (checkResults) {
678
- await updateChecksInProgress(octokit, owner, repo, checkResults.checkRunMap);
679
- }
680
- // Perform the review with debug options
681
- const review = await reviewer.reviewPR(owner, repo, prNumber, prInfo, reviewOptions);
682
- // Complete GitHub check runs with results
683
- if (checkResults) {
684
- await completeGitHubChecks(octokit, owner, repo, checkResults.checkRunMap, review, config);
685
- }
686
- // If debug mode is enabled, output debug information to console
687
- if (reviewOptions.debug && review.debug) {
688
- console.log('\n========================================');
689
- console.log('🐛 DEBUG INFORMATION');
690
- console.log('========================================');
691
- console.log(`Provider: ${review.debug.provider}`);
692
- console.log(`Model: ${review.debug.model}`);
693
- console.log(`API Key Source: ${review.debug.apiKeySource}`);
694
- console.log(`Processing Time: ${review.debug.processingTime}ms`);
695
- console.log(`Prompt Length: ${review.debug.promptLength} characters`);
696
- console.log(`Response Length: ${review.debug.responseLength} characters`);
697
- console.log(`JSON Parse Success: ${review.debug.jsonParseSuccess ? '✅' : '❌'}`);
698
- if (review.debug.errors && review.debug.errors.length > 0) {
699
- console.log(`\n⚠️ Errors:`);
700
- review.debug.errors.forEach(err => console.log(` - ${err}`));
701
- }
702
- console.log('\n--- AI PROMPT ---');
703
- console.log(review.debug.prompt.substring(0, 500) + '...');
704
- console.log('\n--- RAW RESPONSE ---');
705
- console.log(review.debug.rawResponse.substring(0, 500) + '...');
706
- console.log('========================================\n');
707
- }
708
- const reviewComment = await reviewer['formatReviewCommentWithVisorFormat'](review, reviewOptions);
709
- const fullComment = reviewContext + reviewComment;
710
- // Use smart comment updating - will update existing comment or create new one
711
- try {
712
- const comment = await commentManager.updateOrCreateComment(owner, repo, prNumber, fullComment, {
713
- commentId,
714
- triggeredBy: action,
715
- allowConcurrentUpdates: true, // Allow updates even if comment was modified externally
716
- commitSha: pullRequest.head?.sha,
717
- });
718
- console.log(`✅ ${action === 'opened' ? 'Created' : 'Updated'} PR review comment (ID: ${comment.id})`);
719
- }
720
- catch (error) {
721
- console.error(`❌ Failed to ${action === 'opened' ? 'create' : 'update'} PR review comment:`, error);
722
- // Fallback to creating a new comment without the smart updating
723
- await octokit.rest.issues.createComment({
724
- owner,
725
- repo,
726
- issue_number: prNumber,
727
- body: fullComment,
728
- });
729
- console.log('✅ Created fallback PR review comment');
730
- }
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
+ });
731
539
  // Set outputs
732
- (0, core_1.setOutput)('auto-review-completed', 'true');
733
- (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());
734
542
  (0, core_1.setOutput)('pr-action', action);
735
- (0, core_1.setOutput)('incremental-analysis', action === 'synchronize' ? 'true' : 'false');
736
- // Set GitHub check run outputs
737
- (0, core_1.setOutput)('checks-api-available', checkResults.checksApiAvailable.toString());
738
- (0, core_1.setOutput)('check-runs-created', checkResults.checkRunsCreated.toString());
739
- (0, core_1.setOutput)('check-runs-urls', checkResults.checkRunUrls.join(','));
740
543
  }
741
544
  async function handleRepoInfo(octokit, owner, repo) {
742
545
  const { data: repoData } = await octokit.rest.repos.get({
@@ -750,6 +553,57 @@ async function handleRepoInfo(octokit, owner, repo) {
750
553
  console.log(`Description: ${repoData.description || 'No description'}`);
751
554
  console.log(`Stars: ${repoData.stargazers_count}`);
752
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
+ }
753
607
  /**
754
608
  * Create GitHub check runs for individual checks if enabled
755
609
  */
@@ -880,7 +734,7 @@ async function updateChecksInProgress(octokit, owner, repo, checkRunMap) {
880
734
  /**
881
735
  * Complete GitHub check runs with results
882
736
  */
883
- async function completeGitHubChecks(octokit, owner, repo, checkRunMap, reviewSummary, config) {
737
+ async function completeGitHubChecks(octokit, owner, repo, checkRunMap, groupedResults, config) {
884
738
  if (!checkRunMap)
885
739
  return;
886
740
  const checkService = new github_check_service_1.GitHubCheckService(octokit);
@@ -888,40 +742,113 @@ async function completeGitHubChecks(octokit, owner, repo, checkRunMap, reviewSum
888
742
  console.log(`🏁 Completing ${checkRunMap.size} GitHub check runs...`);
889
743
  if (perCheckMode && !checkRunMap.has('combined')) {
890
744
  // Per-check mode: complete individual check runs
891
- await completeIndividualChecks(checkService, owner, repo, checkRunMap, reviewSummary, config);
745
+ await completeIndividualChecks(checkService, owner, repo, checkRunMap, groupedResults, config);
892
746
  }
893
747
  else {
894
748
  // Combined mode: complete single check run with all results
895
- await completeCombinedCheck(checkService, owner, repo, checkRunMap, reviewSummary, config);
749
+ await completeCombinedCheck(checkService, owner, repo, checkRunMap, groupedResults, config);
896
750
  }
897
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
+ }
793
+ }
794
+ return issues;
795
+ }
898
796
  /**
899
797
  * Complete individual GitHub check runs
900
798
  */
901
- async function completeIndividualChecks(checkService, owner, repo, checkRunMap, reviewSummary, config) {
902
- // 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);
903
805
  const issuesByCheck = new Map();
904
806
  // Initialize empty arrays for all checks
905
807
  for (const checkName of checkRunMap.keys()) {
906
808
  issuesByCheck.set(checkName, []);
907
809
  }
908
- // Group issues by their check name (extracted from ruleId prefix)
909
- for (const issue of reviewSummary.issues || []) {
910
- if (issue.ruleId && issue.ruleId.includes('/')) {
911
- const checkName = issue.ruleId.split('/')[0];
912
- if (issuesByCheck.has(checkName)) {
913
- issuesByCheck.get(checkName).push(issue);
914
- }
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);
915
815
  }
916
816
  }
917
817
  for (const [checkName, checkRun] of checkRunMap) {
918
818
  try {
819
+ // Get pre-grouped issues for this check - O(1) lookup
919
820
  const checkIssues = issuesByCheck.get(checkName) || [];
920
- const checkConfig = config.checks?.[checkName];
921
- // Evaluate failure conditions for this specific check
922
- const failureResults = await evaluateCheckFailureConditions(config, checkConfig, checkName, checkIssues);
923
- await checkService.completeCheckRun(owner, repo, checkRun.id, checkName, failureResults, checkIssues);
924
- 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`);
925
852
  }
926
853
  catch (error) {
927
854
  console.error(`❌ Failed to complete ${checkName} check:`, error);
@@ -932,130 +859,46 @@ async function completeIndividualChecks(checkService, owner, repo, checkRunMap,
932
859
  /**
933
860
  * Complete combined GitHub check run
934
861
  */
935
- async function completeCombinedCheck(checkService, owner, repo, checkRunMap, reviewSummary, config) {
862
+ async function completeCombinedCheck(checkService, owner, repo, checkRunMap, groupedResults, config) {
936
863
  const combinedCheckRun = checkRunMap.get('combined');
937
864
  if (!combinedCheckRun)
938
865
  return;
866
+ // Create failure condition evaluator
867
+ const { FailureConditionEvaluator } = await Promise.resolve().then(() => __importStar(require('./failure-condition-evaluator')));
868
+ const failureEvaluator = new FailureConditionEvaluator();
939
869
  try {
940
- // Use all issues for the combined check
941
- const allIssues = reviewSummary.issues || [];
942
- // Evaluate global failure conditions
943
- const failureResults = await evaluateGlobalFailureConditions(config, allIssues);
944
- await checkService.completeCheckRun(owner, repo, combinedCheckRun.id, 'Code Review', failureResults, allIssues);
945
- console.log(`✅ Completed combined check with ${allIssues.length} issues`);
946
- }
947
- catch (error) {
948
- console.error(`❌ Failed to complete combined check:`, error);
949
- await markCheckAsFailed(checkService, owner, repo, combinedCheckRun.id, 'Code Review', error);
950
- }
951
- }
952
- /**
953
- * Evaluate failure conditions for a specific check
954
- */
955
- async function evaluateCheckFailureConditions(config, checkConfig, checkName, checkIssues) {
956
- const failureResults = [];
957
- const criticalIssues = checkIssues.filter(issue => issue.severity === 'critical').length;
958
- const errorIssues = checkIssues.filter(issue => issue.severity === 'error').length;
959
- // Check global fail_if condition
960
- if (config.fail_if) {
961
- try {
962
- const evaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
963
- const reviewSummary = {
964
- issues: [],
965
- suggestions: [],
966
- metadata: {
967
- totalIssues: checkIssues.length,
968
- criticalIssues,
969
- errorIssues,
970
- warningIssues: 0,
971
- infoIssues: 0,
972
- },
973
- };
974
- const shouldFail = await evaluator.evaluateSimpleCondition(checkName, 'legacy', 'legacy', reviewSummary, config.fail_if);
975
- 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) {
976
884
  failureResults.push({
977
885
  conditionName: 'global_fail_if',
886
+ expression: globalFailIf,
978
887
  failed: true,
979
888
  severity: 'error',
980
- expression: config.fail_if,
981
889
  message: 'Global failure condition met',
890
+ haltExecution: false,
982
891
  });
983
892
  }
984
893
  }
985
- catch (error) {
986
- console.error('❌ Failed to evaluate global fail_if condition:', config.fail_if, error);
987
- }
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`);
988
897
  }
989
- // Check check-specific fail_if condition
990
- if (checkConfig?.fail_if) {
991
- try {
992
- const evaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
993
- const reviewSummary = {
994
- issues: [],
995
- suggestions: [],
996
- metadata: {
997
- totalIssues: checkIssues.length,
998
- criticalIssues,
999
- errorIssues,
1000
- warningIssues: 0,
1001
- infoIssues: 0,
1002
- },
1003
- };
1004
- const shouldFail = await evaluator.evaluateSimpleCondition(checkName, 'legacy', 'legacy', reviewSummary, checkConfig.fail_if);
1005
- if (shouldFail) {
1006
- failureResults.push({
1007
- conditionName: `${checkName}_fail_if`,
1008
- failed: true,
1009
- severity: 'error',
1010
- expression: checkConfig.fail_if,
1011
- message: `Check ${checkName} failure condition met`,
1012
- });
1013
- }
1014
- }
1015
- catch (error) {
1016
- console.error('❌ Failed to evaluate check-specific fail_if condition:', checkConfig.fail_if, error);
1017
- }
1018
- }
1019
- return failureResults;
1020
- }
1021
- /**
1022
- * Evaluate global failure conditions for combined check
1023
- */
1024
- async function evaluateGlobalFailureConditions(config, allIssues) {
1025
- const failureResults = [];
1026
- const criticalIssues = allIssues.filter(issue => issue.severity === 'critical').length;
1027
- const errorIssues = allIssues.filter(issue => issue.severity === 'error').length;
1028
- // Check global fail_if condition
1029
- if (config.fail_if) {
1030
- try {
1031
- const evaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
1032
- const reviewSummary = {
1033
- issues: [],
1034
- suggestions: [],
1035
- metadata: {
1036
- totalIssues: allIssues.length,
1037
- criticalIssues,
1038
- errorIssues,
1039
- warningIssues: 0,
1040
- infoIssues: 0,
1041
- },
1042
- };
1043
- const shouldFail = await evaluator.evaluateSimpleCondition('combined', 'legacy', 'legacy', reviewSummary, config.fail_if);
1044
- if (shouldFail) {
1045
- failureResults.push({
1046
- conditionName: 'global_fail_if',
1047
- failed: true,
1048
- severity: 'error',
1049
- expression: config.fail_if,
1050
- message: 'Global failure condition met',
1051
- });
1052
- }
1053
- }
1054
- catch (error) {
1055
- console.error('❌ Failed to evaluate global fail_if condition:', config.fail_if, error);
1056
- }
898
+ catch (error) {
899
+ console.error(`❌ Failed to complete combined check:`, error);
900
+ await markCheckAsFailed(checkService, owner, repo, combinedCheckRun.id, 'Code Review', error);
1057
901
  }
1058
- return failureResults;
1059
902
  }
1060
903
  /**
1061
904
  * Mark a check as failed due to execution error
@@ -1068,198 +911,6 @@ async function markCheckAsFailed(checkService, owner, repo, checkRunId, checkNam
1068
911
  console.error(`❌ Failed to mark ${checkName} check as failed:`, finalError);
1069
912
  }
1070
913
  }
1071
- /**
1072
- * Handle PR review using Visor config but with proper GitHub API PR diff analysis
1073
- */
1074
- async function handlePullRequestVisorMode(inputs, _context, octokit, _authType) {
1075
- const owner = inputs.owner || process.env.GITHUB_REPOSITORY_OWNER;
1076
- const repo = inputs.repo || process.env.GITHUB_REPOSITORY?.split('/')[1];
1077
- if (!owner || !repo) {
1078
- console.error('❌ Missing required GitHub parameters for PR analysis');
1079
- (0, core_1.setFailed)('Missing required GitHub parameters');
1080
- return;
1081
- }
1082
- // Use the provided authenticated Octokit instance
1083
- const prDetector = new pr_detector_1.PRDetector(octokit, inputs.debug === 'true');
1084
- // Convert GitHub context to our format
1085
- const eventContext = {
1086
- event_name: process.env.GITHUB_EVENT_NAME || 'unknown',
1087
- repository: {
1088
- owner: { login: owner },
1089
- name: repo,
1090
- },
1091
- event: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT).event : undefined,
1092
- payload: process.env.GITHUB_CONTEXT ? JSON.parse(process.env.GITHUB_CONTEXT) : {},
1093
- };
1094
- // Use robust PR detection
1095
- const prResult = await prDetector.detectPRNumber(eventContext, owner, repo);
1096
- const action = eventContext.event?.action;
1097
- if (!prResult.prNumber) {
1098
- console.error(`❌ No PR found using any detection strategy: ${prResult.details || 'Unknown reason'}`);
1099
- if (inputs.debug === 'true') {
1100
- console.error('Available detection strategies:');
1101
- prDetector.getDetectionStrategies().forEach(strategy => console.error(` ${strategy}`));
1102
- }
1103
- (0, core_1.setFailed)('No PR number found');
1104
- return;
1105
- }
1106
- const prNumber = prResult.prNumber;
1107
- console.log(`✅ Found PR #${prNumber} using ${prResult.source} (confidence: ${prResult.confidence})`);
1108
- if (prResult.details) {
1109
- console.log(` Details: ${prResult.details}`);
1110
- }
1111
- console.log(`🔍 Analyzing PR #${prNumber} using Visor config (action: ${action})`);
1112
- try {
1113
- // Use the existing PR analysis infrastructure but with Visor config
1114
- const analyzer = new pr_analyzer_1.PRAnalyzer(octokit);
1115
- const reviewer = new reviewer_1.PRReviewer(octokit);
1116
- // Load Visor config
1117
- const configManager = new config_1.ConfigManager();
1118
- let config;
1119
- const configPath = inputs['config-path'];
1120
- if (configPath) {
1121
- try {
1122
- config = await configManager.loadConfig(configPath);
1123
- console.log(`📋 Loaded Visor config from: ${configPath}`);
1124
- }
1125
- catch (error) {
1126
- console.error(`⚠️ Could not load config from ${configPath}:`, error);
1127
- config = await configManager.findAndLoadConfig();
1128
- }
1129
- }
1130
- else {
1131
- // Try to find and load config from default locations (.visor.yaml)
1132
- config = await configManager.findAndLoadConfig();
1133
- const hasCustomConfig = config.checks && Object.keys(config.checks).length > 0;
1134
- if (hasCustomConfig) {
1135
- console.log(`📋 Loaded Visor config from default location (.visor.yaml)`);
1136
- }
1137
- else {
1138
- console.log(`📋 Using default Visor configuration`);
1139
- }
1140
- }
1141
- // Extract checks from config
1142
- const configChecks = Object.keys(config.checks || {});
1143
- const checksToRun = configChecks.length > 0 ? configChecks : ['security', 'performance', 'style', 'architecture'];
1144
- console.log(`🔧 Running checks: ${checksToRun.join(', ')}`);
1145
- // Fetch PR diff using GitHub API
1146
- const prInfo = await analyzer.fetchPRDiff(owner, repo, prNumber);
1147
- console.log(`📄 Found ${prInfo.files.length} changed files`);
1148
- if (prInfo.files.length === 0) {
1149
- console.log('⚠️ No files changed in this PR - skipping review');
1150
- // Set basic outputs
1151
- (0, core_1.setOutput)('auto-review-completed', 'true');
1152
- (0, core_1.setOutput)('issues-found', '0');
1153
- (0, core_1.setOutput)('pr-action', action || 'unknown');
1154
- (0, core_1.setOutput)('incremental-analysis', action === 'synchronize' ? 'true' : 'false');
1155
- return;
1156
- }
1157
- // Create a custom review options with Visor config
1158
- const reviewOptions = {
1159
- debug: inputs.debug === 'true',
1160
- config: config,
1161
- checks: checksToRun,
1162
- parallelExecution: true,
1163
- };
1164
- // Fetch PR info to get commit SHA for metadata
1165
- const { data: pullRequest } = await octokit.rest.pulls.get({
1166
- owner,
1167
- repo,
1168
- pull_number: prNumber,
1169
- });
1170
- // Create GitHub check runs for each configured check
1171
- const checkResults = await createGitHubChecks(octokit, inputs, owner, repo, pullRequest.head.sha, checksToRun, config);
1172
- // Update checks to in-progress status
1173
- await updateChecksInProgress(octokit, owner, repo, checkResults.checkRunMap);
1174
- // Perform the review
1175
- console.log('🤖 Starting parallel AI review with Visor config...');
1176
- const review = await reviewer.reviewPR(owner, repo, prNumber, prInfo, reviewOptions);
1177
- // Update the review summary to show correct checks executed
1178
- if (review.debug) {
1179
- review.debug.checksExecuted = checksToRun;
1180
- review.debug.parallelExecution = true;
1181
- }
1182
- // Complete GitHub check runs with results
1183
- if (checkResults) {
1184
- await completeGitHubChecks(octokit, owner, repo, checkResults.checkRunMap, review, config);
1185
- }
1186
- // Post comment using group-based comment separation
1187
- const commentId = `visor-config-review-${prNumber}`;
1188
- await reviewer.postReviewComment(owner, repo, prNumber, review, {
1189
- ...reviewOptions,
1190
- commentId,
1191
- triggeredBy: `visor-config-${action}`,
1192
- commitSha: pullRequest.head?.sha,
1193
- });
1194
- console.log('✅ Posted Visor config-based review comment');
1195
- // Check for API errors in the review issues
1196
- const apiErrors = review.issues.filter(issue => issue.file === 'system' &&
1197
- issue.severity === 'critical' &&
1198
- (issue.message.includes('API rate limit') ||
1199
- issue.message.includes('403') ||
1200
- issue.message.includes('401') ||
1201
- issue.message.includes('authentication') ||
1202
- issue.message.includes('API key')));
1203
- if (apiErrors.length > 0) {
1204
- console.error('🚨 Critical API errors detected in review:');
1205
- apiErrors.forEach(error => {
1206
- console.error(` - ${error.message}`);
1207
- });
1208
- // Check if we should fail on API errors
1209
- const failOnApiError = inputs['fail-on-api-error'] === 'true';
1210
- if (failOnApiError) {
1211
- (0, core_1.setFailed)(`Critical API errors detected: ${apiErrors.length} authentication/rate limit issues found. Please check your API credentials.`);
1212
- return;
1213
- }
1214
- }
1215
- // Set outputs
1216
- (0, core_1.setOutput)('auto-review-completed', 'true');
1217
- (0, core_1.setOutput)('issues-found', (0, reviewer_1.calculateTotalIssues)(review.issues).toString());
1218
- (0, core_1.setOutput)('pr-action', action || 'unknown');
1219
- (0, core_1.setOutput)('incremental-analysis', action === 'synchronize' ? 'true' : 'false');
1220
- (0, core_1.setOutput)('visor-config-used', 'true');
1221
- (0, core_1.setOutput)('checks-executed', checksToRun.join(','));
1222
- (0, core_1.setOutput)('api-errors-found', apiErrors.length.toString());
1223
- // Set GitHub check run outputs
1224
- (0, core_1.setOutput)('checks-api-available', checkResults.checksApiAvailable.toString());
1225
- (0, core_1.setOutput)('check-runs-created', checkResults.checkRunsCreated.toString());
1226
- (0, core_1.setOutput)('check-runs-urls', checkResults.checkRunUrls.join(','));
1227
- }
1228
- catch (error) {
1229
- console.error('❌ Error in Visor PR analysis:', error);
1230
- (0, core_1.setFailed)(error instanceof Error ? error.message : 'Visor PR analysis failed');
1231
- }
1232
- }
1233
- /**
1234
- * Detect if we're in a PR context for any GitHub event type
1235
- */
1236
- async function detectPRContext(inputs, context, octokit) {
1237
- try {
1238
- const owner = inputs.owner || process.env.GITHUB_REPOSITORY_OWNER;
1239
- const repo = inputs.repo || process.env.GITHUB_REPOSITORY?.split('/')[1];
1240
- if (!owner || !repo) {
1241
- return false;
1242
- }
1243
- // Use the provided authenticated Octokit instance
1244
- const prDetector = new pr_detector_1.PRDetector(octokit, inputs.debug === 'true');
1245
- // Convert GitHub context to our format
1246
- const eventContext = {
1247
- event_name: context.event_name,
1248
- repository: {
1249
- owner: { login: owner },
1250
- name: repo,
1251
- },
1252
- event: context.event,
1253
- payload: context.payload || {},
1254
- };
1255
- const prResult = await prDetector.detectPRNumber(eventContext, owner, repo);
1256
- return prResult.prNumber !== null;
1257
- }
1258
- catch (error) {
1259
- console.error('Error detecting PR context:', error);
1260
- return false;
1261
- }
1262
- }
1263
914
  if (require.main === module) {
1264
915
  run();
1265
916
  }