@probelabs/visor 0.1.0
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 +1240 -0
- package/action.yml +142 -0
- package/defaults/.visor.yaml +184 -0
- package/dist/action-cli-bridge.d.ts +104 -0
- package/dist/action-cli-bridge.d.ts.map +1 -0
- package/dist/action-cli-bridge.js +372 -0
- package/dist/action-cli-bridge.js.map +1 -0
- package/dist/ai-review-service.d.ts +84 -0
- package/dist/ai-review-service.d.ts.map +1 -0
- package/dist/ai-review-service.js +674 -0
- package/dist/ai-review-service.js.map +1 -0
- package/dist/check-execution-engine.d.ts +165 -0
- package/dist/check-execution-engine.d.ts.map +1 -0
- package/dist/check-execution-engine.js +1172 -0
- package/dist/check-execution-engine.js.map +1 -0
- package/dist/cli-main.d.ts +6 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +247 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/cli.d.ts +47 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +224 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +53 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +369 -0
- package/dist/config.js.map +1 -0
- package/dist/dependency-resolver.d.ts +54 -0
- package/dist/dependency-resolver.d.ts.map +1 -0
- package/dist/dependency-resolver.js +163 -0
- package/dist/dependency-resolver.js.map +1 -0
- package/dist/event-mapper.d.ts +125 -0
- package/dist/event-mapper.d.ts.map +1 -0
- package/dist/event-mapper.js +311 -0
- package/dist/event-mapper.js.map +1 -0
- package/dist/failure-condition-evaluator.d.ts +81 -0
- package/dist/failure-condition-evaluator.d.ts.map +1 -0
- package/dist/failure-condition-evaluator.js +445 -0
- package/dist/failure-condition-evaluator.js.map +1 -0
- package/dist/git-repository-analyzer.d.ts +45 -0
- package/dist/git-repository-analyzer.d.ts.map +1 -0
- package/dist/git-repository-analyzer.js +285 -0
- package/dist/git-repository-analyzer.js.map +1 -0
- package/dist/github-check-service.d.ts +104 -0
- package/dist/github-check-service.d.ts.map +1 -0
- package/dist/github-check-service.js +382 -0
- package/dist/github-check-service.js.map +1 -0
- package/dist/github-comments.d.ts +109 -0
- package/dist/github-comments.d.ts.map +1 -0
- package/dist/github-comments.js +289 -0
- package/dist/github-comments.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1265 -0
- package/dist/index.js.map +1 -0
- package/dist/output-formatters.d.ts +66 -0
- package/dist/output-formatters.d.ts.map +1 -0
- package/dist/output-formatters.js +624 -0
- package/dist/output-formatters.js.map +1 -0
- package/dist/pr-analyzer.d.ts +47 -0
- package/dist/pr-analyzer.d.ts.map +1 -0
- package/dist/pr-analyzer.js +194 -0
- package/dist/pr-analyzer.js.map +1 -0
- package/dist/pr-detector.d.ts +78 -0
- package/dist/pr-detector.d.ts.map +1 -0
- package/dist/pr-detector.js +357 -0
- package/dist/pr-detector.js.map +1 -0
- package/dist/providers/ai-check-provider.d.ts +40 -0
- package/dist/providers/ai-check-provider.d.ts.map +1 -0
- package/dist/providers/ai-check-provider.js +416 -0
- package/dist/providers/ai-check-provider.js.map +1 -0
- package/dist/providers/check-provider-registry.d.ts +67 -0
- package/dist/providers/check-provider-registry.d.ts.map +1 -0
- package/dist/providers/check-provider-registry.js +138 -0
- package/dist/providers/check-provider-registry.js.map +1 -0
- package/dist/providers/check-provider.interface.d.ts +78 -0
- package/dist/providers/check-provider.interface.d.ts.map +1 -0
- package/dist/providers/check-provider.interface.js +11 -0
- package/dist/providers/check-provider.interface.js.map +1 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +19 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/script-check-provider.d.ts +20 -0
- package/dist/providers/script-check-provider.d.ts.map +1 -0
- package/dist/providers/script-check-provider.js +163 -0
- package/dist/providers/script-check-provider.js.map +1 -0
- package/dist/providers/tool-check-provider.d.ts +19 -0
- package/dist/providers/tool-check-provider.d.ts.map +1 -0
- package/dist/providers/tool-check-provider.js +125 -0
- package/dist/providers/tool-check-provider.js.map +1 -0
- package/dist/providers/webhook-check-provider.d.ts +21 -0
- package/dist/providers/webhook-check-provider.d.ts.map +1 -0
- package/dist/providers/webhook-check-provider.js +173 -0
- package/dist/providers/webhook-check-provider.js.map +1 -0
- package/dist/reviewer.d.ts +88 -0
- package/dist/reviewer.d.ts.map +1 -0
- package/dist/reviewer.js +760 -0
- package/dist/reviewer.js.map +1 -0
- package/dist/types/cli.d.ts +41 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +3 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/config.d.ts +315 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/utils/env-resolver.d.ts +38 -0
- package/dist/utils/env-resolver.d.ts.map +1 -0
- package/dist/utils/env-resolver.js +130 -0
- package/dist/utils/env-resolver.js.map +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1,1172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CheckExecutionEngine = void 0;
|
|
4
|
+
const reviewer_1 = require("./reviewer");
|
|
5
|
+
const git_repository_analyzer_1 = require("./git-repository-analyzer");
|
|
6
|
+
const check_provider_registry_1 = require("./providers/check-provider-registry");
|
|
7
|
+
const dependency_resolver_1 = require("./dependency-resolver");
|
|
8
|
+
const failure_condition_evaluator_1 = require("./failure-condition-evaluator");
|
|
9
|
+
const github_check_service_1 = require("./github-check-service");
|
|
10
|
+
/**
|
|
11
|
+
* Filter environment variables to only include safe ones for sandbox evaluation
|
|
12
|
+
*/
|
|
13
|
+
function getSafeEnvironmentVariables() {
|
|
14
|
+
const safeEnvVars = [
|
|
15
|
+
'CI',
|
|
16
|
+
'GITHUB_EVENT_NAME',
|
|
17
|
+
'GITHUB_REPOSITORY',
|
|
18
|
+
'GITHUB_REF',
|
|
19
|
+
'GITHUB_SHA',
|
|
20
|
+
'GITHUB_HEAD_REF',
|
|
21
|
+
'GITHUB_BASE_REF',
|
|
22
|
+
'GITHUB_ACTOR',
|
|
23
|
+
'GITHUB_WORKFLOW',
|
|
24
|
+
'GITHUB_RUN_ID',
|
|
25
|
+
'GITHUB_RUN_NUMBER',
|
|
26
|
+
'NODE_ENV',
|
|
27
|
+
];
|
|
28
|
+
const safeEnv = {};
|
|
29
|
+
for (const key of safeEnvVars) {
|
|
30
|
+
if (process.env[key]) {
|
|
31
|
+
safeEnv[key] = process.env[key];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return safeEnv;
|
|
35
|
+
}
|
|
36
|
+
class CheckExecutionEngine {
|
|
37
|
+
gitAnalyzer;
|
|
38
|
+
mockOctokit;
|
|
39
|
+
reviewer;
|
|
40
|
+
providerRegistry;
|
|
41
|
+
failureEvaluator;
|
|
42
|
+
githubCheckService;
|
|
43
|
+
checkRunMap;
|
|
44
|
+
githubContext;
|
|
45
|
+
constructor(workingDirectory) {
|
|
46
|
+
this.gitAnalyzer = new git_repository_analyzer_1.GitRepositoryAnalyzer(workingDirectory);
|
|
47
|
+
this.providerRegistry = check_provider_registry_1.CheckProviderRegistry.getInstance();
|
|
48
|
+
this.failureEvaluator = new failure_condition_evaluator_1.FailureConditionEvaluator();
|
|
49
|
+
// Create a mock Octokit instance for local analysis
|
|
50
|
+
// This allows us to reuse the existing PRReviewer logic without network calls
|
|
51
|
+
this.mockOctokit = this.createMockOctokit();
|
|
52
|
+
this.reviewer = new reviewer_1.PRReviewer(this.mockOctokit);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Execute checks on the local repository
|
|
56
|
+
*/
|
|
57
|
+
async executeChecks(options) {
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
const timestamp = new Date().toISOString();
|
|
60
|
+
try {
|
|
61
|
+
// Determine where to send log messages based on output format
|
|
62
|
+
const logFn = options.outputFormat === 'json' || options.outputFormat === 'sarif'
|
|
63
|
+
? console.error
|
|
64
|
+
: console.log;
|
|
65
|
+
// Initialize GitHub checks if enabled
|
|
66
|
+
if (options.githubChecks?.enabled && options.githubChecks.octokit) {
|
|
67
|
+
await this.initializeGitHubChecks(options, logFn);
|
|
68
|
+
}
|
|
69
|
+
// Analyze the repository
|
|
70
|
+
logFn('🔍 Analyzing local git repository...');
|
|
71
|
+
const repositoryInfo = await this.gitAnalyzer.analyzeRepository();
|
|
72
|
+
if (!repositoryInfo.isGitRepository) {
|
|
73
|
+
// Complete GitHub checks with error if they were initialized
|
|
74
|
+
if (this.checkRunMap) {
|
|
75
|
+
await this.completeGitHubChecksWithError('Not a git repository or no changes found');
|
|
76
|
+
}
|
|
77
|
+
return this.createErrorResult(repositoryInfo, 'Not a git repository or no changes found', startTime, timestamp, options.checks);
|
|
78
|
+
}
|
|
79
|
+
// Convert to PRInfo format for compatibility with existing reviewer
|
|
80
|
+
const prInfo = this.gitAnalyzer.toPRInfo(repositoryInfo);
|
|
81
|
+
// Update GitHub checks to in-progress status
|
|
82
|
+
if (this.checkRunMap) {
|
|
83
|
+
await this.updateGitHubChecksInProgress(options);
|
|
84
|
+
}
|
|
85
|
+
// Execute checks using the existing PRReviewer
|
|
86
|
+
logFn(`🤖 Executing checks: ${options.checks.join(', ')}`);
|
|
87
|
+
const reviewSummary = await this.executeReviewChecks(prInfo, options.checks, options.timeout, options.config, options.outputFormat, options.debug);
|
|
88
|
+
// Complete GitHub checks with results
|
|
89
|
+
if (this.checkRunMap) {
|
|
90
|
+
await this.completeGitHubChecksWithResults(reviewSummary, options);
|
|
91
|
+
}
|
|
92
|
+
const executionTime = Date.now() - startTime;
|
|
93
|
+
// Collect debug information when debug mode is enabled
|
|
94
|
+
let debugInfo;
|
|
95
|
+
if (options.debug && reviewSummary.debug) {
|
|
96
|
+
debugInfo = {
|
|
97
|
+
provider: reviewSummary.debug.provider,
|
|
98
|
+
model: reviewSummary.debug.model,
|
|
99
|
+
processingTime: reviewSummary.debug.processingTime,
|
|
100
|
+
parallelExecution: options.checks.length > 1,
|
|
101
|
+
checksExecuted: options.checks,
|
|
102
|
+
totalApiCalls: reviewSummary.debug.totalApiCalls || options.checks.length,
|
|
103
|
+
apiCallDetails: reviewSummary.debug.apiCallDetails,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
repositoryInfo,
|
|
108
|
+
reviewSummary,
|
|
109
|
+
executionTime,
|
|
110
|
+
timestamp,
|
|
111
|
+
checksExecuted: options.checks,
|
|
112
|
+
debug: debugInfo,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error('Error executing checks:', error);
|
|
117
|
+
// Complete GitHub checks with error if they were initialized
|
|
118
|
+
if (this.checkRunMap) {
|
|
119
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
120
|
+
await this.completeGitHubChecksWithError(errorMessage);
|
|
121
|
+
}
|
|
122
|
+
const fallbackRepositoryInfo = {
|
|
123
|
+
title: 'Error during analysis',
|
|
124
|
+
body: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
125
|
+
author: 'system',
|
|
126
|
+
base: 'main',
|
|
127
|
+
head: 'HEAD',
|
|
128
|
+
files: [],
|
|
129
|
+
totalAdditions: 0,
|
|
130
|
+
totalDeletions: 0,
|
|
131
|
+
isGitRepository: false,
|
|
132
|
+
workingDirectory: options.workingDirectory || process.cwd(),
|
|
133
|
+
};
|
|
134
|
+
return this.createErrorResult(fallbackRepositoryInfo, error instanceof Error ? error.message : 'Unknown error occurred', startTime, timestamp, options.checks);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Execute review checks using parallel execution for multiple AI checks
|
|
139
|
+
*/
|
|
140
|
+
async executeReviewChecks(prInfo, checks, timeout, config, outputFormat, debug) {
|
|
141
|
+
// Determine where to send log messages based on output format
|
|
142
|
+
const logFn = outputFormat === 'json' || outputFormat === 'sarif' ? console.error : console.log;
|
|
143
|
+
logFn(`🔧 Debug: executeReviewChecks called with checks: ${JSON.stringify(checks)}`);
|
|
144
|
+
logFn(`🔧 Debug: Config available: ${!!config}, Config has checks: ${!!config?.checks}`);
|
|
145
|
+
// If we have a config with individual check definitions, use dependency-aware execution
|
|
146
|
+
// Check if any of the checks have dependencies or if there are multiple checks
|
|
147
|
+
const hasDependencies = config?.checks &&
|
|
148
|
+
checks.some(checkName => {
|
|
149
|
+
const checkConfig = config.checks[checkName];
|
|
150
|
+
return checkConfig?.depends_on && checkConfig.depends_on.length > 0;
|
|
151
|
+
});
|
|
152
|
+
if (config?.checks && (checks.length > 1 || hasDependencies)) {
|
|
153
|
+
logFn(`🔧 Debug: Using dependency-aware execution for ${checks.length} checks (has dependencies: ${hasDependencies})`);
|
|
154
|
+
return await this.executeDependencyAwareChecks(prInfo, checks, timeout, config, logFn, debug);
|
|
155
|
+
}
|
|
156
|
+
// Single check execution (existing logic)
|
|
157
|
+
if (checks.length === 1) {
|
|
158
|
+
logFn(`🔧 Debug: Using single check execution for: ${checks[0]}`);
|
|
159
|
+
// If we have a config definition for this check, use it
|
|
160
|
+
if (config?.checks?.[checks[0]]) {
|
|
161
|
+
return await this.executeSingleConfiguredCheck(prInfo, checks[0], timeout, config, logFn);
|
|
162
|
+
}
|
|
163
|
+
// Try provider system for single checks
|
|
164
|
+
if (this.providerRegistry.hasProvider(checks[0])) {
|
|
165
|
+
const provider = this.providerRegistry.getProviderOrThrow(checks[0]);
|
|
166
|
+
const providerConfig = {
|
|
167
|
+
type: checks[0],
|
|
168
|
+
prompt: 'all',
|
|
169
|
+
ai: timeout ? { timeout } : undefined,
|
|
170
|
+
};
|
|
171
|
+
const result = await provider.execute(prInfo, providerConfig);
|
|
172
|
+
// Prefix issues with check name for consistent grouping
|
|
173
|
+
const prefixedIssues = result.issues.map(issue => ({
|
|
174
|
+
...issue,
|
|
175
|
+
ruleId: `${checks[0]}/${issue.ruleId}`,
|
|
176
|
+
}));
|
|
177
|
+
return {
|
|
178
|
+
...result,
|
|
179
|
+
issues: prefixedIssues,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Check if 'ai' provider is available for focus-based checks (legacy support)
|
|
184
|
+
if (this.providerRegistry.hasProvider('ai')) {
|
|
185
|
+
logFn(`🔧 Debug: Using AI provider with focus mapping`);
|
|
186
|
+
const provider = this.providerRegistry.getProviderOrThrow('ai');
|
|
187
|
+
let focus = 'all';
|
|
188
|
+
let checkName = 'all';
|
|
189
|
+
if (checks.length === 1) {
|
|
190
|
+
checkName = checks[0];
|
|
191
|
+
if (checks[0] === 'security' || checks[0] === 'performance' || checks[0] === 'style') {
|
|
192
|
+
focus = checks[0];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// For multiple checks, combine them into 'all' focus
|
|
197
|
+
focus = 'all';
|
|
198
|
+
}
|
|
199
|
+
const providerConfig = {
|
|
200
|
+
type: 'ai',
|
|
201
|
+
prompt: focus,
|
|
202
|
+
focus: focus,
|
|
203
|
+
ai: timeout ? { timeout } : undefined,
|
|
204
|
+
// Inherit global AI provider and model settings if config is available
|
|
205
|
+
ai_provider: config?.ai_provider,
|
|
206
|
+
ai_model: config?.ai_model,
|
|
207
|
+
};
|
|
208
|
+
const result = await provider.execute(prInfo, providerConfig);
|
|
209
|
+
// Prefix issues with check name for consistent grouping
|
|
210
|
+
const prefixedIssues = result.issues.map(issue => ({
|
|
211
|
+
...issue,
|
|
212
|
+
ruleId: `${checkName}/${issue.ruleId}`,
|
|
213
|
+
}));
|
|
214
|
+
return {
|
|
215
|
+
...result,
|
|
216
|
+
issues: prefixedIssues,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// Fallback to existing PRReviewer for backward compatibility
|
|
220
|
+
logFn(`🔧 Debug: Using legacy PRReviewer fallback`);
|
|
221
|
+
const focusMap = {
|
|
222
|
+
security: 'security',
|
|
223
|
+
performance: 'performance',
|
|
224
|
+
style: 'style',
|
|
225
|
+
all: 'all',
|
|
226
|
+
architecture: 'all',
|
|
227
|
+
};
|
|
228
|
+
let focus = 'all';
|
|
229
|
+
if (checks.length === 1 && focusMap[checks[0]]) {
|
|
230
|
+
focus = focusMap[checks[0]];
|
|
231
|
+
}
|
|
232
|
+
return await this.reviewer.reviewPR('local', 'repository', 0, prInfo, {
|
|
233
|
+
focus,
|
|
234
|
+
format: 'table',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Execute multiple checks with dependency awareness - intelligently parallel and sequential
|
|
239
|
+
*/
|
|
240
|
+
async executeDependencyAwareChecks(prInfo, checks, timeout, config, logFn, debug) {
|
|
241
|
+
const log = logFn || console.error;
|
|
242
|
+
log(`🔧 Debug: Starting dependency-aware execution of ${checks.length} checks`);
|
|
243
|
+
if (!config?.checks) {
|
|
244
|
+
throw new Error('Config with check definitions required for dependency-aware execution');
|
|
245
|
+
}
|
|
246
|
+
// Build dependency graph
|
|
247
|
+
const dependencies = {};
|
|
248
|
+
for (const checkName of checks) {
|
|
249
|
+
const checkConfig = config.checks[checkName];
|
|
250
|
+
if (checkConfig) {
|
|
251
|
+
dependencies[checkName] = checkConfig.depends_on || [];
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
dependencies[checkName] = [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Validate dependencies
|
|
258
|
+
const validation = dependency_resolver_1.DependencyResolver.validateDependencies(checks, dependencies);
|
|
259
|
+
if (!validation.valid) {
|
|
260
|
+
return {
|
|
261
|
+
issues: [
|
|
262
|
+
{
|
|
263
|
+
severity: 'error',
|
|
264
|
+
message: `Dependency validation failed: ${validation.errors.join(', ')}`,
|
|
265
|
+
file: '',
|
|
266
|
+
line: 0,
|
|
267
|
+
ruleId: 'dependency-validation-error',
|
|
268
|
+
category: 'logic',
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
suggestions: [],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// Build dependency graph
|
|
275
|
+
const dependencyGraph = dependency_resolver_1.DependencyResolver.buildDependencyGraph(dependencies);
|
|
276
|
+
if (dependencyGraph.hasCycles) {
|
|
277
|
+
return {
|
|
278
|
+
issues: [
|
|
279
|
+
{
|
|
280
|
+
severity: 'error',
|
|
281
|
+
message: `Circular dependencies detected: ${dependencyGraph.cycleNodes?.join(' -> ')}`,
|
|
282
|
+
file: '',
|
|
283
|
+
line: 0,
|
|
284
|
+
ruleId: 'circular-dependency-error',
|
|
285
|
+
category: 'logic',
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
suggestions: [],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// Log execution plan
|
|
292
|
+
const stats = dependency_resolver_1.DependencyResolver.getExecutionStats(dependencyGraph);
|
|
293
|
+
log(`🔧 Debug: Execution plan - ${stats.totalChecks} checks in ${stats.parallelLevels} levels, max parallelism: ${stats.maxParallelism}`);
|
|
294
|
+
// Execute checks level by level
|
|
295
|
+
const results = new Map();
|
|
296
|
+
const provider = this.providerRegistry.getProviderOrThrow('ai');
|
|
297
|
+
for (let levelIndex = 0; levelIndex < dependencyGraph.executionOrder.length; levelIndex++) {
|
|
298
|
+
const executionGroup = dependencyGraph.executionOrder[levelIndex];
|
|
299
|
+
log(`🔧 Debug: Executing level ${executionGroup.level} with ${executionGroup.parallel.length} checks in parallel`);
|
|
300
|
+
// Execute all checks in this level in parallel
|
|
301
|
+
const levelTasks = executionGroup.parallel.map(async (checkName) => {
|
|
302
|
+
const checkConfig = config.checks[checkName];
|
|
303
|
+
if (!checkConfig) {
|
|
304
|
+
return {
|
|
305
|
+
checkName,
|
|
306
|
+
error: `No configuration found for check: ${checkName}`,
|
|
307
|
+
result: null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
log(`🔧 Debug: Starting check: ${checkName} at level ${executionGroup.level}`);
|
|
312
|
+
// Evaluate if condition to determine whether to run this check
|
|
313
|
+
if (checkConfig.if) {
|
|
314
|
+
const shouldRun = await this.failureEvaluator.evaluateIfCondition(checkName, checkConfig.if, {
|
|
315
|
+
branch: prInfo.head,
|
|
316
|
+
baseBranch: prInfo.base,
|
|
317
|
+
filesChanged: prInfo.files.map(f => f.filename),
|
|
318
|
+
event: 'manual', // TODO: Get actual event from context
|
|
319
|
+
environment: getSafeEnvironmentVariables(),
|
|
320
|
+
previousResults: results,
|
|
321
|
+
});
|
|
322
|
+
if (!shouldRun) {
|
|
323
|
+
log(`🔧 Debug: Skipping check '${checkName}' - if condition evaluated to false`);
|
|
324
|
+
return {
|
|
325
|
+
checkName,
|
|
326
|
+
error: null,
|
|
327
|
+
result: {
|
|
328
|
+
issues: [],
|
|
329
|
+
suggestions: [`Check '${checkName}' was skipped - condition not met`],
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Create provider config for this specific check
|
|
335
|
+
const providerConfig = {
|
|
336
|
+
type: 'ai',
|
|
337
|
+
prompt: checkConfig.prompt,
|
|
338
|
+
focus: checkConfig.focus || this.mapCheckNameToFocus(checkName),
|
|
339
|
+
schema: checkConfig.schema,
|
|
340
|
+
group: checkConfig.group,
|
|
341
|
+
ai: {
|
|
342
|
+
timeout: timeout || 600000,
|
|
343
|
+
debug: debug,
|
|
344
|
+
...(checkConfig.ai || {}),
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
// Pass results from dependencies if needed
|
|
348
|
+
const dependencyResults = new Map();
|
|
349
|
+
for (const depId of checkConfig.depends_on || []) {
|
|
350
|
+
if (results.has(depId)) {
|
|
351
|
+
dependencyResults.set(depId, results.get(depId));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const result = await provider.execute(prInfo, providerConfig, dependencyResults);
|
|
355
|
+
log(`🔧 Debug: Completed check: ${checkName}, issues found: ${result.issues.length}`);
|
|
356
|
+
// Add group, schema, template info and timestamp to issues from config
|
|
357
|
+
const enrichedIssues = result.issues.map(issue => ({
|
|
358
|
+
...issue,
|
|
359
|
+
ruleId: `${checkName}/${issue.ruleId}`,
|
|
360
|
+
group: checkConfig.group,
|
|
361
|
+
schema: checkConfig.schema,
|
|
362
|
+
template: checkConfig.template,
|
|
363
|
+
timestamp: Date.now(),
|
|
364
|
+
}));
|
|
365
|
+
const enrichedResult = {
|
|
366
|
+
...result,
|
|
367
|
+
issues: enrichedIssues,
|
|
368
|
+
};
|
|
369
|
+
return {
|
|
370
|
+
checkName,
|
|
371
|
+
error: null,
|
|
372
|
+
result: enrichedResult,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
377
|
+
log(`🔧 Debug: Error in check ${checkName}: ${errorMessage}`);
|
|
378
|
+
return {
|
|
379
|
+
checkName,
|
|
380
|
+
error: errorMessage,
|
|
381
|
+
result: null,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
// Wait for all checks in this level to complete
|
|
386
|
+
const levelResults = await Promise.allSettled(levelTasks);
|
|
387
|
+
// Process results and store them for next level
|
|
388
|
+
for (let i = 0; i < levelResults.length; i++) {
|
|
389
|
+
const checkName = executionGroup.parallel[i];
|
|
390
|
+
const result = levelResults[i];
|
|
391
|
+
if (result.status === 'fulfilled' && result.value.result && !result.value.error) {
|
|
392
|
+
results.set(checkName, result.value.result);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// Store error result for dependency tracking
|
|
396
|
+
const errorSummary = {
|
|
397
|
+
issues: [
|
|
398
|
+
{
|
|
399
|
+
file: 'system',
|
|
400
|
+
line: 0,
|
|
401
|
+
endLine: undefined,
|
|
402
|
+
ruleId: `${checkName}/error`,
|
|
403
|
+
message: result.status === 'fulfilled'
|
|
404
|
+
? result.value.error || 'Unknown error'
|
|
405
|
+
: result.reason instanceof Error
|
|
406
|
+
? result.reason.message
|
|
407
|
+
: String(result.reason),
|
|
408
|
+
severity: 'error',
|
|
409
|
+
category: 'logic',
|
|
410
|
+
suggestion: undefined,
|
|
411
|
+
replacement: undefined,
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
suggestions: [],
|
|
415
|
+
};
|
|
416
|
+
results.set(checkName, errorSummary);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Aggregate all results
|
|
421
|
+
return this.aggregateDependencyAwareResults(results, dependencyGraph, debug);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Execute multiple checks in parallel using Promise.allSettled (legacy method)
|
|
425
|
+
*/
|
|
426
|
+
async executeParallelChecks(prInfo, checks, timeout, config, logFn, debug) {
|
|
427
|
+
const log = logFn || console.error;
|
|
428
|
+
log(`🔧 Debug: Starting parallel execution of ${checks.length} checks`);
|
|
429
|
+
if (!config?.checks) {
|
|
430
|
+
throw new Error('Config with check definitions required for parallel execution');
|
|
431
|
+
}
|
|
432
|
+
const provider = this.providerRegistry.getProviderOrThrow('ai');
|
|
433
|
+
// Create individual check tasks
|
|
434
|
+
const checkTasks = checks.map(async (checkName) => {
|
|
435
|
+
const checkConfig = config.checks[checkName];
|
|
436
|
+
if (!checkConfig) {
|
|
437
|
+
log(`🔧 Debug: No config found for check: ${checkName}`);
|
|
438
|
+
return {
|
|
439
|
+
checkName,
|
|
440
|
+
error: `No configuration found for check: ${checkName}`,
|
|
441
|
+
result: null,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
console.error(`🔧 Debug: Starting check: ${checkName} with prompt type: ${typeof checkConfig.prompt}`);
|
|
446
|
+
// Evaluate if condition to determine whether to run this check
|
|
447
|
+
if (checkConfig.if) {
|
|
448
|
+
const shouldRun = await this.failureEvaluator.evaluateIfCondition(checkName, checkConfig.if, {
|
|
449
|
+
branch: prInfo.head,
|
|
450
|
+
baseBranch: prInfo.base,
|
|
451
|
+
filesChanged: prInfo.files.map(f => f.filename),
|
|
452
|
+
event: 'manual', // TODO: Get actual event from context
|
|
453
|
+
environment: getSafeEnvironmentVariables(),
|
|
454
|
+
previousResults: new Map(), // No previous results in parallel execution
|
|
455
|
+
});
|
|
456
|
+
if (!shouldRun) {
|
|
457
|
+
console.error(`🔧 Debug: Skipping check '${checkName}' - if condition evaluated to false`);
|
|
458
|
+
return {
|
|
459
|
+
checkName,
|
|
460
|
+
error: null,
|
|
461
|
+
result: {
|
|
462
|
+
issues: [],
|
|
463
|
+
suggestions: [`Check '${checkName}' was skipped - condition not met`],
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Create provider config for this specific check
|
|
469
|
+
const providerConfig = {
|
|
470
|
+
type: 'ai',
|
|
471
|
+
prompt: checkConfig.prompt,
|
|
472
|
+
focus: checkConfig.focus || this.mapCheckNameToFocus(checkName),
|
|
473
|
+
schema: checkConfig.schema,
|
|
474
|
+
group: checkConfig.group,
|
|
475
|
+
ai: {
|
|
476
|
+
timeout: timeout || 600000,
|
|
477
|
+
debug: debug, // Pass debug flag to AI provider
|
|
478
|
+
...(checkConfig.ai || {}),
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
const result = await provider.execute(prInfo, providerConfig);
|
|
482
|
+
console.error(`🔧 Debug: Completed check: ${checkName}, issues found: ${result.issues.length}`);
|
|
483
|
+
// Add group, schema info and timestamp to issues from config
|
|
484
|
+
const enrichedIssues = result.issues.map(issue => ({
|
|
485
|
+
...issue,
|
|
486
|
+
ruleId: `${checkName}/${issue.ruleId}`,
|
|
487
|
+
group: checkConfig.group,
|
|
488
|
+
schema: checkConfig.schema,
|
|
489
|
+
template: checkConfig.template,
|
|
490
|
+
timestamp: Date.now(),
|
|
491
|
+
}));
|
|
492
|
+
const enrichedResult = {
|
|
493
|
+
...result,
|
|
494
|
+
issues: enrichedIssues,
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
checkName,
|
|
498
|
+
error: null,
|
|
499
|
+
result: enrichedResult,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
504
|
+
log(`🔧 Debug: Error in check ${checkName}: ${errorMessage}`);
|
|
505
|
+
return {
|
|
506
|
+
checkName,
|
|
507
|
+
error: errorMessage,
|
|
508
|
+
result: null,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
// Execute all checks in parallel using Promise.allSettled
|
|
513
|
+
log(`🔧 Debug: Executing ${checkTasks.length} checks in parallel`);
|
|
514
|
+
const results = await Promise.allSettled(checkTasks);
|
|
515
|
+
// Aggregate results from all checks
|
|
516
|
+
return this.aggregateParallelResults(results, checks, debug);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Execute a single configured check
|
|
520
|
+
*/
|
|
521
|
+
async executeSingleConfiguredCheck(prInfo, checkName, timeout, config, _logFn) {
|
|
522
|
+
if (!config?.checks?.[checkName]) {
|
|
523
|
+
throw new Error(`No configuration found for check: ${checkName}`);
|
|
524
|
+
}
|
|
525
|
+
const checkConfig = config.checks[checkName];
|
|
526
|
+
const provider = this.providerRegistry.getProviderOrThrow('ai');
|
|
527
|
+
const providerConfig = {
|
|
528
|
+
type: 'ai',
|
|
529
|
+
prompt: checkConfig.prompt,
|
|
530
|
+
focus: checkConfig.focus || this.mapCheckNameToFocus(checkName),
|
|
531
|
+
schema: checkConfig.schema,
|
|
532
|
+
group: checkConfig.group,
|
|
533
|
+
ai: {
|
|
534
|
+
timeout: timeout || 600000,
|
|
535
|
+
...(checkConfig.ai || {}),
|
|
536
|
+
},
|
|
537
|
+
// Inherit global AI provider and model settings
|
|
538
|
+
ai_provider: checkConfig.ai_provider || config.ai_provider,
|
|
539
|
+
ai_model: checkConfig.ai_model || config.ai_model,
|
|
540
|
+
};
|
|
541
|
+
const result = await provider.execute(prInfo, providerConfig);
|
|
542
|
+
// Prefix issues with check name and add group/schema info and timestamp from config
|
|
543
|
+
const prefixedIssues = result.issues.map(issue => ({
|
|
544
|
+
...issue,
|
|
545
|
+
ruleId: `${checkName}/${issue.ruleId}`,
|
|
546
|
+
group: checkConfig.group,
|
|
547
|
+
schema: checkConfig.schema,
|
|
548
|
+
timestamp: Date.now(),
|
|
549
|
+
}));
|
|
550
|
+
return {
|
|
551
|
+
...result,
|
|
552
|
+
issues: prefixedIssues,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Map check name to focus for AI provider
|
|
557
|
+
* This is a fallback when focus is not explicitly configured
|
|
558
|
+
*/
|
|
559
|
+
mapCheckNameToFocus(checkName) {
|
|
560
|
+
const focusMap = {
|
|
561
|
+
security: 'security',
|
|
562
|
+
performance: 'performance',
|
|
563
|
+
style: 'style',
|
|
564
|
+
architecture: 'architecture',
|
|
565
|
+
};
|
|
566
|
+
return focusMap[checkName] || 'all';
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Aggregate results from dependency-aware check execution
|
|
570
|
+
*/
|
|
571
|
+
aggregateDependencyAwareResults(results, dependencyGraph, debug) {
|
|
572
|
+
const aggregatedIssues = [];
|
|
573
|
+
const aggregatedSuggestions = [];
|
|
574
|
+
const debugInfo = [];
|
|
575
|
+
// Add execution plan info
|
|
576
|
+
const stats = dependency_resolver_1.DependencyResolver.getExecutionStats(dependencyGraph);
|
|
577
|
+
debugInfo.push(`🔍 Dependency-aware execution completed:`, ` - ${stats.totalChecks} checks in ${stats.parallelLevels} execution levels`, ` - Maximum parallelism: ${stats.maxParallelism}`, ` - Average parallelism: ${stats.averageParallelism.toFixed(1)}`, ` - Checks with dependencies: ${stats.checksWithDependencies}`);
|
|
578
|
+
// Process results in dependency order for better output organization
|
|
579
|
+
for (const executionGroup of dependencyGraph.executionOrder) {
|
|
580
|
+
for (const checkName of executionGroup.parallel) {
|
|
581
|
+
const result = results.get(checkName);
|
|
582
|
+
if (!result) {
|
|
583
|
+
debugInfo.push(`❌ Check "${checkName}" had no result`);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
// Check if this was a successful result
|
|
587
|
+
const hasErrors = result.issues.some(issue => issue.ruleId?.includes('/error') || issue.ruleId?.includes('/promise-error'));
|
|
588
|
+
if (hasErrors) {
|
|
589
|
+
debugInfo.push(`❌ Check "${checkName}" failed with errors`);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
debugInfo.push(`✅ Check "${checkName}" completed: ${result.issues.length} issues found (level ${executionGroup.level})`);
|
|
593
|
+
}
|
|
594
|
+
// Issues are already prefixed and enriched with group/schema info
|
|
595
|
+
aggregatedIssues.push(...result.issues);
|
|
596
|
+
// Add suggestions with check name prefix
|
|
597
|
+
const prefixedSuggestions = result.suggestions.map(suggestion => `[${checkName}] ${suggestion}`);
|
|
598
|
+
aggregatedSuggestions.push(...prefixedSuggestions);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Add summary information
|
|
602
|
+
aggregatedSuggestions.unshift(...debugInfo);
|
|
603
|
+
console.error(`🔧 Debug: Aggregated ${aggregatedIssues.length} issues from ${results.size} dependency-aware checks`);
|
|
604
|
+
// Collect debug information when debug mode is enabled
|
|
605
|
+
let aggregatedDebug;
|
|
606
|
+
if (debug) {
|
|
607
|
+
const debugResults = Array.from(results.entries()).filter(([_, result]) => result.debug);
|
|
608
|
+
if (debugResults.length > 0) {
|
|
609
|
+
const [, firstResult] = debugResults[0];
|
|
610
|
+
const firstDebug = firstResult.debug;
|
|
611
|
+
const totalProcessingTime = debugResults.reduce((sum, [_, result]) => {
|
|
612
|
+
return sum + (result.debug.processingTime || 0);
|
|
613
|
+
}, 0);
|
|
614
|
+
aggregatedDebug = {
|
|
615
|
+
provider: firstDebug.provider,
|
|
616
|
+
model: firstDebug.model,
|
|
617
|
+
apiKeySource: firstDebug.apiKeySource,
|
|
618
|
+
processingTime: totalProcessingTime,
|
|
619
|
+
prompt: debugResults
|
|
620
|
+
.map(([checkName, result]) => `[${checkName}]\n${result.debug.prompt}`)
|
|
621
|
+
.join('\n\n'),
|
|
622
|
+
rawResponse: debugResults
|
|
623
|
+
.map(([checkName, result]) => `[${checkName}]\n${result.debug.rawResponse}`)
|
|
624
|
+
.join('\n\n'),
|
|
625
|
+
promptLength: debugResults.reduce((sum, [_, result]) => sum + (result.debug.promptLength || 0), 0),
|
|
626
|
+
responseLength: debugResults.reduce((sum, [_, result]) => sum + (result.debug.responseLength || 0), 0),
|
|
627
|
+
jsonParseSuccess: debugResults.every(([_, result]) => result.debug.jsonParseSuccess),
|
|
628
|
+
errors: debugResults.flatMap(([checkName, result]) => (result.debug.errors || []).map((error) => `[${checkName}] ${error}`)),
|
|
629
|
+
timestamp: new Date().toISOString(),
|
|
630
|
+
totalApiCalls: debugResults.length,
|
|
631
|
+
apiCallDetails: debugResults.map(([checkName, result]) => ({
|
|
632
|
+
checkName,
|
|
633
|
+
provider: result.debug.provider,
|
|
634
|
+
model: result.debug.model,
|
|
635
|
+
processingTime: result.debug.processingTime,
|
|
636
|
+
success: result.debug.jsonParseSuccess,
|
|
637
|
+
})),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
issues: aggregatedIssues,
|
|
643
|
+
suggestions: aggregatedSuggestions,
|
|
644
|
+
debug: aggregatedDebug,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Aggregate results from parallel check execution (legacy method)
|
|
649
|
+
*/
|
|
650
|
+
aggregateParallelResults(results, checkNames, debug) {
|
|
651
|
+
const aggregatedIssues = [];
|
|
652
|
+
const aggregatedSuggestions = [];
|
|
653
|
+
const debugInfo = [];
|
|
654
|
+
let successfulChecks = 0;
|
|
655
|
+
let failedChecks = 0;
|
|
656
|
+
results.forEach((result, index) => {
|
|
657
|
+
const checkName = checkNames[index];
|
|
658
|
+
if (result.status === 'fulfilled') {
|
|
659
|
+
const checkResult = result.value;
|
|
660
|
+
if (checkResult.error) {
|
|
661
|
+
failedChecks++;
|
|
662
|
+
const log = console.error;
|
|
663
|
+
log(`🔧 Debug: Check ${checkName} failed: ${checkResult.error}`);
|
|
664
|
+
debugInfo.push(`❌ Check "${checkName}" failed: ${checkResult.error}`);
|
|
665
|
+
// Check if this is a critical error
|
|
666
|
+
const isCriticalError = checkResult.error.includes('API rate limit') ||
|
|
667
|
+
checkResult.error.includes('403') ||
|
|
668
|
+
checkResult.error.includes('401') ||
|
|
669
|
+
checkResult.error.includes('authentication') ||
|
|
670
|
+
checkResult.error.includes('API key');
|
|
671
|
+
// Add error as an issue with appropriate severity
|
|
672
|
+
aggregatedIssues.push({
|
|
673
|
+
file: 'system',
|
|
674
|
+
line: 0,
|
|
675
|
+
endLine: undefined,
|
|
676
|
+
ruleId: `${checkName}/error`,
|
|
677
|
+
message: `Check "${checkName}" failed: ${checkResult.error}`,
|
|
678
|
+
severity: isCriticalError ? 'critical' : 'error',
|
|
679
|
+
category: 'logic',
|
|
680
|
+
suggestion: isCriticalError
|
|
681
|
+
? 'Please check your API credentials and rate limits'
|
|
682
|
+
: undefined,
|
|
683
|
+
replacement: undefined,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
else if (checkResult.result) {
|
|
687
|
+
successfulChecks++;
|
|
688
|
+
console.error(`🔧 Debug: Check ${checkName} succeeded with ${checkResult.result.issues.length} issues`);
|
|
689
|
+
debugInfo.push(`✅ Check "${checkName}" completed: ${checkResult.result.issues.length} issues found`);
|
|
690
|
+
// Issues are already prefixed and enriched with group/schema info
|
|
691
|
+
aggregatedIssues.push(...checkResult.result.issues);
|
|
692
|
+
// Add suggestions with check name prefix
|
|
693
|
+
const prefixedSuggestions = checkResult.result.suggestions.map(suggestion => `[${checkName}] ${suggestion}`);
|
|
694
|
+
aggregatedSuggestions.push(...prefixedSuggestions);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
failedChecks++;
|
|
699
|
+
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
700
|
+
const log = console.error;
|
|
701
|
+
log(`🔧 Debug: Check ${checkName} promise rejected: ${errorMessage}`);
|
|
702
|
+
debugInfo.push(`❌ Check "${checkName}" promise rejected: ${errorMessage}`);
|
|
703
|
+
// Check if this is a critical error
|
|
704
|
+
const isCriticalError = errorMessage.includes('API rate limit') ||
|
|
705
|
+
errorMessage.includes('403') ||
|
|
706
|
+
errorMessage.includes('401') ||
|
|
707
|
+
errorMessage.includes('authentication') ||
|
|
708
|
+
errorMessage.includes('API key');
|
|
709
|
+
aggregatedIssues.push({
|
|
710
|
+
file: 'system',
|
|
711
|
+
line: 0,
|
|
712
|
+
endLine: undefined,
|
|
713
|
+
ruleId: `${checkName}/promise-error`,
|
|
714
|
+
message: `Check "${checkName}" execution failed: ${errorMessage}`,
|
|
715
|
+
severity: isCriticalError ? 'critical' : 'error',
|
|
716
|
+
category: 'logic',
|
|
717
|
+
suggestion: isCriticalError
|
|
718
|
+
? 'Please check your API credentials and rate limits'
|
|
719
|
+
: undefined,
|
|
720
|
+
replacement: undefined,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
// Add summary information
|
|
725
|
+
debugInfo.unshift(`🔍 Parallel execution completed: ${successfulChecks} successful, ${failedChecks} failed`);
|
|
726
|
+
aggregatedSuggestions.unshift(...debugInfo);
|
|
727
|
+
console.error(`🔧 Debug: Aggregated ${aggregatedIssues.length} issues from ${results.length} checks`);
|
|
728
|
+
// Collect debug information when debug mode is enabled
|
|
729
|
+
let aggregatedDebug;
|
|
730
|
+
if (debug) {
|
|
731
|
+
// Find the first successful result with debug information to use as template
|
|
732
|
+
const debugResults = results
|
|
733
|
+
.map((result, index) => ({
|
|
734
|
+
result,
|
|
735
|
+
checkName: checkNames[index],
|
|
736
|
+
}))
|
|
737
|
+
.filter(({ result }) => result.status === 'fulfilled' && result.value?.result?.debug);
|
|
738
|
+
if (debugResults.length > 0) {
|
|
739
|
+
const firstResult = debugResults[0].result;
|
|
740
|
+
if (firstResult.status === 'fulfilled') {
|
|
741
|
+
const firstDebug = firstResult.value.result.debug;
|
|
742
|
+
const totalProcessingTime = debugResults.reduce((sum, { result }) => {
|
|
743
|
+
if (result.status === 'fulfilled') {
|
|
744
|
+
return sum + (result.value.result.debug.processingTime || 0);
|
|
745
|
+
}
|
|
746
|
+
return sum;
|
|
747
|
+
}, 0);
|
|
748
|
+
aggregatedDebug = {
|
|
749
|
+
// Use first result as template for provider/model info
|
|
750
|
+
provider: firstDebug.provider,
|
|
751
|
+
model: firstDebug.model,
|
|
752
|
+
apiKeySource: firstDebug.apiKeySource,
|
|
753
|
+
// Aggregate processing time from all checks
|
|
754
|
+
processingTime: totalProcessingTime,
|
|
755
|
+
// Combine prompts with check names
|
|
756
|
+
prompt: debugResults
|
|
757
|
+
.map(({ checkName, result }) => {
|
|
758
|
+
if (result.status === 'fulfilled') {
|
|
759
|
+
return `[${checkName}]\n${result.value.result.debug.prompt}`;
|
|
760
|
+
}
|
|
761
|
+
return `[${checkName}] Error: Promise was rejected`;
|
|
762
|
+
})
|
|
763
|
+
.join('\n\n'),
|
|
764
|
+
// Combine responses
|
|
765
|
+
rawResponse: debugResults
|
|
766
|
+
.map(({ checkName, result }) => {
|
|
767
|
+
if (result.status === 'fulfilled') {
|
|
768
|
+
return `[${checkName}]\n${result.value.result.debug.rawResponse}`;
|
|
769
|
+
}
|
|
770
|
+
return `[${checkName}] Error: Promise was rejected`;
|
|
771
|
+
})
|
|
772
|
+
.join('\n\n'),
|
|
773
|
+
promptLength: debugResults.reduce((sum, { result }) => {
|
|
774
|
+
if (result.status === 'fulfilled') {
|
|
775
|
+
return sum + (result.value.result.debug.promptLength || 0);
|
|
776
|
+
}
|
|
777
|
+
return sum;
|
|
778
|
+
}, 0),
|
|
779
|
+
responseLength: debugResults.reduce((sum, { result }) => {
|
|
780
|
+
if (result.status === 'fulfilled') {
|
|
781
|
+
return sum + (result.value.result.debug.responseLength || 0);
|
|
782
|
+
}
|
|
783
|
+
return sum;
|
|
784
|
+
}, 0),
|
|
785
|
+
jsonParseSuccess: debugResults.every(({ result }) => {
|
|
786
|
+
if (result.status === 'fulfilled') {
|
|
787
|
+
return result.value.result.debug.jsonParseSuccess;
|
|
788
|
+
}
|
|
789
|
+
return false;
|
|
790
|
+
}),
|
|
791
|
+
errors: debugResults.flatMap(({ result, checkName }) => {
|
|
792
|
+
if (result.status === 'fulfilled') {
|
|
793
|
+
return (result.value.result.debug.errors || []).map((error) => `[${checkName}] ${error}`);
|
|
794
|
+
}
|
|
795
|
+
return [`[${checkName}] Promise was rejected`];
|
|
796
|
+
}),
|
|
797
|
+
timestamp: new Date().toISOString(),
|
|
798
|
+
// Add additional debug information for parallel execution
|
|
799
|
+
totalApiCalls: debugResults.length,
|
|
800
|
+
apiCallDetails: debugResults.map(({ checkName, result }) => {
|
|
801
|
+
if (result.status === 'fulfilled') {
|
|
802
|
+
return {
|
|
803
|
+
checkName,
|
|
804
|
+
provider: result.value.result.debug.provider,
|
|
805
|
+
model: result.value.result.debug.model,
|
|
806
|
+
processingTime: result.value.result.debug.processingTime,
|
|
807
|
+
success: result.value.result.debug.jsonParseSuccess,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
checkName,
|
|
812
|
+
provider: 'unknown',
|
|
813
|
+
model: 'unknown',
|
|
814
|
+
processingTime: 0,
|
|
815
|
+
success: false,
|
|
816
|
+
};
|
|
817
|
+
}),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
issues: aggregatedIssues,
|
|
824
|
+
suggestions: aggregatedSuggestions,
|
|
825
|
+
debug: aggregatedDebug,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Get available check types
|
|
830
|
+
*/
|
|
831
|
+
static getAvailableCheckTypes() {
|
|
832
|
+
const registry = check_provider_registry_1.CheckProviderRegistry.getInstance();
|
|
833
|
+
const providerTypes = registry.getAvailableProviders();
|
|
834
|
+
// Add standard focus-based checks
|
|
835
|
+
const standardTypes = ['security', 'performance', 'style', 'architecture', 'all'];
|
|
836
|
+
// Combine provider types with standard types (remove duplicates)
|
|
837
|
+
return [...new Set([...providerTypes, ...standardTypes])];
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Validate check types
|
|
841
|
+
*/
|
|
842
|
+
static validateCheckTypes(checks) {
|
|
843
|
+
const availableChecks = CheckExecutionEngine.getAvailableCheckTypes();
|
|
844
|
+
const valid = [];
|
|
845
|
+
const invalid = [];
|
|
846
|
+
for (const check of checks) {
|
|
847
|
+
if (availableChecks.includes(check)) {
|
|
848
|
+
valid.push(check);
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
invalid.push(check);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return { valid, invalid };
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* List available providers with their status
|
|
858
|
+
*/
|
|
859
|
+
async listProviders() {
|
|
860
|
+
return await this.providerRegistry.listProviders();
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Create a mock Octokit instance for local analysis
|
|
864
|
+
*/
|
|
865
|
+
createMockOctokit() {
|
|
866
|
+
// Create simple mock functions that return promises
|
|
867
|
+
const mockGet = async () => ({
|
|
868
|
+
data: {
|
|
869
|
+
number: 0,
|
|
870
|
+
title: 'Local Analysis',
|
|
871
|
+
body: 'Local repository analysis',
|
|
872
|
+
user: { login: 'local-user' },
|
|
873
|
+
base: { ref: 'main' },
|
|
874
|
+
head: { ref: 'HEAD' },
|
|
875
|
+
},
|
|
876
|
+
});
|
|
877
|
+
const mockListFiles = async () => ({
|
|
878
|
+
data: [],
|
|
879
|
+
});
|
|
880
|
+
const mockListComments = async () => ({
|
|
881
|
+
data: [],
|
|
882
|
+
});
|
|
883
|
+
const mockCreateComment = async () => ({
|
|
884
|
+
data: { id: 1 },
|
|
885
|
+
});
|
|
886
|
+
return {
|
|
887
|
+
rest: {
|
|
888
|
+
pulls: {
|
|
889
|
+
get: mockGet,
|
|
890
|
+
listFiles: mockListFiles,
|
|
891
|
+
},
|
|
892
|
+
issues: {
|
|
893
|
+
listComments: mockListComments,
|
|
894
|
+
createComment: mockCreateComment,
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
request: async () => ({ data: {} }),
|
|
898
|
+
graphql: async () => ({}),
|
|
899
|
+
log: {
|
|
900
|
+
debug: () => { },
|
|
901
|
+
info: () => { },
|
|
902
|
+
warn: () => { },
|
|
903
|
+
error: () => { },
|
|
904
|
+
},
|
|
905
|
+
hook: {
|
|
906
|
+
before: () => { },
|
|
907
|
+
after: () => { },
|
|
908
|
+
error: () => { },
|
|
909
|
+
wrap: () => { },
|
|
910
|
+
},
|
|
911
|
+
auth: async () => ({ token: 'mock-token' }),
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Create an error result
|
|
916
|
+
*/
|
|
917
|
+
createErrorResult(repositoryInfo, errorMessage, startTime, timestamp, checksExecuted) {
|
|
918
|
+
const executionTime = Date.now() - startTime;
|
|
919
|
+
return {
|
|
920
|
+
repositoryInfo,
|
|
921
|
+
reviewSummary: {
|
|
922
|
+
issues: [
|
|
923
|
+
{
|
|
924
|
+
file: 'system',
|
|
925
|
+
line: 0,
|
|
926
|
+
endLine: undefined,
|
|
927
|
+
ruleId: 'system/error',
|
|
928
|
+
message: errorMessage,
|
|
929
|
+
severity: 'error',
|
|
930
|
+
category: 'logic',
|
|
931
|
+
suggestion: undefined,
|
|
932
|
+
replacement: undefined,
|
|
933
|
+
},
|
|
934
|
+
],
|
|
935
|
+
suggestions: [`Error: ${errorMessage}`],
|
|
936
|
+
},
|
|
937
|
+
executionTime,
|
|
938
|
+
timestamp,
|
|
939
|
+
checksExecuted,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Check if the working directory is a valid git repository
|
|
944
|
+
*/
|
|
945
|
+
async isGitRepository() {
|
|
946
|
+
try {
|
|
947
|
+
const repositoryInfo = await this.gitAnalyzer.analyzeRepository();
|
|
948
|
+
return repositoryInfo.isGitRepository;
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Evaluate failure conditions for a check result
|
|
956
|
+
*/
|
|
957
|
+
async evaluateFailureConditions(checkName, reviewSummary, config) {
|
|
958
|
+
if (!config) {
|
|
959
|
+
return [];
|
|
960
|
+
}
|
|
961
|
+
const checkConfig = config.checks[checkName];
|
|
962
|
+
const checkSchema = checkConfig?.schema || '';
|
|
963
|
+
const checkGroup = checkConfig?.group || '';
|
|
964
|
+
// Handle new simple fail_if syntax
|
|
965
|
+
const globalFailIf = config.fail_if;
|
|
966
|
+
const checkFailIf = checkConfig?.fail_if;
|
|
967
|
+
// If using new fail_if syntax
|
|
968
|
+
if (globalFailIf || checkFailIf) {
|
|
969
|
+
const results = [];
|
|
970
|
+
// Evaluate global fail_if
|
|
971
|
+
if (globalFailIf) {
|
|
972
|
+
const failed = await this.failureEvaluator.evaluateSimpleCondition(checkName, checkSchema, checkGroup, reviewSummary, globalFailIf);
|
|
973
|
+
if (failed) {
|
|
974
|
+
results.push({
|
|
975
|
+
conditionName: 'global_fail_if',
|
|
976
|
+
expression: globalFailIf,
|
|
977
|
+
failed: true,
|
|
978
|
+
severity: 'error',
|
|
979
|
+
message: 'Global failure condition met',
|
|
980
|
+
haltExecution: false,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// Evaluate check-specific fail_if (overrides global if present)
|
|
985
|
+
if (checkFailIf) {
|
|
986
|
+
const failed = await this.failureEvaluator.evaluateSimpleCondition(checkName, checkSchema, checkGroup, reviewSummary, checkFailIf);
|
|
987
|
+
if (failed) {
|
|
988
|
+
results.push({
|
|
989
|
+
conditionName: `${checkName}_fail_if`,
|
|
990
|
+
expression: checkFailIf,
|
|
991
|
+
failed: true,
|
|
992
|
+
severity: 'error',
|
|
993
|
+
message: `Check ${checkName} failure condition met`,
|
|
994
|
+
haltExecution: false,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return results;
|
|
999
|
+
}
|
|
1000
|
+
// Fall back to old failure_conditions syntax
|
|
1001
|
+
const globalConditions = config.failure_conditions;
|
|
1002
|
+
const checkConditions = checkConfig?.failure_conditions;
|
|
1003
|
+
return await this.failureEvaluator.evaluateConditions(checkName, checkSchema, checkGroup, reviewSummary, globalConditions, checkConditions);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Get repository status summary
|
|
1007
|
+
*/
|
|
1008
|
+
async getRepositoryStatus() {
|
|
1009
|
+
try {
|
|
1010
|
+
const repositoryInfo = await this.gitAnalyzer.analyzeRepository();
|
|
1011
|
+
return {
|
|
1012
|
+
isGitRepository: repositoryInfo.isGitRepository,
|
|
1013
|
+
hasChanges: repositoryInfo.files.length > 0,
|
|
1014
|
+
branch: repositoryInfo.head,
|
|
1015
|
+
filesChanged: repositoryInfo.files.length,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
return {
|
|
1020
|
+
isGitRepository: false,
|
|
1021
|
+
hasChanges: false,
|
|
1022
|
+
branch: 'unknown',
|
|
1023
|
+
filesChanged: 0,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Initialize GitHub check runs for each configured check
|
|
1029
|
+
*/
|
|
1030
|
+
async initializeGitHubChecks(options, logFn) {
|
|
1031
|
+
if (!options.githubChecks?.octokit ||
|
|
1032
|
+
!options.githubChecks.owner ||
|
|
1033
|
+
!options.githubChecks.repo ||
|
|
1034
|
+
!options.githubChecks.headSha) {
|
|
1035
|
+
logFn('⚠️ GitHub checks enabled but missing required parameters');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
this.githubCheckService = new github_check_service_1.GitHubCheckService(options.githubChecks.octokit);
|
|
1040
|
+
this.checkRunMap = new Map();
|
|
1041
|
+
this.githubContext = {
|
|
1042
|
+
owner: options.githubChecks.owner,
|
|
1043
|
+
repo: options.githubChecks.repo,
|
|
1044
|
+
};
|
|
1045
|
+
logFn(`🔍 Creating GitHub check runs for ${options.checks.length} checks...`);
|
|
1046
|
+
for (const checkName of options.checks) {
|
|
1047
|
+
try {
|
|
1048
|
+
const checkRunOptions = {
|
|
1049
|
+
owner: options.githubChecks.owner,
|
|
1050
|
+
repo: options.githubChecks.repo,
|
|
1051
|
+
head_sha: options.githubChecks.headSha,
|
|
1052
|
+
name: `Visor: ${checkName}`,
|
|
1053
|
+
external_id: `visor-${checkName}-${options.githubChecks.headSha.substring(0, 7)}`,
|
|
1054
|
+
};
|
|
1055
|
+
const checkRun = await this.githubCheckService.createCheckRun(checkRunOptions, {
|
|
1056
|
+
title: `${checkName} Analysis`,
|
|
1057
|
+
summary: `Running ${checkName} check using AI-powered analysis...`,
|
|
1058
|
+
});
|
|
1059
|
+
this.checkRunMap.set(checkName, checkRun);
|
|
1060
|
+
logFn(`✅ Created check run for ${checkName}: ${checkRun.url}`);
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
logFn(`❌ Failed to create check run for ${checkName}: ${error}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
// Check if this is a permissions error
|
|
1069
|
+
if (error instanceof Error &&
|
|
1070
|
+
(error.message.includes('403') || error.message.includes('checks:write'))) {
|
|
1071
|
+
logFn('⚠️ GitHub checks API not available - insufficient permissions. Check runs will be skipped.');
|
|
1072
|
+
logFn('💡 To enable check runs, ensure your GitHub token has "checks:write" permission.');
|
|
1073
|
+
this.githubCheckService = undefined;
|
|
1074
|
+
this.checkRunMap = undefined;
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
logFn(`❌ Failed to initialize GitHub check runs: ${error}`);
|
|
1078
|
+
this.githubCheckService = undefined;
|
|
1079
|
+
this.checkRunMap = undefined;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Update GitHub check runs to in-progress status
|
|
1085
|
+
*/
|
|
1086
|
+
async updateGitHubChecksInProgress(options) {
|
|
1087
|
+
if (!this.githubCheckService ||
|
|
1088
|
+
!this.checkRunMap ||
|
|
1089
|
+
!options.githubChecks?.owner ||
|
|
1090
|
+
!options.githubChecks.repo) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
for (const [checkName, checkRun] of this.checkRunMap) {
|
|
1094
|
+
try {
|
|
1095
|
+
await this.githubCheckService.updateCheckRunInProgress(options.githubChecks.owner, options.githubChecks.repo, checkRun.id, {
|
|
1096
|
+
title: `Analyzing with ${checkName}...`,
|
|
1097
|
+
summary: `AI-powered analysis is in progress for ${checkName} check.`,
|
|
1098
|
+
});
|
|
1099
|
+
console.log(`🔄 Updated ${checkName} check to in-progress status`);
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
console.error(`❌ Failed to update ${checkName} check to in-progress: ${error}`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Complete GitHub check runs with results
|
|
1108
|
+
*/
|
|
1109
|
+
async completeGitHubChecksWithResults(reviewSummary, options) {
|
|
1110
|
+
if (!this.githubCheckService ||
|
|
1111
|
+
!this.checkRunMap ||
|
|
1112
|
+
!options.githubChecks?.owner ||
|
|
1113
|
+
!options.githubChecks.repo) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
// Group issues by check name
|
|
1117
|
+
const issuesByCheck = new Map();
|
|
1118
|
+
// Initialize empty arrays for all checks
|
|
1119
|
+
for (const checkName of this.checkRunMap.keys()) {
|
|
1120
|
+
issuesByCheck.set(checkName, []);
|
|
1121
|
+
}
|
|
1122
|
+
// Group issues by their check name (extracted from ruleId prefix)
|
|
1123
|
+
for (const issue of reviewSummary.issues || []) {
|
|
1124
|
+
if (issue.ruleId && issue.ruleId.includes('/')) {
|
|
1125
|
+
const checkName = issue.ruleId.split('/')[0];
|
|
1126
|
+
if (issuesByCheck.has(checkName)) {
|
|
1127
|
+
issuesByCheck.get(checkName).push(issue);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
console.log(`🏁 Completing ${this.checkRunMap.size} GitHub check runs...`);
|
|
1132
|
+
for (const [checkName, checkRun] of this.checkRunMap) {
|
|
1133
|
+
try {
|
|
1134
|
+
const checkIssues = issuesByCheck.get(checkName) || [];
|
|
1135
|
+
// Evaluate failure conditions for this specific check
|
|
1136
|
+
const failureResults = await this.evaluateFailureConditions(checkName, { issues: checkIssues, suggestions: [] }, options.config);
|
|
1137
|
+
await this.githubCheckService.completeCheckRun(options.githubChecks.owner, options.githubChecks.repo, checkRun.id, checkName, failureResults, checkIssues);
|
|
1138
|
+
console.log(`✅ Completed ${checkName} check with ${checkIssues.length} issues`);
|
|
1139
|
+
}
|
|
1140
|
+
catch (error) {
|
|
1141
|
+
console.error(`❌ Failed to complete ${checkName} check: ${error}`);
|
|
1142
|
+
// Try to mark the check as failed due to execution error
|
|
1143
|
+
try {
|
|
1144
|
+
await this.githubCheckService.completeCheckRun(options.githubChecks.owner, options.githubChecks.repo, checkRun.id, checkName, [], [], error instanceof Error ? error.message : 'Unknown error occurred');
|
|
1145
|
+
}
|
|
1146
|
+
catch (finalError) {
|
|
1147
|
+
console.error(`❌ Failed to mark ${checkName} check as failed: ${finalError}`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Complete GitHub check runs with error status
|
|
1154
|
+
*/
|
|
1155
|
+
async completeGitHubChecksWithError(errorMessage) {
|
|
1156
|
+
if (!this.githubCheckService || !this.checkRunMap || !this.githubContext) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
console.log(`❌ Completing ${this.checkRunMap.size} GitHub check runs with error...`);
|
|
1160
|
+
for (const [checkName, checkRun] of this.checkRunMap) {
|
|
1161
|
+
try {
|
|
1162
|
+
await this.githubCheckService.completeCheckRun(this.githubContext.owner, this.githubContext.repo, checkRun.id, checkName, [], [], errorMessage);
|
|
1163
|
+
console.log(`❌ Completed ${checkName} check with error: ${errorMessage}`);
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
console.error(`❌ Failed to complete ${checkName} check with error: ${error}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
exports.CheckExecutionEngine = CheckExecutionEngine;
|
|
1172
|
+
//# sourceMappingURL=check-execution-engine.js.map
|