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