@probelabs/visor 0.1.17 → 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 (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/reviewer.js CHANGED
@@ -32,27 +32,55 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
35
  Object.defineProperty(exports, "__esModule", { value: true });
39
36
  exports.PRReviewer = void 0;
37
+ exports.convertReviewSummaryToGroupedResults = convertReviewSummaryToGroupedResults;
40
38
  exports.calculateTotalIssues = calculateTotalIssues;
41
39
  exports.calculateCriticalIssues = calculateCriticalIssues;
42
40
  exports.convertIssuesToComments = convertIssuesToComments;
43
41
  const github_comments_1 = require("./github-comments");
44
42
  const ai_review_service_1 = require("./ai-review-service");
45
- const liquidjs_1 = require("liquidjs");
46
- const promises_1 = __importDefault(require("fs/promises"));
47
- const path_1 = __importDefault(require("path"));
48
- const crypto = __importStar(require("crypto"));
49
- // Helper functions for calculating metrics from issues
43
+ // Test utility function - Convert old ReviewSummary to new GroupedCheckResults format
44
+ // This is for backward compatibility with tests only
45
+ function convertReviewSummaryToGroupedResults(reviewSummary, checkName = 'test-check', groupName = 'default') {
46
+ // Create a simple content string from issues and suggestions
47
+ let content = '';
48
+ if (reviewSummary.issues && reviewSummary.issues.length > 0) {
49
+ content += `## Issues Found (${reviewSummary.issues.length})\n\n`;
50
+ reviewSummary.issues.forEach(issue => {
51
+ content += `- **${issue.severity.toUpperCase()}**: ${issue.message} (${issue.file}:${issue.line})\n`;
52
+ });
53
+ content += '\n';
54
+ }
55
+ if (reviewSummary.suggestions && reviewSummary.suggestions.length > 0) {
56
+ content += `## Suggestions\n\n`;
57
+ reviewSummary.suggestions.forEach(suggestion => {
58
+ content += `- ${suggestion}\n`;
59
+ });
60
+ }
61
+ if (!content) {
62
+ content = 'No issues found.';
63
+ }
64
+ const checkResult = {
65
+ checkName,
66
+ content: content.trim(),
67
+ group: groupName,
68
+ debug: reviewSummary.debug,
69
+ issues: reviewSummary.issues, // Include structured issues
70
+ };
71
+ const groupedResults = {};
72
+ groupedResults[groupName] = [checkResult];
73
+ return groupedResults;
74
+ }
75
+ // Helper functions for GitHub checks - ONLY for structured schemas that have issues
76
+ // These are the ONLY acceptable hardcoded schema dependencies, and only for GitHub integration
50
77
  function calculateTotalIssues(issues) {
51
- return issues.length;
78
+ return (issues || []).length;
52
79
  }
53
80
  function calculateCriticalIssues(issues) {
54
- return issues.filter(i => i.severity === 'critical').length;
81
+ return (issues || []).filter(i => i.severity === 'critical').length;
55
82
  }
83
+ // Legacy converter - ONLY for GitHub integration compatibility
56
84
  function convertIssuesToComments(issues) {
57
85
  return issues.map(issue => ({
58
86
  file: issue.file,
@@ -62,7 +90,7 @@ function convertIssuesToComments(issues) {
62
90
  category: issue.category,
63
91
  suggestion: issue.suggestion,
64
92
  replacement: issue.replacement,
65
- ruleId: issue.ruleId, // Preserve ruleId for check-based grouping
93
+ ruleId: issue.ruleId,
66
94
  }));
67
95
  }
68
96
  class PRReviewer {
@@ -76,276 +104,52 @@ class PRReviewer {
76
104
  }
77
105
  async reviewPR(owner, repo, prNumber, prInfo, options = {}) {
78
106
  const { debug = false, config, checks } = options;
79
- // If we have a config and checks, use CheckExecutionEngine
80
107
  if (config && checks && checks.length > 0) {
81
- // Import CheckExecutionEngine dynamically to avoid circular dependencies
82
108
  const { CheckExecutionEngine } = await Promise.resolve().then(() => __importStar(require('./check-execution-engine')));
83
109
  const engine = new CheckExecutionEngine();
84
- // Execute checks using the engine
85
- const reviewSummary = await engine['executeReviewChecks'](prInfo, checks, undefined, config, undefined, debug);
86
- // Return all issues - no filtering needed
87
- return reviewSummary;
110
+ const groupedResults = await engine.executeGroupedChecks(prInfo, checks, undefined, config, undefined, debug);
111
+ return groupedResults;
88
112
  }
89
- // No config provided - require configuration
90
113
  throw new Error('No configuration provided. Please create a .visor.yaml file with check definitions. ' +
91
114
  'Built-in prompts have been removed - all checks must be explicitly configured.');
92
115
  }
93
- async postReviewComment(owner, repo, prNumber, summary, options = {}) {
94
- // Group issues by their group property
95
- const issuesByGroup = this.groupIssuesByGroup(summary.issues);
96
- // If no groups or only one group, still use consistent group-based comment IDs
97
- if (Object.keys(issuesByGroup).length <= 1) {
98
- const comment = await this.formatReviewCommentWithVisorFormat(summary, options, {
116
+ async postReviewComment(owner, repo, prNumber, groupedResults, options = {}) {
117
+ // Post separate comments for each group
118
+ for (const [groupName, checkResults] of Object.entries(groupedResults)) {
119
+ const comment = await this.formatGroupComment(checkResults, options, {
99
120
  owner,
100
121
  repo,
101
122
  prNumber,
102
123
  commitSha: options.commitSha,
103
124
  });
104
- // Use consistent group-based comment ID even for single group
105
- const baseCommentId = options.commentId || 'visor-review';
106
- const groupName = Object.keys(issuesByGroup)[0] || 'default';
107
- const consistentCommentId = `${baseCommentId}-${groupName}`;
125
+ const commentId = options.commentId
126
+ ? `${options.commentId}-${groupName}`
127
+ : `visor-review-${groupName}`;
108
128
  await this.commentManager.updateOrCreateComment(owner, repo, prNumber, comment, {
109
- commentId: consistentCommentId,
110
- triggeredBy: options.triggeredBy || 'unknown',
111
- allowConcurrentUpdates: false,
112
- commitSha: options.commitSha,
113
- });
114
- return;
115
- }
116
- // Sort groups by the earliest timestamp of issues in each group
117
- // This ensures comments are posted in the order checks completed
118
- const sortedGroups = Object.entries(issuesByGroup).sort(([_aName, aIssues], [_bName, bIssues]) => {
119
- // Find the earliest timestamp in each group
120
- const aEarliest = Math.min(...aIssues.map(i => i.timestamp || Infinity));
121
- const bEarliest = Math.min(...bIssues.map(i => i.timestamp || Infinity));
122
- return aEarliest - bEarliest;
123
- });
124
- // Create separate comments for each group
125
- for (const [groupName, groupIssues] of sortedGroups) {
126
- const groupSummary = {
127
- ...summary,
128
- issues: groupIssues,
129
- };
130
- // Use group name in comment ID to create separate comments
131
- // Always include the group name suffix to ensure uniqueness
132
- const baseCommentId = options.commentId || 'visor-review';
133
- const groupCommentId = `${baseCommentId}-${groupName}`;
134
- const comment = await this.formatReviewCommentWithVisorFormat(groupSummary, options, {
135
- owner,
136
- repo,
137
- prNumber,
138
- commitSha: options.commitSha,
139
- });
140
- await this.commentManager.updateOrCreateComment(owner, repo, prNumber, comment, {
141
- commentId: groupCommentId,
129
+ commentId,
142
130
  triggeredBy: options.triggeredBy || 'unknown',
143
131
  allowConcurrentUpdates: false,
144
132
  commitSha: options.commitSha,
145
133
  });
146
134
  }
147
135
  }
148
- async formatReviewCommentWithVisorFormat(summary, _options, githubContext) {
149
- // const totalIssues = calculateTotalIssues(summary.issues); // TODO: Use this for summary stats
136
+ async formatGroupComment(checkResults, _options, _githubContext) {
150
137
  let comment = '';
151
- // Simple header - let templates handle the content logic
152
138
  comment += `## 🔍 Code Analysis Results\n\n`;
153
- // Use template system for all content generation
154
- const templateContent = await this.renderWithSchemaTemplate(summary, githubContext);
155
- comment += templateContent;
156
- // Add debug section if available
157
- if (summary.debug) {
158
- comment += '\n\n' + this.formatDebugSection(summary.debug);
139
+ // Simple concatenation of all check outputs in this group
140
+ const checkContents = checkResults
141
+ .map(result => result.content)
142
+ .filter(content => content.trim());
143
+ comment += checkContents.join('\n\n');
144
+ // Add debug info if any check has it
145
+ const debugInfo = checkResults.find(result => result.debug)?.debug;
146
+ if (debugInfo) {
147
+ comment += '\n\n' + this.formatDebugSection(debugInfo);
159
148
  comment += '\n\n';
160
149
  }
161
- // Simple footer
162
150
  comment += `\n---\n*Powered by [Visor](https://probelabs.com/visor) from [Probelabs](https://probelabs.com)*`;
163
151
  return comment;
164
152
  }
165
- async renderWithSchemaTemplate(summary, githubContext) {
166
- try {
167
- // If we have plain text suggestions (no schema/failed schema), render them directly
168
- if (summary.issues.length === 0 && summary.suggestions.length > 0) {
169
- return summary.suggestions.join('\n\n');
170
- }
171
- // If we have issues, use templates to render them
172
- if (summary.issues.length > 0) {
173
- const issuesByCheck = this.groupIssuesByCheck(summary.issues);
174
- const renderedSections = [];
175
- for (const [checkName, checkIssues] of Object.entries(issuesByCheck)) {
176
- const checkSchema = checkIssues[0]?.schema || 'code-review';
177
- const customTemplate = checkIssues[0]?.template;
178
- const renderedSection = await this.renderSingleCheckTemplate(checkName, checkIssues, checkSchema, customTemplate, githubContext);
179
- renderedSections.push(renderedSection);
180
- }
181
- return renderedSections.join('\n\n');
182
- }
183
- // No issues, no suggestions - let template handle this
184
- return await this.renderSingleCheckTemplate('review', [], 'code-review', undefined, githubContext);
185
- }
186
- catch (error) {
187
- console.warn('Failed to render with schema-template system, falling back to old system:', error);
188
- // Fallback to old system if template fails
189
- const comments = convertIssuesToComments(summary.issues);
190
- return this.formatIssuesTable(comments);
191
- }
192
- }
193
- generateGitHubDiffHash(filePath) {
194
- // GitHub uses SHA256 hash of the file path for diff anchors
195
- return crypto.createHash('sha256').update(filePath).digest('hex');
196
- }
197
- enhanceIssuesWithGitHubLinks(issues, githubContext) {
198
- if (!githubContext) {
199
- return issues;
200
- }
201
- // Use commit SHA for permalink format that auto-expands
202
- // If no commit SHA provided, fall back to PR files view
203
- const baseUrl = githubContext.commitSha
204
- ? `https://github.com/${githubContext.owner}/${githubContext.repo}/blob/${githubContext.commitSha}`
205
- : `https://github.com/${githubContext.owner}/${githubContext.repo}/pull/${githubContext.prNumber}/files`;
206
- return issues.map(issue => ({
207
- ...issue,
208
- githubUrl: githubContext.commitSha && issue.line
209
- ? `${baseUrl}/${issue.file}#L${issue.line}${issue.endLine && issue.endLine !== issue.line ? `-L${issue.endLine}` : ''}`
210
- : baseUrl,
211
- fileHash: this.generateGitHubDiffHash(issue.file),
212
- }));
213
- }
214
- async renderSingleCheckTemplate(checkName, issues, schema, customTemplate, githubContext) {
215
- const liquid = new liquidjs_1.Liquid({
216
- // Configure Liquid to handle whitespace better
217
- trimTagLeft: false, // Don't auto-trim left side of tags
218
- trimTagRight: false, // Don't auto-trim right side of tags
219
- trimOutputLeft: false, // Don't auto-trim left side of output
220
- trimOutputRight: false, // Don't auto-trim right side of output
221
- greedy: false, // Don't be greedy with whitespace trimming
222
- });
223
- // Load template content based on configuration
224
- let templateContent;
225
- if (customTemplate) {
226
- templateContent = await this.loadCustomTemplate(customTemplate);
227
- }
228
- else {
229
- // Sanitize schema name to prevent path traversal attacks
230
- const sanitizedSchema = schema.replace(/[^a-zA-Z0-9-]/g, '');
231
- if (!sanitizedSchema) {
232
- throw new Error('Invalid schema name');
233
- }
234
- // Load the appropriate template based on schema
235
- const templatePath = path_1.default.join(__dirname, `../output/${sanitizedSchema}/template.liquid`);
236
- templateContent = await promises_1.default.readFile(templatePath, 'utf-8');
237
- }
238
- // Enhance issues with GitHub links if context is available
239
- const enhancedIssues = this.enhanceIssuesWithGitHubLinks(issues, githubContext);
240
- // Pass enhanced issues with GitHub links
241
- const templateData = {
242
- issues: enhancedIssues,
243
- checkName: checkName,
244
- github: githubContext,
245
- };
246
- // Render with Liquid template and trim extra whitespace at the start/end
247
- const rendered = await liquid.parseAndRender(templateContent, templateData);
248
- return rendered.trim();
249
- }
250
- groupIssuesByCheck(issues) {
251
- const grouped = {};
252
- for (const issue of issues) {
253
- const checkName = this.extractCheckNameFromRuleId(issue.ruleId || 'uncategorized');
254
- if (!grouped[checkName]) {
255
- grouped[checkName] = [];
256
- }
257
- grouped[checkName].push(issue);
258
- }
259
- return grouped;
260
- }
261
- extractCheckNameFromRuleId(ruleId) {
262
- if (ruleId && ruleId.includes('/')) {
263
- return ruleId.split('/')[0];
264
- }
265
- return 'uncategorized';
266
- }
267
- groupIssuesByGroup(issues) {
268
- const grouped = {};
269
- for (const issue of issues) {
270
- const groupName = issue.group || 'default';
271
- if (!grouped[groupName]) {
272
- grouped[groupName] = [];
273
- }
274
- grouped[groupName].push(issue);
275
- }
276
- return grouped;
277
- }
278
- formatReviewComment(summary, options) {
279
- const { format = 'table' } = options;
280
- // Calculate metrics from issues
281
- const totalIssues = calculateTotalIssues(summary.issues);
282
- const criticalIssues = calculateCriticalIssues(summary.issues);
283
- const comments = convertIssuesToComments(summary.issues);
284
- let comment = `## 🤖 AI Code Review\n\n`;
285
- comment += `**Issues Found:** ${totalIssues} (${criticalIssues} critical)\n\n`;
286
- if (summary.suggestions.length > 0) {
287
- comment += `### 💡 Suggestions\n`;
288
- for (const suggestion of summary.suggestions) {
289
- comment += `- ${suggestion}\n`;
290
- }
291
- comment += '\n';
292
- }
293
- if (comments.length > 0) {
294
- comment += `### 🔍 Code Issues\n`;
295
- for (const reviewComment of comments) {
296
- const emoji = reviewComment.severity === 'error'
297
- ? '❌'
298
- : reviewComment.severity === 'warning'
299
- ? '⚠️'
300
- : 'ℹ️';
301
- comment += `${emoji} **${reviewComment.file}:${reviewComment.line}** (${reviewComment.category})\n`;
302
- comment += ` ${reviewComment.message}\n\n`;
303
- }
304
- }
305
- if (format === 'table' && totalIssues > 5) {
306
- comment += `*Showing top 5 issues. Use \`/review --format=markdown\` for complete analysis.*\n\n`;
307
- }
308
- // Add debug section if debug information is available
309
- if (summary.debug) {
310
- comment += '\n\n' + this.formatDebugSection(summary.debug);
311
- comment += '\n\n';
312
- }
313
- comment += `\n---\n`;
314
- comment += `*Powered by [Visor](https://probelabs.com/visor) from [Probelabs](https://probelabs.com)*`;
315
- return comment;
316
- }
317
- groupCommentsByCategory(comments) {
318
- const grouped = {
319
- security: [],
320
- performance: [],
321
- style: [],
322
- logic: [],
323
- documentation: [],
324
- };
325
- for (const comment of comments) {
326
- if (!grouped[comment.category]) {
327
- grouped[comment.category] = [];
328
- }
329
- grouped[comment.category].push(comment);
330
- }
331
- return grouped;
332
- }
333
- groupCommentsByCheck(comments) {
334
- const grouped = {};
335
- for (const comment of comments) {
336
- // Extract check name from ruleId prefix (e.g., "security/sql-injection" -> "security")
337
- let checkName = 'uncategorized';
338
- if (comment.ruleId && comment.ruleId.includes('/')) {
339
- const parts = comment.ruleId.split('/');
340
- checkName = parts[0];
341
- }
342
- if (!grouped[checkName]) {
343
- grouped[checkName] = [];
344
- }
345
- grouped[checkName].push(comment);
346
- }
347
- return grouped;
348
- }
349
153
  formatDebugSection(debug) {
350
154
  const formattedContent = [
351
155
  `**Provider:** ${debug.provider}`,
@@ -363,7 +167,6 @@ class PRReviewer {
363
167
  formattedContent.push(`- ${error}`);
364
168
  });
365
169
  }
366
- // Check if debug content would be too large for GitHub comment
367
170
  const fullDebugContent = [
368
171
  ...formattedContent,
369
172
  '',
@@ -377,9 +180,7 @@ class PRReviewer {
377
180
  debug.rawResponse,
378
181
  '```',
379
182
  ].join('\n');
380
- // GitHub comment limit is 65536 characters, leave some buffer
381
183
  if (fullDebugContent.length > 60000) {
382
- // Save debug info to artifact and provide link
383
184
  const artifactPath = this.saveDebugArtifact(debug);
384
185
  formattedContent.push('');
385
186
  formattedContent.push('### Debug Details');
@@ -387,7 +188,6 @@ class PRReviewer {
387
188
  if (artifactPath) {
388
189
  formattedContent.push(`📁 **Full debug information saved to artifact:** \`${artifactPath}\``);
389
190
  formattedContent.push('');
390
- // Try to get GitHub context for artifact link
391
191
  const runId = process.env.GITHUB_RUN_ID;
392
192
  const repoUrl = process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY
393
193
  ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}`
@@ -403,7 +203,6 @@ class PRReviewer {
403
203
  }
404
204
  }
405
205
  else {
406
- // Include full debug content if it fits
407
206
  formattedContent.push('');
408
207
  formattedContent.push('### AI Prompt');
409
208
  formattedContent.push('```');
@@ -415,356 +214,47 @@ class PRReviewer {
415
214
  formattedContent.push(debug.rawResponse);
416
215
  formattedContent.push('```');
417
216
  }
418
- return this.commentManager.createCollapsibleSection('🐛 Debug Information', formattedContent.join('\n'), false // Start collapsed
419
- );
217
+ return this.commentManager.createCollapsibleSection('🐛 Debug Information', formattedContent.join('\n'), false);
420
218
  }
421
219
  saveDebugArtifact(debug) {
422
220
  try {
423
221
  const fs = require('fs');
424
222
  const path = require('path');
425
- // Create debug directory if it doesn't exist
426
223
  const debugDir = path.join(process.cwd(), 'debug-artifacts');
427
224
  if (!fs.existsSync(debugDir)) {
428
225
  fs.mkdirSync(debugDir, { recursive: true });
429
226
  }
430
- // Create debug file with timestamp
431
227
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
432
228
  const filename = `visor-debug-${timestamp}.md`;
433
- const filePath = path.join(debugDir, filename);
434
- // Parse the combined prompts and responses to extract individual checks
435
- const markdownContent = this.formatDebugAsMarkdown(debug);
436
- fs.writeFileSync(filePath, markdownContent);
229
+ const filepath = path.join(debugDir, filename);
230
+ const content = [
231
+ `# Visor Debug Information`,
232
+ ``,
233
+ `**Timestamp:** ${debug.timestamp}`,
234
+ `**Provider:** ${debug.provider}`,
235
+ `**Model:** ${debug.model}`,
236
+ `**Processing Time:** ${debug.processingTime}ms`,
237
+ ``,
238
+ `## AI Prompt`,
239
+ ``,
240
+ '```',
241
+ debug.prompt,
242
+ '```',
243
+ ``,
244
+ `## Raw AI Response`,
245
+ ``,
246
+ '```json',
247
+ debug.rawResponse,
248
+ '```',
249
+ ].join('\n');
250
+ fs.writeFileSync(filepath, content, 'utf8');
437
251
  return filename;
438
252
  }
439
253
  catch (error) {
440
- console.error(`❌ Failed to save debug artifact: ${error}`);
254
+ console.error('Failed to save debug artifact:', error);
441
255
  return null;
442
256
  }
443
257
  }
444
- formatDebugAsMarkdown(debug) {
445
- const lines = [
446
- '# Visor AI Debug Information',
447
- '',
448
- `**Generated:** ${debug.timestamp}`,
449
- `**Provider:** ${debug.provider}`,
450
- `**Model:** ${debug.model}`,
451
- `**API Key Source:** ${debug.apiKeySource}`,
452
- `**Schema Type:** ${debug.schemaName || 'default (code-review)'}`,
453
- `**Total Processing Time:** ${debug.processingTime}ms`,
454
- `**Total Prompt Length:** ${debug.promptLength} characters`,
455
- `**Total Response Length:** ${debug.responseLength} characters`,
456
- `**JSON Parse Success:** ${debug.jsonParseSuccess ? '✅' : '❌'}`,
457
- '',
458
- ];
459
- if (debug.errors && debug.errors.length > 0) {
460
- lines.push('## ❌ Errors');
461
- debug.errors.forEach(error => {
462
- lines.push(`- ${error}`);
463
- });
464
- lines.push('');
465
- }
466
- // Add schema information if available
467
- if (debug.schema) {
468
- lines.push('## 📋 Schema Options Passed to ProbeAgent');
469
- lines.push('```json');
470
- lines.push(debug.schema);
471
- lines.push('```');
472
- lines.push('');
473
- }
474
- // Parse combined prompt and response to extract individual checks
475
- const promptSections = this.parseCheckSections(debug.prompt);
476
- const responseSections = this.parseCheckSections(debug.rawResponse);
477
- lines.push('## 📊 Check Results Summary');
478
- lines.push('');
479
- promptSections.forEach(section => {
480
- const responseSection = responseSections.find(r => r.checkName === section.checkName);
481
- lines.push(`- **${section.checkName}**: ${responseSection ? 'Success' : 'Failed'}`);
482
- });
483
- lines.push('');
484
- // Add detailed information for each check
485
- promptSections.forEach((promptSection, index) => {
486
- const responseSection = responseSections.find(r => r.checkName === promptSection.checkName);
487
- lines.push(`## ${index + 1}. ${promptSection.checkName.toUpperCase()} Check`);
488
- lines.push('');
489
- lines.push('### 📝 AI Prompt');
490
- lines.push('');
491
- lines.push('```');
492
- lines.push(promptSection.content);
493
- lines.push('```');
494
- lines.push('');
495
- lines.push('### 🤖 AI Response');
496
- lines.push('');
497
- if (responseSection) {
498
- lines.push('```json');
499
- lines.push(responseSection.content);
500
- lines.push('```');
501
- }
502
- else {
503
- lines.push('❌ No response available for this check');
504
- }
505
- lines.push('');
506
- lines.push('---');
507
- lines.push('');
508
- });
509
- // Add raw unprocessed prompt and response at the end for complete transparency
510
- lines.push('## 📄 Raw Prompt (Complete)');
511
- lines.push('');
512
- lines.push('```');
513
- lines.push(debug.prompt);
514
- lines.push('```');
515
- lines.push('');
516
- lines.push('## 📄 Raw Response (Complete)');
517
- lines.push('');
518
- lines.push('```');
519
- lines.push(debug.rawResponse);
520
- lines.push('```');
521
- lines.push('');
522
- return lines.join('\n');
523
- }
524
- parseCheckSections(combinedText) {
525
- const sections = [];
526
- // Split by check sections like [security], [performance], etc.
527
- const parts = combinedText.split(/\[(\w+)\]\s*\n/);
528
- for (let i = 1; i < parts.length; i += 2) {
529
- const checkName = parts[i];
530
- const content = parts[i + 1]?.trim() || '';
531
- if (checkName && content) {
532
- sections.push({ checkName, content });
533
- }
534
- }
535
- return sections;
536
- }
537
- formatIssuesTable(comments) {
538
- let content = `## 🔍 Code Analysis Results\n\n`;
539
- // Group comments by check (extracted from ruleId prefix)
540
- const groupedComments = this.groupCommentsByCheck(comments);
541
- // Create a table for each check that has issues
542
- for (const [checkName, checkComments] of Object.entries(groupedComments)) {
543
- if (checkComments.length === 0)
544
- continue;
545
- const checkTitle = checkName.charAt(0).toUpperCase() + checkName.slice(1);
546
- // Check heading
547
- content += `### ${checkTitle} Issues (${checkComments.length})\n\n`;
548
- // Start HTML table for this category
549
- content += `<table>\n`;
550
- content += ` <thead>\n`;
551
- content += ` <tr>\n`;
552
- content += ` <th>Severity</th>\n`;
553
- content += ` <th>File</th>\n`;
554
- content += ` <th>Line</th>\n`;
555
- content += ` <th>Issue</th>\n`;
556
- content += ` </tr>\n`;
557
- content += ` </thead>\n`;
558
- content += ` <tbody>\n`;
559
- // Sort comments within check by severity, then by file
560
- const sortedCheckComments = checkComments.sort((a, b) => {
561
- const severityOrder = { critical: 0, error: 1, warning: 2, info: 3 };
562
- const severityDiff = (severityOrder[a.severity] || 4) - (severityOrder[b.severity] || 4);
563
- if (severityDiff !== 0)
564
- return severityDiff;
565
- return a.file.localeCompare(b.file);
566
- });
567
- for (const comment of sortedCheckComments) {
568
- const severityEmoji = comment.severity === 'critical'
569
- ? '🔴'
570
- : comment.severity === 'error'
571
- ? '🔴'
572
- : comment.severity === 'warning'
573
- ? '🟡'
574
- : '🟢';
575
- const severityText = comment.severity.charAt(0).toUpperCase() + comment.severity.slice(1);
576
- // Build the issue description with suggestion/replacement if available
577
- // Wrap content in a div for better table layout control
578
- let issueContent = '';
579
- // Pass the message as-is - Liquid template will handle escaping
580
- issueContent += comment.message;
581
- if (comment.suggestion) {
582
- // Pass suggestion as-is - Liquid template will handle escaping
583
- issueContent += `\n<details><summary>💡 <strong>Suggestion</strong></summary>${comment.suggestion}</details>`;
584
- }
585
- if (comment.replacement) {
586
- // Extract language hint from file extension
587
- const fileExt = comment.file.split('.').pop()?.toLowerCase() || 'text';
588
- const languageHint = this.getLanguageHint(fileExt);
589
- // Pass replacement as-is - Liquid template will handle escaping
590
- issueContent += `\n<details><summary>🔧 <strong>Suggested Fix</strong></summary><pre><code class="language-${languageHint}">${comment.replacement}</code></pre></details>`;
591
- }
592
- // Wrap all content in a div for better table cell containment
593
- const issueDescription = `<div>${issueContent}</div>`;
594
- content += ` <tr>\n`;
595
- content += ` <td>${severityEmoji} ${severityText}</td>\n`;
596
- content += ` <td><code>${comment.file}</code></td>\n`;
597
- content += ` <td>${comment.line}</td>\n`;
598
- content += ` <td>${issueDescription}</td>\n`;
599
- content += ` </tr>\n`;
600
- }
601
- // Close HTML table for this category
602
- content += ` </tbody>\n`;
603
- content += `</table>\n\n`;
604
- // No hardcoded recommendations - all guidance comes from .visor.yaml prompts
605
- }
606
- return content;
607
- }
608
- getLanguageHint(fileExtension) {
609
- const langMap = {
610
- ts: 'typescript',
611
- tsx: 'typescript',
612
- js: 'javascript',
613
- jsx: 'javascript',
614
- py: 'python',
615
- java: 'java',
616
- kt: 'kotlin',
617
- swift: 'swift',
618
- go: 'go',
619
- rs: 'rust',
620
- cpp: 'cpp',
621
- c: 'c',
622
- cs: 'csharp',
623
- php: 'php',
624
- rb: 'ruby',
625
- scala: 'scala',
626
- sh: 'bash',
627
- bash: 'bash',
628
- zsh: 'bash',
629
- sql: 'sql',
630
- json: 'json',
631
- yaml: 'yaml',
632
- yml: 'yaml',
633
- xml: 'xml',
634
- html: 'html',
635
- css: 'css',
636
- scss: 'scss',
637
- sass: 'sass',
638
- md: 'markdown',
639
- dockerfile: 'dockerfile',
640
- tf: 'hcl',
641
- };
642
- return langMap[fileExtension] || fileExtension;
643
- }
644
- /**
645
- * Load custom template content from file or raw content
646
- */
647
- async loadCustomTemplate(config) {
648
- if (config.content) {
649
- // Auto-detect if content is actually a file path
650
- if (await this.isFilePath(config.content)) {
651
- return await this.loadTemplateFromFile(config.content);
652
- }
653
- else {
654
- // Use raw template content directly
655
- return config.content;
656
- }
657
- }
658
- if (config.file) {
659
- // Legacy explicit file property
660
- return await this.loadTemplateFromFile(config.file);
661
- }
662
- throw new Error('Custom template configuration must specify either "file" or "content"');
663
- }
664
- /**
665
- * Detect if a string is likely a file path and if the file exists
666
- */
667
- async isFilePath(str) {
668
- // Quick checks to exclude obvious non-file-path content
669
- if (!str || str.trim() !== str || str.length > 512) {
670
- return false;
671
- }
672
- // Exclude strings that are clearly content (contain common content indicators)
673
- // But be more careful with paths that might contain common words as directory names
674
- if (/\s{2,}/.test(str) || // Multiple consecutive spaces
675
- /\n/.test(str) || // Contains newlines
676
- /^(please|analyze|review|check|find|identify|look|search)/i.test(str.trim()) || // Starts with command words
677
- str.split(' ').length > 8 // Too many words for a typical file path
678
- ) {
679
- return false;
680
- }
681
- // For strings with path separators, be more lenient about common words
682
- // as they might be legitimate directory names
683
- if (!/[\/\\]/.test(str)) {
684
- // Only apply strict English word filter to non-path strings
685
- if (/\b(the|and|or|but|for|with|by|from|in|on|at|as)\b/i.test(str)) {
686
- return false;
687
- }
688
- }
689
- // Positive indicators for file paths
690
- const hasFileExtension = /\.[a-zA-Z0-9]{1,10}$/i.test(str);
691
- const hasPathSeparators = /[\/\\]/.test(str);
692
- const isRelativePath = /^\.{1,2}\//.test(str);
693
- const isAbsolutePath = path_1.default.isAbsolute(str);
694
- const hasTypicalFileChars = /^[a-zA-Z0-9._\-\/\\:~]+$/.test(str);
695
- // Must have at least one strong indicator
696
- if (!(hasFileExtension || isRelativePath || isAbsolutePath || hasPathSeparators)) {
697
- return false;
698
- }
699
- // Must contain only typical file path characters
700
- if (!hasTypicalFileChars) {
701
- return false;
702
- }
703
- // Additional validation for suspected file paths
704
- try {
705
- // Try to resolve and check if file exists
706
- let resolvedPath;
707
- if (path_1.default.isAbsolute(str)) {
708
- resolvedPath = path_1.default.normalize(str);
709
- }
710
- else {
711
- // Resolve relative to current working directory
712
- resolvedPath = path_1.default.resolve(process.cwd(), str);
713
- }
714
- // Check if file exists
715
- try {
716
- const stat = await promises_1.default.stat(resolvedPath);
717
- return stat.isFile();
718
- }
719
- catch {
720
- // File doesn't exist, but might still be a valid file path format
721
- // Return true if it has strong file path indicators
722
- return hasFileExtension && (isRelativePath || isAbsolutePath || hasPathSeparators);
723
- }
724
- }
725
- catch {
726
- return false;
727
- }
728
- }
729
- /**
730
- * Safely load template from file with security checks
731
- */
732
- async loadTemplateFromFile(templatePath) {
733
- // Resolve the path (handles both relative and absolute paths)
734
- let resolvedPath;
735
- if (path_1.default.isAbsolute(templatePath)) {
736
- // Absolute path - use as-is but validate it's not trying to escape expected directories
737
- resolvedPath = path_1.default.normalize(templatePath);
738
- }
739
- else {
740
- // Relative path - resolve relative to current working directory
741
- resolvedPath = path_1.default.resolve(process.cwd(), templatePath);
742
- }
743
- // Security: Normalize and check for path traversal attempts
744
- const normalizedPath = path_1.default.normalize(resolvedPath);
745
- // Security: For relative paths, ensure they don't escape the current directory
746
- if (!path_1.default.isAbsolute(templatePath)) {
747
- const currentDir = path_1.default.resolve(process.cwd());
748
- if (!normalizedPath.startsWith(currentDir)) {
749
- throw new Error('Invalid template file path: path traversal detected');
750
- }
751
- }
752
- // Security: Additional check for obvious path traversal patterns
753
- if (templatePath.includes('../..')) {
754
- throw new Error('Invalid template file path: path traversal detected');
755
- }
756
- // Security: Check file extension
757
- if (!normalizedPath.endsWith('.liquid')) {
758
- throw new Error('Invalid template file: must have .liquid extension');
759
- }
760
- try {
761
- const templateContent = await promises_1.default.readFile(normalizedPath, 'utf-8');
762
- return templateContent;
763
- }
764
- catch (error) {
765
- throw new Error(`Failed to load custom template from ${normalizedPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
766
- }
767
- }
768
258
  }
769
259
  exports.PRReviewer = PRReviewer;
770
260
  //# sourceMappingURL=reviewer.js.map