code-auditor-mcp 2.2.1 → 2.6.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/dist/auditRunner.d.ts +1 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +97 -46
- package/dist/auditRunner.js.map +1 -1
- package/dist/config/configLoader.d.ts +1 -1
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/mcp-standalone.js +177 -138
- package/dist/mcp-standalone.js.map +1 -1
- package/dist/mcp-tools/workflowGuide.d.ts.map +1 -1
- package/dist/mcp-tools/workflowGuide.js +79 -45
- package/dist/mcp-tools/workflowGuide.js.map +1 -1
- package/dist/mcp.js +198 -159
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts +28 -0
- package/dist/mcpAuditJobs.d.ts.map +1 -0
- package/dist/mcpAuditJobs.js +852 -0
- package/dist/mcpAuditJobs.js.map +1 -0
- package/dist/mcpDiagnostics.d.ts +4 -0
- package/dist/mcpDiagnostics.d.ts.map +1 -1
- package/dist/mcpDiagnostics.js +12 -1
- package/dist/mcpDiagnostics.js.map +1 -1
- package/dist/scripts/benchmarkWorkers.d.ts +2 -0
- package/dist/scripts/benchmarkWorkers.d.ts.map +1 -0
- package/dist/scripts/benchmarkWorkers.js +64 -0
- package/dist/scripts/benchmarkWorkers.js.map +1 -0
- package/dist/services/auditJobService.d.ts +28 -0
- package/dist/services/auditJobService.d.ts.map +1 -0
- package/dist/services/auditJobService.js +36 -0
- package/dist/services/auditJobService.js.map +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -1
- package/dist/workers/auditWorker.d.ts +2 -0
- package/dist/workers/auditWorker.d.ts.map +1 -0
- package/dist/workers/auditWorker.js +122 -0
- package/dist/workers/auditWorker.js.map +1 -0
- package/dist/workers/auditWorkerProtocol.d.ts +85 -0
- package/dist/workers/auditWorkerProtocol.d.ts.map +1 -0
- package/dist/workers/auditWorkerProtocol.js +32 -0
- package/dist/workers/auditWorkerProtocol.js.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { fork } from 'node:child_process';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { cpus } from 'node:os';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { CodeIndexDB } from './codeIndexDB.js';
|
|
8
|
+
import { syncFileIndex } from './codeIndexService.js';
|
|
9
|
+
import { CodeMapGenerator } from './services/CodeMapGenerator.js';
|
|
10
|
+
import { analyzeDocumentation } from './analyzers/documentationAnalyzer.js';
|
|
11
|
+
import { assertAuditPathExists, ContextualError } from './mcpToolErrors.js';
|
|
12
|
+
import { createAuditJob, getAuditJob, patchAuditJob, setAuditJobProgress } from './services/auditJobService.js';
|
|
13
|
+
import { mcpDebugStderr } from './mcpDiagnostics.js';
|
|
14
|
+
import { findFiles } from './utils/fileDiscovery.js';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
const SOURCE_FOLDERS = ['app', 'src'];
|
|
17
|
+
const GLOBAL_ONLY_ANALYZERS = new Set(['dry', 'data-access', 'schema']);
|
|
18
|
+
const RETRYABLE_ERROR_PATTERNS = [/timed out/i, /timeout/i, /econnreset/i, /eagain/i, /emfile/i];
|
|
19
|
+
/** Hard cap so pathological configs cannot fork unbounded processes. */
|
|
20
|
+
const MAX_AUDIT_WORKERS = 8;
|
|
21
|
+
const DEFAULT_JOB_TIMEOUT_MS = 30 * 60 * 1000;
|
|
22
|
+
const ABSOLUTE_MAX_JOB_TIMEOUT_MS = 4 * 60 * 60 * 1000;
|
|
23
|
+
const MIN_JOB_TIMEOUT_MS = 60 * 1000;
|
|
24
|
+
function defaultJobTimeoutMs() {
|
|
25
|
+
const raw = process.env.CODE_AUDITOR_JOB_TIMEOUT_MS;
|
|
26
|
+
if (!raw)
|
|
27
|
+
return DEFAULT_JOB_TIMEOUT_MS;
|
|
28
|
+
const n = Number(raw);
|
|
29
|
+
if (!Number.isFinite(n) || n < MIN_JOB_TIMEOUT_MS)
|
|
30
|
+
return DEFAULT_JOB_TIMEOUT_MS;
|
|
31
|
+
return Math.min(n, ABSOLUTE_MAX_JOB_TIMEOUT_MS);
|
|
32
|
+
}
|
|
33
|
+
function resolveWorkerEntrypoint() {
|
|
34
|
+
const current = fileURLToPath(import.meta.url);
|
|
35
|
+
const ext = path.extname(current);
|
|
36
|
+
const dir = path.dirname(current);
|
|
37
|
+
const filename = ext === '.ts' ? 'auditWorker.ts' : 'auditWorker.js';
|
|
38
|
+
return path.join(dir, 'workers', filename);
|
|
39
|
+
}
|
|
40
|
+
function asSerializableConfig(options) {
|
|
41
|
+
return {
|
|
42
|
+
projectRoot: options.projectRoot || process.cwd(),
|
|
43
|
+
includePaths: options.includePaths,
|
|
44
|
+
excludePaths: options.excludePaths,
|
|
45
|
+
fileExtensions: options.fileExtensions,
|
|
46
|
+
minSeverity: options.minSeverity,
|
|
47
|
+
enabledAnalyzers: options.enabledAnalyzers,
|
|
48
|
+
indexFunctions: options.indexFunctions,
|
|
49
|
+
analyzerConfigs: options.analyzerConfigs,
|
|
50
|
+
analyzerConcurrency: options.analyzerConcurrency,
|
|
51
|
+
explicitFiles: options.explicitFiles,
|
|
52
|
+
maxFilesPerRun: options.maxFilesPerRun,
|
|
53
|
+
shardSoftBudgetMs: options.shardSoftBudgetMs,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isRetryableShardError(error) {
|
|
57
|
+
return RETRYABLE_ERROR_PATTERNS.some((p) => p.test(error));
|
|
58
|
+
}
|
|
59
|
+
async function runShardTasksWithWorkerPool(jobId, tasks, options) {
|
|
60
|
+
if (tasks.length === 0)
|
|
61
|
+
return [];
|
|
62
|
+
const queue = [...tasks];
|
|
63
|
+
const completedResults = [];
|
|
64
|
+
const pending = new Map();
|
|
65
|
+
const workers = new Set();
|
|
66
|
+
let runningShards = 0;
|
|
67
|
+
let retryCount = 0;
|
|
68
|
+
let aborted = false;
|
|
69
|
+
let settled = false;
|
|
70
|
+
const spawnCount = Math.max(1, Math.min(options.maxWorkers, tasks.length));
|
|
71
|
+
const workerEntry = resolveWorkerEntrypoint();
|
|
72
|
+
const cleanupWorker = (worker) => {
|
|
73
|
+
workers.delete(worker);
|
|
74
|
+
try {
|
|
75
|
+
worker.removeAllListeners();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// ignore
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
if (worker.connected)
|
|
82
|
+
worker.disconnect();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// ignore
|
|
86
|
+
}
|
|
87
|
+
const safeKill = (signal) => {
|
|
88
|
+
try {
|
|
89
|
+
if (signal)
|
|
90
|
+
worker.kill(signal);
|
|
91
|
+
else
|
|
92
|
+
worker.kill();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// ignore
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (!worker.killed) {
|
|
99
|
+
safeKill('SIGTERM');
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
if (!worker.killed) {
|
|
102
|
+
safeKill('SIGKILL');
|
|
103
|
+
safeKill();
|
|
104
|
+
}
|
|
105
|
+
}, 750);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const post = (worker, message) => {
|
|
109
|
+
try {
|
|
110
|
+
if (worker.connected && !worker.killed) {
|
|
111
|
+
worker.send(message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
mcpDebugStderr(chalk.yellow('[WARN]'), 'Failed to send to audit worker (IPC):', e);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const disposeAllWorkers = () => {
|
|
119
|
+
for (const [rid, { worker, timer }] of [...pending.entries()]) {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
post(worker, { kind: 'cancel-request', requestId: rid });
|
|
122
|
+
}
|
|
123
|
+
pending.clear();
|
|
124
|
+
for (const w of [...workers]) {
|
|
125
|
+
cleanupWorker(w);
|
|
126
|
+
}
|
|
127
|
+
workers.clear();
|
|
128
|
+
};
|
|
129
|
+
return await new Promise((resolve, reject) => {
|
|
130
|
+
const finish = (ok, value) => {
|
|
131
|
+
if (settled)
|
|
132
|
+
return;
|
|
133
|
+
settled = true;
|
|
134
|
+
if (options.signal) {
|
|
135
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
136
|
+
}
|
|
137
|
+
disposeAllWorkers();
|
|
138
|
+
if (ok)
|
|
139
|
+
resolve(value);
|
|
140
|
+
else
|
|
141
|
+
reject(value);
|
|
142
|
+
};
|
|
143
|
+
const onAbort = () => {
|
|
144
|
+
if (aborted)
|
|
145
|
+
return;
|
|
146
|
+
aborted = true;
|
|
147
|
+
const reason = options.signal?.reason;
|
|
148
|
+
const msg = reason instanceof Error
|
|
149
|
+
? reason.message
|
|
150
|
+
: typeof reason === 'string'
|
|
151
|
+
? reason
|
|
152
|
+
: 'Audit job was cancelled or exceeded the maximum duration';
|
|
153
|
+
finish(false, new Error(msg));
|
|
154
|
+
};
|
|
155
|
+
if (options.signal?.aborted) {
|
|
156
|
+
onAbort();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
160
|
+
const progressTotal = () => Math.max(tasks.length, completedResults.length + queue.length + runningShards);
|
|
161
|
+
const maybeDispatch = () => {
|
|
162
|
+
if (aborted || settled)
|
|
163
|
+
return;
|
|
164
|
+
if (queue.length === 0 && runningShards === 0 && pending.size === 0) {
|
|
165
|
+
finish(true, completedResults);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const worker of [...workers]) {
|
|
169
|
+
const hasAssigned = [...pending.values()].some((p) => p.worker === worker);
|
|
170
|
+
if (hasAssigned)
|
|
171
|
+
continue;
|
|
172
|
+
const next = queue.shift();
|
|
173
|
+
if (!next)
|
|
174
|
+
continue;
|
|
175
|
+
runningShards++;
|
|
176
|
+
const requestId = randomUUID();
|
|
177
|
+
const timer = setTimeout(() => {
|
|
178
|
+
if (settled || aborted)
|
|
179
|
+
return;
|
|
180
|
+
const entry = pending.get(requestId);
|
|
181
|
+
if (!entry)
|
|
182
|
+
return;
|
|
183
|
+
clearTimeout(entry.timer);
|
|
184
|
+
pending.delete(requestId);
|
|
185
|
+
runningShards--;
|
|
186
|
+
const timedOutWorker = entry.worker;
|
|
187
|
+
post(timedOutWorker, { kind: 'cancel-request', requestId });
|
|
188
|
+
cleanupWorker(timedOutWorker);
|
|
189
|
+
if (!spawnOneWorker()) {
|
|
190
|
+
aborted = true;
|
|
191
|
+
finish(false, new Error(`Shard '${next.shardId}' timed out after ${options.shardTimeoutMs}ms and a replacement worker could not be started.`));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const msg = `Shard '${next.shardId}' timed out after ${options.shardTimeoutMs}ms`;
|
|
195
|
+
if (next.attempts < options.maxRetries) {
|
|
196
|
+
next.attempts += 1;
|
|
197
|
+
retryCount++;
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
queue.push(next);
|
|
200
|
+
setAuditJobProgress(jobId, {
|
|
201
|
+
phase: 'analysis',
|
|
202
|
+
message: `Retrying shard ${next.shardId} (${next.attempts}/${options.maxRetries}) after worker recycle`,
|
|
203
|
+
current: completedResults.length,
|
|
204
|
+
total: progressTotal(),
|
|
205
|
+
});
|
|
206
|
+
maybeDispatch();
|
|
207
|
+
}, options.retryBackoffMs * next.attempts);
|
|
208
|
+
maybeDispatch();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
aborted = true;
|
|
212
|
+
finish(false, new Error(`${msg}. Retries exhausted (${options.maxRetries}).`));
|
|
213
|
+
}, options.shardTimeoutMs);
|
|
214
|
+
pending.set(requestId, { worker, task: next, timer });
|
|
215
|
+
post(worker, {
|
|
216
|
+
kind: 'run-audit-shard',
|
|
217
|
+
requestId,
|
|
218
|
+
shardId: next.shardId,
|
|
219
|
+
config: next.config,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const workerEndHandled = new WeakSet();
|
|
224
|
+
const handleWorkerProcessEnd = (proc, code, signal, procErr) => {
|
|
225
|
+
if (settled || aborted)
|
|
226
|
+
return;
|
|
227
|
+
if (workerEndHandled.has(proc))
|
|
228
|
+
return;
|
|
229
|
+
workerEndHandled.add(proc);
|
|
230
|
+
const wasTracked = workers.has(proc);
|
|
231
|
+
if (wasTracked) {
|
|
232
|
+
workers.delete(proc);
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
proc.removeAllListeners();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// ignore
|
|
239
|
+
}
|
|
240
|
+
const detail = procErr
|
|
241
|
+
? `Worker process error: ${procErr.message}`
|
|
242
|
+
: `Worker exited (code=${code}, signal=${signal ?? 'none'})`;
|
|
243
|
+
const orphaned = [...pending.entries()].find(([, v]) => v.worker === proc);
|
|
244
|
+
if (orphaned) {
|
|
245
|
+
clearTimeout(orphaned[1].timer);
|
|
246
|
+
pending.delete(orphaned[0]);
|
|
247
|
+
runningShards--;
|
|
248
|
+
if (!spawnOneWorker()) {
|
|
249
|
+
aborted = true;
|
|
250
|
+
finish(false, new Error(`${detail}; could not spawn replacement worker`));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const task = orphaned[1].task;
|
|
254
|
+
if (task.attempts < options.maxRetries) {
|
|
255
|
+
task.attempts += 1;
|
|
256
|
+
retryCount++;
|
|
257
|
+
setTimeout(() => {
|
|
258
|
+
queue.push(task);
|
|
259
|
+
maybeDispatch();
|
|
260
|
+
}, options.retryBackoffMs * task.attempts);
|
|
261
|
+
maybeDispatch();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
aborted = true;
|
|
265
|
+
finish(false, new Error(`${detail} while running shard '${task.shardId}'`));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (wasTracked && (code !== 0 || signal || procErr)) {
|
|
269
|
+
if (!spawnOneWorker()) {
|
|
270
|
+
mcpDebugStderr(chalk.yellow('[WARN]'), 'Could not replenish audit worker after unexpected exit');
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
maybeDispatch();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const handleWorkerMessage = (worker, raw) => {
|
|
278
|
+
if (aborted || settled)
|
|
279
|
+
return;
|
|
280
|
+
const message = raw;
|
|
281
|
+
if (!message || typeof message !== 'object' || !('kind' in message))
|
|
282
|
+
return;
|
|
283
|
+
if (message.kind === 'worker-progress') {
|
|
284
|
+
const entry = pending.get(message.requestId);
|
|
285
|
+
if (!entry)
|
|
286
|
+
return;
|
|
287
|
+
const overallCurrent = completedResults.length +
|
|
288
|
+
Math.min(1, (message.progress.current ?? 0) / Math.max(1, message.progress.total ?? 1));
|
|
289
|
+
setAuditJobProgress(jobId, {
|
|
290
|
+
phase: message.progress.phase ?? 'analysis',
|
|
291
|
+
message: `${message.shardId}: ${message.progress.message ?? 'running'} (retries=${retryCount})`,
|
|
292
|
+
current: Math.floor(overallCurrent),
|
|
293
|
+
total: progressTotal(),
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (message.kind === 'worker-handoff') {
|
|
298
|
+
const entry = pending.get(message.requestId);
|
|
299
|
+
if (!entry)
|
|
300
|
+
return;
|
|
301
|
+
clearTimeout(entry.timer);
|
|
302
|
+
pending.delete(message.requestId);
|
|
303
|
+
runningShards--;
|
|
304
|
+
completedResults.push(message.partialResult);
|
|
305
|
+
queue.push({
|
|
306
|
+
shardId: `${entry.task.shardId}>cont`,
|
|
307
|
+
attempts: 0,
|
|
308
|
+
config: message.continuation,
|
|
309
|
+
});
|
|
310
|
+
setAuditJobProgress(jobId, {
|
|
311
|
+
phase: 'analysis',
|
|
312
|
+
message: `Chunk done for ${entry.task.shardId}; queued ${message.remainingFiles.length} remaining file(s) (retries=${retryCount})`,
|
|
313
|
+
current: completedResults.length,
|
|
314
|
+
total: progressTotal(),
|
|
315
|
+
});
|
|
316
|
+
maybeDispatch();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (message.kind === 'worker-result' || message.kind === 'worker-error') {
|
|
320
|
+
const entry = pending.get(message.requestId);
|
|
321
|
+
if (!entry)
|
|
322
|
+
return;
|
|
323
|
+
clearTimeout(entry.timer);
|
|
324
|
+
pending.delete(message.requestId);
|
|
325
|
+
runningShards--;
|
|
326
|
+
if (message.kind === 'worker-result') {
|
|
327
|
+
completedResults.push(message.result);
|
|
328
|
+
setAuditJobProgress(jobId, {
|
|
329
|
+
phase: 'analysis',
|
|
330
|
+
message: `Completed shard ${entry.task.shardId} (${completedResults.length} chunk(s), retries=${retryCount})`,
|
|
331
|
+
current: completedResults.length,
|
|
332
|
+
total: progressTotal(),
|
|
333
|
+
});
|
|
334
|
+
maybeDispatch();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const errText = message.error || `Shard '${entry.task.shardId}' failed`;
|
|
338
|
+
if (entry.task.attempts < options.maxRetries && isRetryableShardError(errText)) {
|
|
339
|
+
entry.task.attempts += 1;
|
|
340
|
+
retryCount++;
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
queue.push(entry.task);
|
|
343
|
+
maybeDispatch();
|
|
344
|
+
}, options.retryBackoffMs * entry.task.attempts);
|
|
345
|
+
maybeDispatch();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
aborted = true;
|
|
349
|
+
finish(false, new Error(`${errText}${message.stack ? `\n${message.stack}` : ''}`));
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const spawnOneWorker = () => {
|
|
353
|
+
try {
|
|
354
|
+
const proc = fork(workerEntry, [], {
|
|
355
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
356
|
+
});
|
|
357
|
+
workers.add(proc);
|
|
358
|
+
proc.on('message', (msg) => handleWorkerMessage(proc, msg));
|
|
359
|
+
proc.on('error', (err) => {
|
|
360
|
+
mcpDebugStderr(chalk.yellow('[WARN]'), 'Audit worker process error:', err);
|
|
361
|
+
handleWorkerProcessEnd(proc, null, null, err instanceof Error ? err : new Error(String(err)));
|
|
362
|
+
});
|
|
363
|
+
proc.on('exit', (code, signal) => {
|
|
364
|
+
handleWorkerProcessEnd(proc, code, signal);
|
|
365
|
+
});
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
mcpDebugStderr(chalk.red('[ERROR]'), 'fork() failed for audit worker:', e);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
for (let i = 0; i < spawnCount; i++) {
|
|
374
|
+
if (!spawnOneWorker()) {
|
|
375
|
+
aborted = true;
|
|
376
|
+
finish(false, new Error('Failed to start audit worker processes'));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
maybeDispatch();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function getAllViolations(result) {
|
|
384
|
+
const violations = [];
|
|
385
|
+
for (const [analyzerName, analyzerResult] of Object.entries(result.analyzerResults ?? {})) {
|
|
386
|
+
for (const violation of analyzerResult.violations) {
|
|
387
|
+
violations.push({
|
|
388
|
+
...violation,
|
|
389
|
+
analyzer: analyzerName,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return violations;
|
|
394
|
+
}
|
|
395
|
+
function calculateHealthScore(result) {
|
|
396
|
+
const filesAnalyzed = result.metadata?.filesAnalyzed || 1;
|
|
397
|
+
const critical = result.summary?.criticalIssues || 0;
|
|
398
|
+
const warnings = result.summary?.warnings || 0;
|
|
399
|
+
const suggestions = result.summary?.suggestions || 0;
|
|
400
|
+
const weightedViolations = critical * 10 + warnings * 3 + suggestions * 0.5;
|
|
401
|
+
let score = 100 - (weightedViolations / filesAnalyzed) * 2;
|
|
402
|
+
return Math.max(0, Math.round(Math.min(100, score)));
|
|
403
|
+
}
|
|
404
|
+
function summarizeAnalyzerResults(analyzerResults) {
|
|
405
|
+
let totalViolations = 0;
|
|
406
|
+
let criticalIssues = 0;
|
|
407
|
+
let warnings = 0;
|
|
408
|
+
let suggestions = 0;
|
|
409
|
+
const violationsByCategory = {};
|
|
410
|
+
for (const [analyzer, result] of Object.entries(analyzerResults)) {
|
|
411
|
+
for (const violation of result.violations) {
|
|
412
|
+
totalViolations++;
|
|
413
|
+
if (violation.severity === 'critical')
|
|
414
|
+
criticalIssues++;
|
|
415
|
+
else if (violation.severity === 'warning')
|
|
416
|
+
warnings++;
|
|
417
|
+
else
|
|
418
|
+
suggestions++;
|
|
419
|
+
const category = violation.type || analyzer;
|
|
420
|
+
violationsByCategory[category] = (violationsByCategory[category] || 0) + 1;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
totalFiles: 0,
|
|
425
|
+
totalViolations,
|
|
426
|
+
criticalIssues,
|
|
427
|
+
warnings,
|
|
428
|
+
suggestions,
|
|
429
|
+
violationsByCategory,
|
|
430
|
+
topIssues: [],
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function mergeAnalyzerResult(base, next) {
|
|
434
|
+
if (!base)
|
|
435
|
+
return { ...next, violations: [...next.violations], errors: [...(next.errors || [])] };
|
|
436
|
+
const dedupeKey = (v) => `${v.file ?? ''}:${v.line ?? ''}:${v.column ?? ''}:${v.rule ?? ''}:${v.message ?? ''}:${v.severity ?? ''}`;
|
|
437
|
+
const seen = new Set(base.violations.map(dedupeKey));
|
|
438
|
+
const mergedViolations = [...base.violations];
|
|
439
|
+
for (const v of next.violations) {
|
|
440
|
+
const key = dedupeKey(v);
|
|
441
|
+
if (!seen.has(key)) {
|
|
442
|
+
seen.add(key);
|
|
443
|
+
mergedViolations.push(v);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
...base,
|
|
448
|
+
violations: mergedViolations,
|
|
449
|
+
filesProcessed: (base.filesProcessed || 0) + (next.filesProcessed || 0),
|
|
450
|
+
executionTime: (base.executionTime || 0) + (next.executionTime || 0),
|
|
451
|
+
errors: [...(base.errors || []), ...(next.errors || [])],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function mergeAuditResults(results, orderedAnalyzers) {
|
|
455
|
+
const analyzerResults = {};
|
|
456
|
+
const fileToFunctionsMap = {};
|
|
457
|
+
const collectedFunctions = [];
|
|
458
|
+
const recommendations = [];
|
|
459
|
+
let filesAnalyzed = 0;
|
|
460
|
+
let auditDuration = 0;
|
|
461
|
+
for (const result of results) {
|
|
462
|
+
for (const [analyzerName, analyzerResult] of Object.entries(result.analyzerResults || {})) {
|
|
463
|
+
analyzerResults[analyzerName] = mergeAnalyzerResult(analyzerResults[analyzerName], analyzerResult);
|
|
464
|
+
}
|
|
465
|
+
for (const [fp, funcs] of Object.entries(result.metadata?.fileToFunctionsMap || {})) {
|
|
466
|
+
fileToFunctionsMap[fp] = funcs;
|
|
467
|
+
}
|
|
468
|
+
if (result.metadata?.collectedFunctions) {
|
|
469
|
+
collectedFunctions.push(...result.metadata.collectedFunctions);
|
|
470
|
+
}
|
|
471
|
+
filesAnalyzed += result.metadata?.filesAnalyzed || 0;
|
|
472
|
+
auditDuration += result.metadata?.auditDuration || 0;
|
|
473
|
+
if (result.recommendations?.length)
|
|
474
|
+
recommendations.push(...result.recommendations);
|
|
475
|
+
}
|
|
476
|
+
const ordered = {};
|
|
477
|
+
for (const name of orderedAnalyzers) {
|
|
478
|
+
if (analyzerResults[name])
|
|
479
|
+
ordered[name] = analyzerResults[name];
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
timestamp: new Date(),
|
|
483
|
+
summary: summarizeAnalyzerResults(ordered),
|
|
484
|
+
analyzerResults: ordered,
|
|
485
|
+
recommendations,
|
|
486
|
+
metadata: {
|
|
487
|
+
auditDuration,
|
|
488
|
+
filesAnalyzed,
|
|
489
|
+
analyzersRun: orderedAnalyzers,
|
|
490
|
+
...(collectedFunctions.length > 0 && { collectedFunctions }),
|
|
491
|
+
...(Object.keys(fileToFunctionsMap).length > 0 && { fileToFunctionsMap }),
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
export async function derivePartitionPlan(args, projectRoot, isFile, enabledAnalyzers) {
|
|
496
|
+
const strategy = (args.partitionStrategy || 'auto');
|
|
497
|
+
if (isFile || strategy === 'none') {
|
|
498
|
+
return { mode: 'none', partitionPaths: [], globalAnalyzers: enabledAnalyzers, shardedAnalyzers: [] };
|
|
499
|
+
}
|
|
500
|
+
const allFiles = await findFiles(projectRoot);
|
|
501
|
+
const threshold = Math.max(1, Number(args.partitionThresholdFiles) || 250);
|
|
502
|
+
if (strategy === 'auto' && allFiles.length < threshold) {
|
|
503
|
+
return { mode: 'none', partitionPaths: [], globalAnalyzers: enabledAnalyzers, shardedAnalyzers: [] };
|
|
504
|
+
}
|
|
505
|
+
const byTop = new Map();
|
|
506
|
+
for (const file of allFiles) {
|
|
507
|
+
const rel = path.relative(projectRoot, file);
|
|
508
|
+
if (!rel || rel.startsWith('..'))
|
|
509
|
+
continue;
|
|
510
|
+
const seg = rel.split(path.sep)[0];
|
|
511
|
+
byTop.set(seg, (byTop.get(seg) || 0) + 1);
|
|
512
|
+
}
|
|
513
|
+
const preferred = SOURCE_FOLDERS.filter((name) => byTop.has(name));
|
|
514
|
+
const others = [...byTop.entries()]
|
|
515
|
+
.filter(([name]) => !preferred.includes(name))
|
|
516
|
+
.sort((a, b) => b[1] - a[1])
|
|
517
|
+
.map(([name]) => name);
|
|
518
|
+
const maxPartitions = Math.max(1, Number(args.maxPartitions) || 4);
|
|
519
|
+
let selected = [...preferred, ...others].slice(0, maxPartitions);
|
|
520
|
+
if (selected.length < 2 && preferred.length > 0) {
|
|
521
|
+
const focus = preferred[0];
|
|
522
|
+
const focusDir = path.join(projectRoot, focus);
|
|
523
|
+
try {
|
|
524
|
+
const entries = await fs.readdir(focusDir, { withFileTypes: true });
|
|
525
|
+
const subdirs = entries
|
|
526
|
+
.filter((e) => e.isDirectory())
|
|
527
|
+
.map((e) => path.join(focus, e.name))
|
|
528
|
+
.slice(0, maxPartitions);
|
|
529
|
+
if (subdirs.length >= 2) {
|
|
530
|
+
selected = subdirs;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// Ignore fallback partitioning errors
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const shardedAnalyzers = enabledAnalyzers.filter((a) => !GLOBAL_ONLY_ANALYZERS.has(a));
|
|
538
|
+
const globalAnalyzers = enabledAnalyzers.filter((a) => GLOBAL_ONLY_ANALYZERS.has(a));
|
|
539
|
+
if (selected.length < 2 || shardedAnalyzers.length === 0) {
|
|
540
|
+
return { mode: 'none', partitionPaths: [], globalAnalyzers: enabledAnalyzers, shardedAnalyzers: [] };
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
mode: 'top-level',
|
|
544
|
+
partitionPaths: selected.map((seg) => path.join(projectRoot, seg)),
|
|
545
|
+
globalAnalyzers,
|
|
546
|
+
shardedAnalyzers,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
export async function startAuditJob(args, defaults) {
|
|
550
|
+
const auditPath = path.resolve(args.path || process.cwd());
|
|
551
|
+
await assertAuditPathExists(auditPath);
|
|
552
|
+
const job = createAuditJob(auditPath);
|
|
553
|
+
setTimeout(() => {
|
|
554
|
+
void runAuditJob(job.jobId, args, defaults).catch((err) => {
|
|
555
|
+
try {
|
|
556
|
+
patchAuditJob(job.jobId, {
|
|
557
|
+
status: 'failed',
|
|
558
|
+
finishedAt: new Date().toISOString(),
|
|
559
|
+
error: err instanceof Error ? err.message : String(err),
|
|
560
|
+
progress: { phase: 'failed', message: 'Audit failed' },
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// ignore secondary failures — never let an audit rejection crash the MCP process
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}, 0);
|
|
568
|
+
return {
|
|
569
|
+
jobId: job.jobId,
|
|
570
|
+
status: 'queued',
|
|
571
|
+
path: auditPath,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async function runAuditJob(jobId, args, defaults) {
|
|
575
|
+
let jobTimer;
|
|
576
|
+
const ac = new AbortController();
|
|
577
|
+
try {
|
|
578
|
+
patchAuditJob(jobId, {
|
|
579
|
+
status: 'running',
|
|
580
|
+
startedAt: new Date().toISOString(),
|
|
581
|
+
progress: {
|
|
582
|
+
phase: 'queued',
|
|
583
|
+
message: 'Audit queued',
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
const auditPath = path.resolve(args.path || process.cwd());
|
|
587
|
+
const indexFunctions = args.indexFunctions !== false;
|
|
588
|
+
const generateCodeMap = args.generateCodeMap ?? defaults.defaultGenerateCodeMap;
|
|
589
|
+
const jobTimeoutMs = Math.min(ABSOLUTE_MAX_JOB_TIMEOUT_MS, Math.max(MIN_JOB_TIMEOUT_MS, Number(args.jobTimeoutMs) || defaultJobTimeoutMs()));
|
|
590
|
+
jobTimer = setTimeout(() => {
|
|
591
|
+
try {
|
|
592
|
+
ac.abort(new Error(`Audit job exceeded maximum duration (${jobTimeoutMs}ms)`));
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
ac.abort();
|
|
596
|
+
}
|
|
597
|
+
}, jobTimeoutMs);
|
|
598
|
+
const { isFile } = await assertAuditPathExists(auditPath);
|
|
599
|
+
const db = CodeIndexDB.getInstance();
|
|
600
|
+
await db.initialize();
|
|
601
|
+
const storedConfigs = await db.getAllAnalyzerConfigs(auditPath);
|
|
602
|
+
const analyzerConfigs = {
|
|
603
|
+
...storedConfigs,
|
|
604
|
+
...(args.analyzerConfigs || {}),
|
|
605
|
+
};
|
|
606
|
+
const projectRoot = isFile ? path.dirname(auditPath) : auditPath;
|
|
607
|
+
const enabledAnalyzers = args.analyzers || defaults.defaultAnalyzers;
|
|
608
|
+
const maxWorkers = Math.max(1, Math.min(MAX_AUDIT_WORKERS, Number(args.workerCount) || Math.max(1, Math.min(4, cpus().length - 1 || 1)), Number(args.maxPartitions) || 4));
|
|
609
|
+
const maxRetries = Math.max(0, Number(args.maxRetries) || 1);
|
|
610
|
+
const shardTimeoutMs = Math.max(5_000, Number(args.shardTimeoutMs) || 180_000);
|
|
611
|
+
const retryBackoffMs = Math.max(100, Number(args.retryBackoffMs) || 500);
|
|
612
|
+
const maxFilesPerRun = typeof args.maxFilesPerRun === 'number' && args.maxFilesPerRun > 0
|
|
613
|
+
? Math.floor(args.maxFilesPerRun)
|
|
614
|
+
: undefined;
|
|
615
|
+
const shardSoftBudgetMs = typeof args.shardSoftBudgetMs === 'number' && args.shardSoftBudgetMs > 0
|
|
616
|
+
? Math.max(1_000, Math.floor(args.shardSoftBudgetMs))
|
|
617
|
+
: undefined;
|
|
618
|
+
const baseOptions = {
|
|
619
|
+
projectRoot,
|
|
620
|
+
enabledAnalyzers,
|
|
621
|
+
minSeverity: (args.minSeverity || defaults.defaultMinSeverity),
|
|
622
|
+
verbose: false,
|
|
623
|
+
indexFunctions,
|
|
624
|
+
analyzerConcurrency: typeof args.analyzerConcurrency === 'number'
|
|
625
|
+
? Math.max(1, Math.floor(args.analyzerConcurrency))
|
|
626
|
+
: undefined,
|
|
627
|
+
...(maxFilesPerRun !== undefined && { maxFilesPerRun }),
|
|
628
|
+
...(shardSoftBudgetMs !== undefined && { shardSoftBudgetMs }),
|
|
629
|
+
...(isFile && { includePaths: [auditPath] }),
|
|
630
|
+
...(Object.keys(analyzerConfigs).length > 0 && { analyzerConfigs }),
|
|
631
|
+
progressCallback: (p) => {
|
|
632
|
+
setAuditJobProgress(jobId, {
|
|
633
|
+
phase: p.phase ?? 'analysis',
|
|
634
|
+
message: p.message ?? p.phase ?? 'running',
|
|
635
|
+
current: typeof p.current === 'number' ? p.current : undefined,
|
|
636
|
+
total: typeof p.total === 'number' ? p.total : undefined,
|
|
637
|
+
});
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
const plan = await derivePartitionPlan(args, projectRoot, isFile, enabledAnalyzers);
|
|
641
|
+
const shardTasks = [];
|
|
642
|
+
if (plan.mode === 'none') {
|
|
643
|
+
shardTasks.push({
|
|
644
|
+
shardId: 'full-scope',
|
|
645
|
+
attempts: 0,
|
|
646
|
+
config: asSerializableConfig(baseOptions),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
setAuditJobProgress(jobId, {
|
|
651
|
+
phase: 'partitioning',
|
|
652
|
+
message: `Planning ${plan.partitionPaths.length} shard(s) + ${plan.globalAnalyzers.length > 0 ? 'global' : 'no-global'} analyzers`,
|
|
653
|
+
});
|
|
654
|
+
if (plan.globalAnalyzers.length > 0) {
|
|
655
|
+
shardTasks.push({
|
|
656
|
+
shardId: 'global-analyzers',
|
|
657
|
+
attempts: 0,
|
|
658
|
+
config: asSerializableConfig({
|
|
659
|
+
...baseOptions,
|
|
660
|
+
enabledAnalyzers: plan.globalAnalyzers,
|
|
661
|
+
includePaths: undefined,
|
|
662
|
+
}),
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
for (const partitionPath of plan.partitionPaths) {
|
|
666
|
+
shardTasks.push({
|
|
667
|
+
shardId: `shard:${path.basename(partitionPath)}`,
|
|
668
|
+
attempts: 0,
|
|
669
|
+
config: asSerializableConfig({
|
|
670
|
+
...baseOptions,
|
|
671
|
+
enabledAnalyzers: plan.shardedAnalyzers,
|
|
672
|
+
includePaths: [`${partitionPath}/**/*`],
|
|
673
|
+
}),
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
setAuditJobProgress(jobId, {
|
|
678
|
+
phase: 'analysis',
|
|
679
|
+
message: `Running ${shardTasks.length} shard task(s) with ${maxWorkers} worker(s)`,
|
|
680
|
+
current: 0,
|
|
681
|
+
total: shardTasks.length,
|
|
682
|
+
});
|
|
683
|
+
const resultParts = await runShardTasksWithWorkerPool(jobId, shardTasks, {
|
|
684
|
+
maxWorkers,
|
|
685
|
+
maxRetries,
|
|
686
|
+
shardTimeoutMs,
|
|
687
|
+
retryBackoffMs,
|
|
688
|
+
signal: ac.signal,
|
|
689
|
+
});
|
|
690
|
+
if (ac.signal.aborted) {
|
|
691
|
+
throw ac.signal.reason instanceof Error
|
|
692
|
+
? ac.signal.reason
|
|
693
|
+
: new Error(String(ac.signal.reason || 'Audit job was cancelled or timed out'));
|
|
694
|
+
}
|
|
695
|
+
const auditResult = resultParts.length === 1 ? resultParts[0] : mergeAuditResults(resultParts, enabledAnalyzers);
|
|
696
|
+
let indexingResult = null;
|
|
697
|
+
if (indexFunctions && auditResult.metadata.fileToFunctionsMap) {
|
|
698
|
+
const syncStats = { added: 0, updated: 0, removed: 0 };
|
|
699
|
+
for (const [filePath, functions] of Object.entries(auditResult.metadata.fileToFunctionsMap)) {
|
|
700
|
+
if (ac.signal.aborted) {
|
|
701
|
+
throw ac.signal.reason instanceof Error
|
|
702
|
+
? ac.signal.reason
|
|
703
|
+
: new Error(String(ac.signal.reason || 'Audit job was cancelled during indexing'));
|
|
704
|
+
}
|
|
705
|
+
const fileStats = await syncFileIndex(filePath, functions);
|
|
706
|
+
syncStats.added += fileStats.added;
|
|
707
|
+
syncStats.updated += fileStats.updated;
|
|
708
|
+
syncStats.removed += fileStats.removed;
|
|
709
|
+
}
|
|
710
|
+
indexingResult = {
|
|
711
|
+
success: true,
|
|
712
|
+
registered: syncStats.added + syncStats.updated,
|
|
713
|
+
failed: 0,
|
|
714
|
+
syncStats,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
let codeMapResult = null;
|
|
718
|
+
if (generateCodeMap && indexingResult && indexingResult.success) {
|
|
719
|
+
if (ac.signal.aborted) {
|
|
720
|
+
throw ac.signal.reason instanceof Error
|
|
721
|
+
? ac.signal.reason
|
|
722
|
+
: new Error(String(ac.signal.reason || 'Audit job was cancelled before code map generation'));
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const mapGenerator = new CodeMapGenerator();
|
|
726
|
+
const files = Object.keys(auditResult.metadata.fileToFunctionsMap || {});
|
|
727
|
+
let documentation = undefined;
|
|
728
|
+
if (files.length > 0) {
|
|
729
|
+
const docResult = await analyzeDocumentation(files);
|
|
730
|
+
documentation = docResult.metrics;
|
|
731
|
+
}
|
|
732
|
+
const paginatedResult = await mapGenerator.generatePaginatedCodeMap(isFile ? path.dirname(auditPath) : auditPath, {
|
|
733
|
+
includeComplexity: true,
|
|
734
|
+
includeDocumentation: !!documentation,
|
|
735
|
+
includeDependencies: true,
|
|
736
|
+
includeUsage: false,
|
|
737
|
+
groupByDirectory: true,
|
|
738
|
+
maxDepth: 10,
|
|
739
|
+
showUnusedImports: true,
|
|
740
|
+
minComplexity: 7,
|
|
741
|
+
});
|
|
742
|
+
codeMapResult = {
|
|
743
|
+
success: true,
|
|
744
|
+
mapId: paginatedResult.mapId,
|
|
745
|
+
summary: paginatedResult.summary,
|
|
746
|
+
quickPreview: paginatedResult.quickPreview,
|
|
747
|
+
sections: paginatedResult.summary.sectionsAvailable,
|
|
748
|
+
documentationCoverage: documentation?.coverageScore,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
catch (e) {
|
|
752
|
+
mcpDebugStderr(chalk.yellow('[WARN]'), 'Code map generation failed in background audit:', e);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const projectRootForStore = projectRoot;
|
|
756
|
+
const persisted = {
|
|
757
|
+
...auditResult,
|
|
758
|
+
...(indexingResult && { functionIndexing: indexingResult }),
|
|
759
|
+
...(codeMapResult && { codeMap: codeMapResult }),
|
|
760
|
+
};
|
|
761
|
+
const resultId = await db.storeAuditResults(persisted, projectRootForStore);
|
|
762
|
+
patchAuditJob(jobId, {
|
|
763
|
+
status: 'completed',
|
|
764
|
+
finishedAt: new Date().toISOString(),
|
|
765
|
+
progress: { phase: 'completed', message: 'Audit completed' },
|
|
766
|
+
resultId,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
catch (e) {
|
|
770
|
+
patchAuditJob(jobId, {
|
|
771
|
+
status: 'failed',
|
|
772
|
+
finishedAt: new Date().toISOString(),
|
|
773
|
+
error: e instanceof Error ? e.message : String(e),
|
|
774
|
+
progress: { phase: 'failed', message: 'Audit failed' },
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
finally {
|
|
778
|
+
if (jobTimer !== undefined) {
|
|
779
|
+
clearTimeout(jobTimer);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
export function getAuditJobStatus(jobId) {
|
|
784
|
+
const job = getAuditJob(jobId);
|
|
785
|
+
if (!job) {
|
|
786
|
+
throw new ContextualError(`Audit job not found: ${jobId}`, {
|
|
787
|
+
jobId,
|
|
788
|
+
hint: 'Use start_audit first, then poll audit_status with the returned jobId.',
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
jobId: job.jobId,
|
|
793
|
+
status: job.status,
|
|
794
|
+
path: job.path,
|
|
795
|
+
createdAt: job.createdAt,
|
|
796
|
+
startedAt: job.startedAt ?? null,
|
|
797
|
+
finishedAt: job.finishedAt ?? null,
|
|
798
|
+
progress: job.progress ?? null,
|
|
799
|
+
resultId: job.resultId ?? null,
|
|
800
|
+
error: job.error ?? null,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
export async function getAuditResultsPage(args) {
|
|
804
|
+
const resultId = args.resultId || args.auditId;
|
|
805
|
+
if (!resultId) {
|
|
806
|
+
throw new ContextualError('resultId is required to fetch audit results.', {
|
|
807
|
+
hint: 'Call start_audit, poll audit_status until completed, then pass resultId to audit_results.',
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
const limit = Math.min(Math.max(0, Number(args.limit)) || 50, 100);
|
|
811
|
+
const offset = Math.max(0, Number(args.offset) || 0);
|
|
812
|
+
const db = CodeIndexDB.getInstance();
|
|
813
|
+
await db.initialize();
|
|
814
|
+
const auditResult = await db.getAuditResults(resultId);
|
|
815
|
+
if (!auditResult) {
|
|
816
|
+
throw new ContextualError(`Audit result not found or expired: ${resultId}`, {
|
|
817
|
+
resultId,
|
|
818
|
+
hint: 'Results expire after 24h. Start a new audit if the result is no longer available.',
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
const allViolations = auditResult.violations || getAllViolations(auditResult);
|
|
822
|
+
const paginatedViolations = allViolations.slice(offset, offset + limit);
|
|
823
|
+
return {
|
|
824
|
+
summary: {
|
|
825
|
+
totalViolations: auditResult.summary?.totalViolations ?? allViolations.length,
|
|
826
|
+
criticalIssues: auditResult.summary?.criticalIssues ?? 0,
|
|
827
|
+
warnings: auditResult.summary?.warnings ?? 0,
|
|
828
|
+
suggestions: auditResult.summary?.suggestions ?? 0,
|
|
829
|
+
filesAnalyzed: auditResult.metadata?.filesAnalyzed ?? 0,
|
|
830
|
+
executionTime: auditResult.metadata?.auditDuration ?? 0,
|
|
831
|
+
healthScore: auditResult.summary?.healthScore ?? calculateHealthScore(auditResult),
|
|
832
|
+
},
|
|
833
|
+
violations: paginatedViolations,
|
|
834
|
+
pagination: {
|
|
835
|
+
total: allViolations.length,
|
|
836
|
+
limit,
|
|
837
|
+
offset,
|
|
838
|
+
hasMore: offset + limit < allViolations.length,
|
|
839
|
+
nextOffset: offset + limit < allViolations.length ? offset + limit : null,
|
|
840
|
+
resultId,
|
|
841
|
+
cachedPage: true,
|
|
842
|
+
},
|
|
843
|
+
recommendations: auditResult.recommendations || [],
|
|
844
|
+
...(auditResult.functionIndexing && { functionIndexing: auditResult.functionIndexing }),
|
|
845
|
+
...(auditResult.codeMap && { codeMap: auditResult.codeMap }),
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
export const __testables = {
|
|
849
|
+
isRetryableShardError,
|
|
850
|
+
mergeAnalyzerResult,
|
|
851
|
+
};
|
|
852
|
+
//# sourceMappingURL=mcpAuditJobs.js.map
|