@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.
- package/README.md +78 -0
- package/defaults/.visor.yaml +54 -0
- package/dist/ai-review-service.d.ts +13 -0
- package/dist/ai-review-service.d.ts.map +1 -1
- package/dist/ai-review-service.js +142 -73
- package/dist/ai-review-service.js.map +1 -1
- package/dist/check-execution-engine.d.ts +41 -1
- package/dist/check-execution-engine.d.ts.map +1 -1
- package/dist/check-execution-engine.js +376 -19
- package/dist/check-execution-engine.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +19 -3
- package/dist/config.js.map +1 -1
- package/dist/event-mapper.d.ts.map +1 -1
- package/dist/event-mapper.js +3 -5
- package/dist/event-mapper.js.map +1 -1
- package/dist/failure-condition-evaluator.d.ts +11 -3
- package/dist/failure-condition-evaluator.d.ts.map +1 -1
- package/dist/failure-condition-evaluator.js +41 -5
- package/dist/failure-condition-evaluator.js.map +1 -1
- package/dist/github-check-service.d.ts.map +1 -1
- package/dist/github-check-service.js +26 -39
- package/dist/github-check-service.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +332 -681
- package/dist/index.js.map +1 -1
- package/dist/licenses.txt +2300 -0
- package/dist/output/code-review/schema.json +84 -0
- package/dist/output/code-review/template.liquid +32 -0
- package/dist/output/plain/schema.json +14 -0
- package/dist/output/plain/template.liquid +1 -0
- package/dist/output-formatters.d.ts.map +1 -1
- package/dist/output-formatters.js +14 -14
- package/dist/output-formatters.js.map +1 -1
- package/dist/pr-analyzer.d.ts +10 -1
- package/dist/pr-analyzer.d.ts.map +1 -1
- package/dist/pr-analyzer.js +2 -1
- package/dist/pr-analyzer.js.map +1 -1
- package/dist/pr-detector.d.ts +14 -4
- package/dist/pr-detector.d.ts.map +1 -1
- package/dist/pr-detector.js.map +1 -1
- package/dist/providers/ai-check-provider.d.ts.map +1 -1
- package/dist/providers/ai-check-provider.js +27 -23
- package/dist/providers/ai-check-provider.js.map +1 -1
- package/dist/providers/check-provider-registry.js +2 -2
- package/dist/providers/check-provider-registry.js.map +1 -1
- package/dist/providers/check-provider.interface.d.ts +3 -1
- package/dist/providers/check-provider.interface.d.ts.map +1 -1
- package/dist/providers/check-provider.interface.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -3
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/noop-check-provider.d.ts +25 -0
- package/dist/providers/noop-check-provider.d.ts.map +1 -0
- package/dist/providers/noop-check-provider.js +55 -0
- package/dist/providers/noop-check-provider.js.map +1 -0
- package/dist/providers/tool-check-provider.d.ts +4 -1
- package/dist/providers/tool-check-provider.d.ts.map +1 -1
- package/dist/providers/tool-check-provider.js +64 -15
- package/dist/providers/tool-check-provider.js.map +1 -1
- package/dist/providers/webhook-check-provider.js +3 -3
- package/dist/providers/webhook-check-provider.js.map +1 -1
- package/dist/reviewer.d.ts +19 -37
- package/dist/reviewer.d.ts.map +1 -1
- package/dist/reviewer.js +85 -596
- package/dist/reviewer.js.map +1 -1
- package/dist/tiktoken_bg.wasm +0 -0
- package/dist/types/config.d.ts +79 -6
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/providers/script-check-provider.d.ts +0 -23
- package/dist/providers/script-check-provider.d.ts.map +0 -1
- package/dist/providers/script-check-provider.js +0 -163
- 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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
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
|
-
|
|
85
|
-
|
|
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,
|
|
94
|
-
//
|
|
95
|
-
const
|
|
96
|
-
|
|
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
|
|
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
|
|
149
|
-
const totalIssues = calculateTotalIssues(summary.issues);
|
|
136
|
+
async formatGroupComment(checkResults, _options, _githubContext) {
|
|
150
137
|
let comment = '';
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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(
|
|
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
|