code-auditor-mcp 3.4.13 → 3.4.15
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +96 -0
- package/dist/analyzers/applicability.d.ts +41 -0
- package/dist/analyzers/applicability.d.ts.map +1 -0
- package/dist/analyzers/applicability.js +96 -0
- package/dist/analyzers/applicability.js.map +1 -0
- package/dist/analyzers/ruleRegistry.js +11 -11
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +8 -8
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +2 -2
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +2 -2
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +9 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/migrations.d.ts +11 -0
- package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/migrations.js +127 -4
- package/dist/analyzers/universal/schema/migrations.js.map +1 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +62 -23
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +393 -1
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts +40 -1
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +222 -50
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/config/defaults.js +2 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/pathProfiles.d.ts +7 -0
- package/dist/config/pathProfiles.d.ts.map +1 -1
- package/dist/config/pathProfiles.js +7 -1
- package/dist/config/pathProfiles.js.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/ledger.d.ts +163 -2
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +528 -7
- package/dist/ledger.js.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts +24 -2
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +364 -58
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/pipeline.d.ts +9 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +165 -18
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +45 -10
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +6 -1
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/ruleAliases.d.ts.map +1 -1
- package/dist/ruleAliases.js +23 -0
- package/dist/ruleAliases.js.map +1 -1
- package/dist/scripts/benchmarkWorkers.js +1 -1
- package/dist/scripts/benchmarkWorkers.js.map +1 -1
- package/dist/services/auditJobService.d.ts +22 -5
- package/dist/services/auditJobService.d.ts.map +1 -1
- package/dist/services/auditJobService.js +67 -26
- package/dist/services/auditJobService.js.map +1 -1
- package/dist/services/fileAccounting.d.ts +83 -0
- package/dist/services/fileAccounting.d.ts.map +1 -0
- package/dist/services/fileAccounting.js +0 -0
- package/dist/services/fileAccounting.js.map +1 -0
- package/dist/styles/styleExtractor.d.ts +2 -2
- package/dist/styles/styleExtractor.d.ts.map +1 -1
- package/dist/styles/styleExtractor.js +83 -11
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts +14 -0
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +117 -16
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindProbe.d.ts +31 -0
- package/dist/styles/tailwindProbe.d.ts.map +1 -1
- package/dist/styles/tailwindProbe.js +145 -4
- package/dist/styles/tailwindProbe.js.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts +3 -0
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js +35 -1
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/styles/types.d.ts +11 -0
- package/dist/styles/types.d.ts.map +1 -1
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +71 -0
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +201 -25
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/workers/auditJobRunner.d.ts +2 -0
- package/dist/workers/auditJobRunner.d.ts.map +1 -0
- package/dist/workers/auditJobRunner.js +37 -0
- package/dist/workers/auditJobRunner.js.map +1 -0
- package/dist/workers/auditWorker.js +26 -0
- package/dist/workers/auditWorker.js.map +1 -1
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
package/dist/mcpAuditJobs.js
CHANGED
|
@@ -2,20 +2,45 @@ import path from 'node:path';
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import { fork } from 'node:child_process';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { cpus } from 'node:os';
|
|
5
|
+
import { cpus, hostname } from 'node:os';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { CodeIndexDB } from './codeIndexDB.js';
|
|
8
|
-
import { syncFileIndex } from './codeIndexService.js';
|
|
9
8
|
import { CodeMapGenerator } from './services/CodeMapGenerator.js';
|
|
10
9
|
import { analyzeDocumentation } from './analyzers/documentationAnalyzer.js';
|
|
11
10
|
import { assertAuditPathExists, ContextualError } from './mcpToolErrors.js';
|
|
12
11
|
import { createAuditJob, getAuditJob, patchAuditJob, setAuditJobProgress } from './services/auditJobService.js';
|
|
12
|
+
import { detectRunInput, evaluateStaleRunning, hashFileSet, markRunsFailed, patchLedgerRun, writeAuditToLedger, } from './ledger.js';
|
|
13
|
+
import { PACKAGE_VERSION } from './constants.js';
|
|
13
14
|
import { mcpDebugStderr } from './mcpDiagnostics.js';
|
|
14
15
|
import { findFiles } from './utils/fileDiscovery.js';
|
|
15
|
-
import { makeVisitorStatus, getFilesProcessed } from './pipeline.js';
|
|
16
|
+
import { makeVisitorStatus, getFilesProcessed, violationMatchesRule } from './pipeline.js';
|
|
17
|
+
import { RULE_REGISTRY } from './analyzers/ruleRegistry.js';
|
|
16
18
|
import chalk from 'chalk';
|
|
17
19
|
const SOURCE_FOLDERS = ['app', 'src'];
|
|
18
|
-
|
|
20
|
+
// Cross-file reducers run once over the FULL scope, never inside partition
|
|
21
|
+
// shards. A shard sees only its partition's files and (worse) reads shared
|
|
22
|
+
// index tables that other shards are still writing, so any reducer that reads
|
|
23
|
+
// accumulated state must be global. The set is every reducer/derived-reducer
|
|
24
|
+
// in the pipeline stage model:
|
|
25
|
+
// dry — cross-file duplicate detection over the function index
|
|
26
|
+
// data-access — per-file, but kept global so its coverage/status is a single
|
|
27
|
+
// full-scope row (harmless to shard, but global is uniform)
|
|
28
|
+
// schema — schema_usage reducers (unknown-table, JSON validation)
|
|
29
|
+
// styles — reads style_declarations/tokens/class_usage written by the
|
|
30
|
+
// styles-css visitor + syncStyleIndex (same analyzer gate)
|
|
31
|
+
// conventions — reads function_calls/conventions via updateDependencyGraph +
|
|
32
|
+
// mineAllConventions (same analyzer gate)
|
|
33
|
+
// invariants — call-constraint/module-boundary need the full file list
|
|
34
|
+
// cross-domain— reads schema_usage/indexed_functions/graph_cache (stage 4)
|
|
35
|
+
const GLOBAL_ONLY_ANALYZERS = new Set([
|
|
36
|
+
'dry',
|
|
37
|
+
'data-access',
|
|
38
|
+
'schema',
|
|
39
|
+
'styles',
|
|
40
|
+
'conventions',
|
|
41
|
+
'invariants',
|
|
42
|
+
'cross-domain',
|
|
43
|
+
]);
|
|
19
44
|
const RETRYABLE_ERROR_PATTERNS = [/timed out/i, /timeout/i, /econnreset/i, /eagain/i, /emfile/i];
|
|
20
45
|
/** Hard cap so pathological configs cannot fork unbounded processes. */
|
|
21
46
|
const MAX_AUDIT_WORKERS = 8;
|
|
@@ -31,6 +56,100 @@ function defaultJobTimeoutMs() {
|
|
|
31
56
|
return DEFAULT_JOB_TIMEOUT_MS;
|
|
32
57
|
return Math.min(n, ABSOLUTE_MAX_JOB_TIMEOUT_MS);
|
|
33
58
|
}
|
|
59
|
+
// Spec 41 R5 — heartbeat-based concurrency lease. A plain `status='running'`
|
|
60
|
+
// count wedges the queue behind a crashed job's ghost row forever; a stale
|
|
61
|
+
// heartbeat is reclaimable instead.
|
|
62
|
+
const DEFAULT_JOB_LEASE_TTL_MS = 30 * 1000;
|
|
63
|
+
const DEFAULT_MAX_RUNNING_JOBS = 1;
|
|
64
|
+
function jobLeaseTtlMs() {
|
|
65
|
+
const raw = process.env.CODE_AUDITOR_JOB_LEASE_TTL_MS;
|
|
66
|
+
const n = Number(raw);
|
|
67
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_JOB_LEASE_TTL_MS;
|
|
68
|
+
}
|
|
69
|
+
function maxRunningJobs() {
|
|
70
|
+
const raw = process.env.CODE_AUDITOR_MAX_RUNNING_JOBS;
|
|
71
|
+
const n = Number(raw);
|
|
72
|
+
return Number.isFinite(n) && n >= 1 ? n : DEFAULT_MAX_RUNNING_JOBS;
|
|
73
|
+
}
|
|
74
|
+
/** True when a write hit better-sqlite3's busy timeout under lock contention. */
|
|
75
|
+
function isBusyError(e) {
|
|
76
|
+
if (!(e instanceof Error))
|
|
77
|
+
return false;
|
|
78
|
+
const code = e.code;
|
|
79
|
+
return code === 'SQLITE_BUSY' || /database is locked/i.test(e.message);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Claims the `running` slot for `jobId`. The reclaim-then-count-then-claim
|
|
83
|
+
* sequence runs under `BEGIN IMMEDIATE` because the lease is contested by
|
|
84
|
+
* separate forked processes (better-sqlite3 serializes writes per-connection,
|
|
85
|
+
* not per-DB) — without the write lock two children could both see zero
|
|
86
|
+
* `running` rows and both claim the slot. Stale `running` rows (killed runners)
|
|
87
|
+
* are reclaimed first, so a ghost never wedges the queue.
|
|
88
|
+
*
|
|
89
|
+
* The polling path is deliberately read-only: while a healthy runner holds the
|
|
90
|
+
* slot, a queued job evaluates the lease pool via `evaluateStaleRunning` (SELECT
|
|
91
|
+
* + `kill(0)`/`ps`, no write lock) and sleeps. It only grabs `BEGIN IMMEDIATE`
|
|
92
|
+
* when the pool shows a free slot or a reclaimable ghost. Grabbing the write
|
|
93
|
+
* lock every poll would contend with the running job's own write transactions
|
|
94
|
+
* (the long `syncFileIndex`/`updateDependencyGraph` phase) and crash it with
|
|
95
|
+
* `database is locked`.
|
|
96
|
+
*/
|
|
97
|
+
async function acquireLease(db, projectRoot, jobId) {
|
|
98
|
+
const ttl = jobLeaseTtlMs();
|
|
99
|
+
const limit = maxRunningJobs();
|
|
100
|
+
const now = () => new Date().toISOString();
|
|
101
|
+
// The transaction only performs the *write* half of reclaim — `reclaimable`
|
|
102
|
+
// and `cutoff` are computed read-only outside the lock (see below), so the
|
|
103
|
+
// `ps` liveness check never runs while holding the write lock.
|
|
104
|
+
const claim = db.transaction((reclaimable, cutoff) => {
|
|
105
|
+
markRunsFailed(db, reclaimable, cutoff);
|
|
106
|
+
const row = db
|
|
107
|
+
.prepare('SELECT COUNT(*) AS cnt FROM findings_ledger_runs WHERE project_root = ? AND status = ?')
|
|
108
|
+
.get(projectRoot, 'running');
|
|
109
|
+
if (row.cnt < limit) {
|
|
110
|
+
// Record PID-based liveness at claim (Spec 41 Amendment B): the child's
|
|
111
|
+
// PID + process start time (defeats PID reuse) + hostname (foreign hosts
|
|
112
|
+
// fall back to the heartbeat). `process.uptime()` in the forked child is
|
|
113
|
+
// measured from the child's start, so `now - uptime` is its true start.
|
|
114
|
+
patchLedgerRun(db, jobId, {
|
|
115
|
+
status: 'running',
|
|
116
|
+
startedAt: now(),
|
|
117
|
+
heartbeatAt: now(),
|
|
118
|
+
runnerPid: process.pid,
|
|
119
|
+
runnerPidStartedAt: new Date(Date.now() - process.uptime() * 1000).toISOString(),
|
|
120
|
+
runnerHost: hostname(),
|
|
121
|
+
});
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
});
|
|
126
|
+
for (;;) {
|
|
127
|
+
// Read-only evaluation — no write lock, so it never contends with the
|
|
128
|
+
// running job's write transactions (the `ps` spawn for a stale-heartbeat
|
|
129
|
+
// candidate happens here, outside the lock).
|
|
130
|
+
const evaluation = evaluateStaleRunning(db, projectRoot, ttl);
|
|
131
|
+
const mightClaim = evaluation.runningCount < limit || evaluation.reclaimable.length > 0;
|
|
132
|
+
if (mightClaim) {
|
|
133
|
+
try {
|
|
134
|
+
if (claim.immediate(evaluation.reclaimable, evaluation.cutoff))
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
// The lease is contested by separate forked processes. A running job can
|
|
139
|
+
// hold the write lock longer than the busy timeout (e.g. the long
|
|
140
|
+
// `writeAuditToLedger` completion transaction), surfacing as SQLITE_BUSY.
|
|
141
|
+
// That is contention, not failure — wait and retry rather than failing
|
|
142
|
+
// the queued job (which would cascade into failing the running job too).
|
|
143
|
+
if (isBusyError(e)) {
|
|
144
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(250, Math.floor(ttl / 4))));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
throw e;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(250, Math.floor(ttl / 4))));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
34
153
|
function resolveWorkerEntrypoint() {
|
|
35
154
|
const current = fileURLToPath(import.meta.url);
|
|
36
155
|
const ext = path.extname(current);
|
|
@@ -38,6 +157,14 @@ function resolveWorkerEntrypoint() {
|
|
|
38
157
|
const filename = ext === '.ts' ? 'auditWorker.ts' : 'auditWorker.js';
|
|
39
158
|
return path.join(dir, 'workers', filename);
|
|
40
159
|
}
|
|
160
|
+
/** Entrypoint for the detached CLI runner (Spec 41 `--detach`). */
|
|
161
|
+
export function resolveJobRunnerEntrypoint() {
|
|
162
|
+
const current = fileURLToPath(import.meta.url);
|
|
163
|
+
const ext = path.extname(current);
|
|
164
|
+
const dir = path.dirname(current);
|
|
165
|
+
const filename = ext === '.ts' ? 'auditJobRunner.ts' : 'auditJobRunner.js';
|
|
166
|
+
return path.join(dir, 'workers', filename);
|
|
167
|
+
}
|
|
41
168
|
function asSerializableConfig(options) {
|
|
42
169
|
return {
|
|
43
170
|
projectRoot: options.projectRoot || process.cwd(),
|
|
@@ -57,7 +184,7 @@ function asSerializableConfig(options) {
|
|
|
57
184
|
function isRetryableShardError(error) {
|
|
58
185
|
return RETRYABLE_ERROR_PATTERNS.some((p) => p.test(error));
|
|
59
186
|
}
|
|
60
|
-
async function runShardTasksWithWorkerPool(jobId, tasks, options) {
|
|
187
|
+
async function runShardTasksWithWorkerPool(getDb, jobId, tasks, options) {
|
|
61
188
|
if (tasks.length === 0)
|
|
62
189
|
return [];
|
|
63
190
|
const queue = [...tasks];
|
|
@@ -198,7 +325,7 @@ async function runShardTasksWithWorkerPool(jobId, tasks, options) {
|
|
|
198
325
|
retryCount++;
|
|
199
326
|
setTimeout(() => {
|
|
200
327
|
queue.push(next);
|
|
201
|
-
setAuditJobProgress(jobId, {
|
|
328
|
+
setAuditJobProgress(getDb(), jobId, {
|
|
202
329
|
phase: 'analysis',
|
|
203
330
|
message: `Retrying shard ${next.shardId} (${next.attempts}/${options.maxRetries}) after worker recycle`,
|
|
204
331
|
current: completedResults.length,
|
|
@@ -287,7 +414,7 @@ async function runShardTasksWithWorkerPool(jobId, tasks, options) {
|
|
|
287
414
|
return;
|
|
288
415
|
const overallCurrent = completedResults.length +
|
|
289
416
|
Math.min(1, (message.progress.current ?? 0) / Math.max(1, message.progress.total ?? 1));
|
|
290
|
-
setAuditJobProgress(jobId, {
|
|
417
|
+
setAuditJobProgress(getDb(), jobId, {
|
|
291
418
|
phase: message.progress.phase ?? 'analysis',
|
|
292
419
|
message: `${message.shardId}: ${message.progress.message ?? 'running'} (retries=${retryCount})`,
|
|
293
420
|
current: Math.floor(overallCurrent),
|
|
@@ -308,7 +435,7 @@ async function runShardTasksWithWorkerPool(jobId, tasks, options) {
|
|
|
308
435
|
attempts: 0,
|
|
309
436
|
config: message.continuation,
|
|
310
437
|
});
|
|
311
|
-
setAuditJobProgress(jobId, {
|
|
438
|
+
setAuditJobProgress(getDb(), jobId, {
|
|
312
439
|
phase: 'analysis',
|
|
313
440
|
message: `Chunk done for ${entry.task.shardId}; queued ${message.remainingFiles.length} remaining file(s) (retries=${retryCount})`,
|
|
314
441
|
current: completedResults.length,
|
|
@@ -326,7 +453,7 @@ async function runShardTasksWithWorkerPool(jobId, tasks, options) {
|
|
|
326
453
|
runningShards--;
|
|
327
454
|
if (message.kind === 'worker-result') {
|
|
328
455
|
completedResults.push(message.result);
|
|
329
|
-
setAuditJobProgress(jobId, {
|
|
456
|
+
setAuditJobProgress(getDb(), jobId, {
|
|
330
457
|
phase: 'analysis',
|
|
331
458
|
message: `Completed shard ${entry.task.shardId} (${completedResults.length} chunk(s), retries=${retryCount})`,
|
|
332
459
|
current: completedResults.length,
|
|
@@ -459,14 +586,99 @@ function mergeAnalyzerResult(base, next) {
|
|
|
459
586
|
errors: [...(base.errors || []), ...(next.errors || [])],
|
|
460
587
|
};
|
|
461
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Merge per-shard coverage rows into the project-wide coverage a synchronous
|
|
591
|
+
* (non-sharded) run would have produced. Every shard's `buildCoverageReport`
|
|
592
|
+
* iterates the full rule registry and emits, for rules whose analyzer that
|
|
593
|
+
* shard did not run, a placeholder `notApplicable` row with reason
|
|
594
|
+
* `analyzer "<name>" not in results`. Those placeholders are filtered out so a
|
|
595
|
+
* rule's real state comes only from the shards that actually ran its analyzer.
|
|
596
|
+
*
|
|
597
|
+
* Precedence across the remaining rows: `fired` wins (its count re-derived from
|
|
598
|
+
* the merged deduped violations — never the summed per-shard counts, which
|
|
599
|
+
* over-count DB-based analyzers that emit full-project findings in every
|
|
600
|
+
* shard), then `clean` (ran somewhere with input present and found nothing)
|
|
601
|
+
* over `unassessed`, over `notApplicable` (only when every shard that ran the
|
|
602
|
+
* rule reported no input). Global-only analyzers (GLOBAL_ONLY_ANALYZERS) run
|
|
603
|
+
* in a single shard over the full scope, so their rows — including Spec 39
|
|
604
|
+
* applicability predicates like `missing-org-filter` — pass through untouched.
|
|
605
|
+
*/
|
|
606
|
+
function mergeCoverage(results, ordered) {
|
|
607
|
+
const rows = results.flatMap((r) => r.metadata?.coverage ?? []);
|
|
608
|
+
if (rows.length === 0)
|
|
609
|
+
return undefined;
|
|
610
|
+
const byRule = new Map();
|
|
611
|
+
for (const row of rows) {
|
|
612
|
+
const key = JSON.stringify([row.analyzer, row.ruleId]);
|
|
613
|
+
const group = byRule.get(key);
|
|
614
|
+
if (group)
|
|
615
|
+
group.push(row);
|
|
616
|
+
else
|
|
617
|
+
byRule.set(key, [row]);
|
|
618
|
+
}
|
|
619
|
+
const merged = [];
|
|
620
|
+
for (const [key, group] of byRule) {
|
|
621
|
+
const [analyzer, ruleId] = JSON.parse(key);
|
|
622
|
+
// Rows from shards that did not run this analyzer are placeholders, not a
|
|
623
|
+
// real state. Prefer the shards that ran it; fall back to placeholders only
|
|
624
|
+
// if the analyzer ran nowhere (then the placeholder's reason is truthful).
|
|
625
|
+
const real = group.filter((r) => r.reason !== `analyzer "${analyzer}" not in results`);
|
|
626
|
+
const src = real.length > 0 ? real : group;
|
|
627
|
+
const fired = src.filter((r) => r.state === 'fired');
|
|
628
|
+
if (fired.length > 0) {
|
|
629
|
+
// Derive the fired count from the merged (deduped) violations, exactly as
|
|
630
|
+
// the synchronous path's buildCoverageReport does. Summing per-shard
|
|
631
|
+
// counts would over-count if a DB-based analyzer ever ran in multiple
|
|
632
|
+
// shards (it no longer does — reducers are global-only), but deduping
|
|
633
|
+
// from the merged set stays correct regardless and keeps coverage.count
|
|
634
|
+
// in agreement with the ledger findings count.
|
|
635
|
+
const field = RULE_REGISTRY[ruleId]?.field;
|
|
636
|
+
const dedupedCount = (ordered[analyzer]?.violations ?? []).filter((v) => violationMatchesRule(v, ruleId, field)).length;
|
|
637
|
+
merged.push({
|
|
638
|
+
ruleId,
|
|
639
|
+
analyzer,
|
|
640
|
+
state: 'fired',
|
|
641
|
+
count: dedupedCount,
|
|
642
|
+
});
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const clean = src.find((r) => r.state === 'clean');
|
|
646
|
+
if (clean) {
|
|
647
|
+
merged.push({ ruleId, analyzer, state: 'clean', count: 0 });
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const unassessed = src.find((r) => r.state === 'unassessed');
|
|
651
|
+
if (unassessed) {
|
|
652
|
+
merged.push({ ruleId, analyzer, state: 'unassessed', count: 0 });
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const notApplicable = src.find((r) => r.state === 'notApplicable');
|
|
656
|
+
if (notApplicable) {
|
|
657
|
+
merged.push({
|
|
658
|
+
ruleId,
|
|
659
|
+
analyzer,
|
|
660
|
+
state: 'notApplicable',
|
|
661
|
+
count: 0,
|
|
662
|
+
reason: notApplicable.reason,
|
|
663
|
+
});
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
merged.push({ ruleId, analyzer, state: src[0].state, count: src[0].count, reason: src[0].reason });
|
|
667
|
+
}
|
|
668
|
+
return merged;
|
|
669
|
+
}
|
|
462
670
|
function mergeAuditResults(results, orderedAnalyzers) {
|
|
463
671
|
const analyzerResults = {};
|
|
464
672
|
const fileToFunctionsMap = {};
|
|
465
673
|
const collectedFunctions = [];
|
|
466
674
|
const recommendations = [];
|
|
675
|
+
const skippedFiles = [];
|
|
676
|
+
const unparsedFiles = [];
|
|
677
|
+
const diagnostics = [];
|
|
467
678
|
let filesAnalyzed = 0;
|
|
468
679
|
let auditDuration = 0;
|
|
469
680
|
let provenanceResolutionMs = 0;
|
|
681
|
+
let tableCatalog;
|
|
470
682
|
for (const result of results) {
|
|
471
683
|
for (const [analyzerName, analyzerResult] of Object.entries(result.analyzerResults || {})) {
|
|
472
684
|
analyzerResults[analyzerName] = mergeAnalyzerResult(analyzerResults[analyzerName], analyzerResult);
|
|
@@ -477,6 +689,18 @@ function mergeAuditResults(results, orderedAnalyzers) {
|
|
|
477
689
|
if (result.metadata?.collectedFunctions) {
|
|
478
690
|
collectedFunctions.push(...result.metadata.collectedFunctions);
|
|
479
691
|
}
|
|
692
|
+
if (result.metadata?.skippedFiles)
|
|
693
|
+
skippedFiles.push(...result.metadata.skippedFiles);
|
|
694
|
+
if (result.metadata?.unparsedFiles)
|
|
695
|
+
unparsedFiles.push(...result.metadata.unparsedFiles);
|
|
696
|
+
if (result.metadata?.diagnostics)
|
|
697
|
+
diagnostics.push(...result.metadata.diagnostics);
|
|
698
|
+
// tableCatalog is produced by the schema analyzer, which is global-only and
|
|
699
|
+
// therefore runs in a single shard over the full scope — first non-undefined
|
|
700
|
+
// is the whole catalog, never a fragment.
|
|
701
|
+
if (result.metadata?.tableCatalog && tableCatalog === undefined) {
|
|
702
|
+
tableCatalog = result.metadata.tableCatalog;
|
|
703
|
+
}
|
|
480
704
|
filesAnalyzed += result.metadata?.filesAnalyzed || 0;
|
|
481
705
|
auditDuration += result.metadata?.auditDuration || 0;
|
|
482
706
|
provenanceResolutionMs += result.metadata?.provenanceResolutionMs || 0;
|
|
@@ -488,6 +712,7 @@ function mergeAuditResults(results, orderedAnalyzers) {
|
|
|
488
712
|
if (analyzerResults[name])
|
|
489
713
|
ordered[name] = analyzerResults[name];
|
|
490
714
|
}
|
|
715
|
+
const coverage = mergeCoverage(results, ordered);
|
|
491
716
|
return {
|
|
492
717
|
timestamp: new Date(),
|
|
493
718
|
summary: summarizeAnalyzerResults(ordered, filesAnalyzed),
|
|
@@ -500,6 +725,11 @@ function mergeAuditResults(results, orderedAnalyzers) {
|
|
|
500
725
|
provenanceResolutionMs,
|
|
501
726
|
...(collectedFunctions.length > 0 && { collectedFunctions }),
|
|
502
727
|
...(Object.keys(fileToFunctionsMap).length > 0 && { fileToFunctionsMap }),
|
|
728
|
+
...(coverage !== undefined && { coverage }),
|
|
729
|
+
...(tableCatalog !== undefined && { tableCatalog }),
|
|
730
|
+
...(skippedFiles.length > 0 && { skippedFiles }),
|
|
731
|
+
...(unparsedFiles.length > 0 && { unparsedFiles }),
|
|
732
|
+
...(diagnostics.length > 0 && { diagnostics }),
|
|
503
733
|
},
|
|
504
734
|
};
|
|
505
735
|
}
|
|
@@ -559,12 +789,15 @@ export async function derivePartitionPlan(args, projectRoot, isFile, enabledAnal
|
|
|
559
789
|
}
|
|
560
790
|
export async function startAuditJob(args, defaults) {
|
|
561
791
|
const auditPath = path.resolve(args.path || process.cwd());
|
|
562
|
-
await assertAuditPathExists(auditPath);
|
|
563
|
-
const
|
|
792
|
+
const { isFile } = await assertAuditPathExists(auditPath);
|
|
793
|
+
const projectRoot = isFile ? path.dirname(auditPath) : auditPath;
|
|
794
|
+
const db = CodeIndexDB.getInstance(undefined, projectRoot);
|
|
795
|
+
await db.initialize();
|
|
796
|
+
const job = createAuditJob(db.rawDb, projectRoot, { surface: 'mcp', command: 'audit.start' });
|
|
564
797
|
setTimeout(() => {
|
|
565
798
|
void runAuditJob(job.jobId, args, defaults).catch((err) => {
|
|
566
799
|
try {
|
|
567
|
-
patchAuditJob(job.jobId, {
|
|
800
|
+
patchAuditJob(db.rawDb, job.jobId, {
|
|
568
801
|
status: 'failed',
|
|
569
802
|
finishedAt: new Date().toISOString(),
|
|
570
803
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -582,19 +815,47 @@ export async function startAuditJob(args, defaults) {
|
|
|
582
815
|
path: auditPath,
|
|
583
816
|
};
|
|
584
817
|
}
|
|
585
|
-
async function runAuditJob(jobId, args, defaults) {
|
|
818
|
+
export async function runAuditJob(jobId, args, defaults) {
|
|
586
819
|
let jobTimer;
|
|
820
|
+
let heartbeat;
|
|
821
|
+
// Declared as `| undefined` so a failure thrown before the DB handle resolves
|
|
822
|
+
// is still catchable; getJobDb() asserts it via `!` and the catch block
|
|
823
|
+
// swallows the resulting throw rather than masking the original error.
|
|
824
|
+
let db;
|
|
587
825
|
const ac = new AbortController();
|
|
826
|
+
// All progress/heartbeat writes go through the singleton connection — there is
|
|
827
|
+
// no transient second connection. A detached run forks shard workers, each of
|
|
828
|
+
// which opens its own SQLite connection to the shared per-project DB;
|
|
829
|
+
// better-sqlite3 opens its fd O_CLOEXEC, so forked children never inherit this
|
|
830
|
+
// parent's fd. A second open connection to the same WAL database left open
|
|
831
|
+
// into the indexing loop participates in the shared -shm state and makes the
|
|
832
|
+
// parent's deferred read→write transactions fail with "database is locked" —
|
|
833
|
+
// the defect that originally motivated a separate progress connection, so this
|
|
834
|
+
// stays a single connection throughout.
|
|
835
|
+
const getJobDb = () => db.rawDb;
|
|
588
836
|
try {
|
|
589
|
-
patchAuditJob(jobId, {
|
|
590
|
-
status: 'running',
|
|
591
|
-
startedAt: new Date().toISOString(),
|
|
592
|
-
progress: {
|
|
593
|
-
phase: 'queued',
|
|
594
|
-
message: 'Audit queued',
|
|
595
|
-
},
|
|
596
|
-
});
|
|
597
837
|
const auditPath = path.resolve(args.path || process.cwd());
|
|
838
|
+
const { isFile } = await assertAuditPathExists(auditPath);
|
|
839
|
+
const projectRoot = isFile ? path.dirname(auditPath) : auditPath;
|
|
840
|
+
// Open the per-project DB so a detached child resolves the same DB as the
|
|
841
|
+
// parent, not the child's cwd.
|
|
842
|
+
db = CodeIndexDB.getInstance(undefined, projectRoot);
|
|
843
|
+
await db.initialize();
|
|
844
|
+
// Spec 41 R5 — heartbeat lease in the prologue, before queued→running.
|
|
845
|
+
await acquireLease(db.rawDb, projectRoot, jobId);
|
|
846
|
+
patchAuditJob(db.rawDb, jobId, {
|
|
847
|
+
progress: { phase: 'queued', message: 'Audit queued' },
|
|
848
|
+
});
|
|
849
|
+
const startedMs = Date.now();
|
|
850
|
+
const heartbeatMs = Math.max(1000, Math.floor(jobLeaseTtlMs() / 2));
|
|
851
|
+
heartbeat = setInterval(() => {
|
|
852
|
+
try {
|
|
853
|
+
patchLedgerRun(getJobDb(), jobId, { heartbeatAt: new Date().toISOString() });
|
|
854
|
+
}
|
|
855
|
+
catch (e) {
|
|
856
|
+
// best-effort — a lost beat only matters after the lease TTL expires
|
|
857
|
+
}
|
|
858
|
+
}, heartbeatMs);
|
|
598
859
|
const indexFunctions = args.indexFunctions !== false;
|
|
599
860
|
const generateCodeMap = args.generateCodeMap ?? defaults.defaultGenerateCodeMap;
|
|
600
861
|
const jobTimeoutMs = Math.min(ABSOLUTE_MAX_JOB_TIMEOUT_MS, Math.max(MIN_JOB_TIMEOUT_MS, Number(args.jobTimeoutMs) || defaultJobTimeoutMs()));
|
|
@@ -606,15 +867,11 @@ async function runAuditJob(jobId, args, defaults) {
|
|
|
606
867
|
ac.abort();
|
|
607
868
|
}
|
|
608
869
|
}, jobTimeoutMs);
|
|
609
|
-
const { isFile } = await assertAuditPathExists(auditPath);
|
|
610
|
-
const db = CodeIndexDB.getInstance();
|
|
611
|
-
await db.initialize();
|
|
612
870
|
const storedConfigs = await db.getAllAnalyzerConfigs(auditPath);
|
|
613
871
|
const analyzerConfigs = {
|
|
614
872
|
...storedConfigs,
|
|
615
873
|
...(args.analyzerConfigs || {}),
|
|
616
874
|
};
|
|
617
|
-
const projectRoot = isFile ? path.dirname(auditPath) : auditPath;
|
|
618
875
|
const enabledAnalyzers = args.analyzers || defaults.defaultAnalyzers;
|
|
619
876
|
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));
|
|
620
877
|
const maxRetries = Math.max(0, Number(args.maxRetries) || 1);
|
|
@@ -641,7 +898,7 @@ async function runAuditJob(jobId, args, defaults) {
|
|
|
641
898
|
...(Object.keys(analyzerConfigs).length > 0 && { analyzerConfigs }),
|
|
642
899
|
...(args.scope && args.scope !== 'all' && { scope: args.scope }),
|
|
643
900
|
progressCallback: (p) => {
|
|
644
|
-
setAuditJobProgress(jobId, {
|
|
901
|
+
setAuditJobProgress(getJobDb(), jobId, {
|
|
645
902
|
phase: p.phase ?? 'analysis',
|
|
646
903
|
message: p.message ?? p.phase ?? 'running',
|
|
647
904
|
current: typeof p.current === 'number' ? p.current : undefined,
|
|
@@ -650,21 +907,30 @@ async function runAuditJob(jobId, args, defaults) {
|
|
|
650
907
|
},
|
|
651
908
|
};
|
|
652
909
|
const plan = await derivePartitionPlan(args, projectRoot, isFile, enabledAnalyzers);
|
|
653
|
-
|
|
910
|
+
// Spec 41 R3 — provenance: capture an aggregate content hash + per-file
|
|
911
|
+
// manifest so `result`/`status` can report staleness cheaply.
|
|
912
|
+
const fileHash = hashFileSet(await findFiles(projectRoot), projectRoot);
|
|
913
|
+
patchLedgerRun(db.rawDb, jobId, {
|
|
914
|
+
contentHash: fileHash.contentHash,
|
|
915
|
+
filesCount: fileHash.filesCount,
|
|
916
|
+
fileManifestJson: JSON.stringify(fileHash.manifest),
|
|
917
|
+
});
|
|
918
|
+
const partitionTasks = [];
|
|
919
|
+
const globalTasks = [];
|
|
654
920
|
if (plan.mode === 'none') {
|
|
655
|
-
|
|
921
|
+
partitionTasks.push({
|
|
656
922
|
shardId: 'full-scope',
|
|
657
923
|
attempts: 0,
|
|
658
924
|
config: asSerializableConfig(baseOptions),
|
|
659
925
|
});
|
|
660
926
|
}
|
|
661
927
|
else {
|
|
662
|
-
setAuditJobProgress(jobId, {
|
|
928
|
+
setAuditJobProgress(getJobDb(), jobId, {
|
|
663
929
|
phase: 'partitioning',
|
|
664
930
|
message: `Planning ${plan.partitionPaths.length} shard(s) + ${plan.globalAnalyzers.length > 0 ? 'global' : 'no-global'} analyzers`,
|
|
665
931
|
});
|
|
666
932
|
if (plan.globalAnalyzers.length > 0) {
|
|
667
|
-
|
|
933
|
+
globalTasks.push({
|
|
668
934
|
shardId: 'global-analyzers',
|
|
669
935
|
attempts: 0,
|
|
670
936
|
config: asSerializableConfig({
|
|
@@ -675,7 +941,7 @@ async function runAuditJob(jobId, args, defaults) {
|
|
|
675
941
|
});
|
|
676
942
|
}
|
|
677
943
|
for (const partitionPath of plan.partitionPaths) {
|
|
678
|
-
|
|
944
|
+
partitionTasks.push({
|
|
679
945
|
shardId: `shard:${path.basename(partitionPath)}`,
|
|
680
946
|
attempts: 0,
|
|
681
947
|
config: asSerializableConfig({
|
|
@@ -686,19 +952,32 @@ async function runAuditJob(jobId, args, defaults) {
|
|
|
686
952
|
});
|
|
687
953
|
}
|
|
688
954
|
}
|
|
689
|
-
|
|
955
|
+
const totalTasks = partitionTasks.length + globalTasks.length;
|
|
956
|
+
setAuditJobProgress(getJobDb(), jobId, {
|
|
690
957
|
phase: 'analysis',
|
|
691
|
-
message: `Running ${
|
|
958
|
+
message: `Running ${totalTasks} shard task(s) with ${maxWorkers} worker(s)`,
|
|
692
959
|
current: 0,
|
|
693
|
-
total:
|
|
960
|
+
total: totalTasks,
|
|
694
961
|
});
|
|
695
|
-
const
|
|
962
|
+
const poolOptions = {
|
|
696
963
|
maxWorkers,
|
|
697
964
|
maxRetries,
|
|
698
965
|
shardTimeoutMs,
|
|
699
966
|
retryBackoffMs,
|
|
700
967
|
signal: ac.signal,
|
|
701
|
-
}
|
|
968
|
+
};
|
|
969
|
+
// Partition shards run first; the global-analyzers shard is scheduled only
|
|
970
|
+
// after every partition shard has completed. The global shard's full-scope
|
|
971
|
+
// reducers (conventions, cross-domain, styles) read cross-file index tables
|
|
972
|
+
// (`functions`, `function_calls`, `style_*`) that the always-on function-index
|
|
973
|
+
// visitor writes from *every* shard. Running it concurrently — even pushed
|
|
974
|
+
// "first" — left a `functions`-table write/read race in principle. Serializing
|
|
975
|
+
// it after the partitions closes that last multi-writer window structurally.
|
|
976
|
+
const resultParts = [];
|
|
977
|
+
resultParts.push(...(await runShardTasksWithWorkerPool(getJobDb, jobId, partitionTasks, poolOptions)));
|
|
978
|
+
if (globalTasks.length > 0) {
|
|
979
|
+
resultParts.push(...(await runShardTasksWithWorkerPool(getJobDb, jobId, globalTasks, poolOptions)));
|
|
980
|
+
}
|
|
702
981
|
if (ac.signal.aborted) {
|
|
703
982
|
throw ac.signal.reason instanceof Error
|
|
704
983
|
? ac.signal.reason
|
|
@@ -707,18 +986,14 @@ async function runAuditJob(jobId, args, defaults) {
|
|
|
707
986
|
const auditResult = resultParts.length === 1 ? resultParts[0] : mergeAuditResults(resultParts, enabledAnalyzers);
|
|
708
987
|
let indexingResult = null;
|
|
709
988
|
if (indexFunctions && auditResult.metadata.fileToFunctionsMap) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
? ac.signal.reason
|
|
715
|
-
: new Error(String(ac.signal.reason || 'Audit job was cancelled during indexing'));
|
|
716
|
-
}
|
|
717
|
-
const fileStats = await syncFileIndex(filePath, functions);
|
|
718
|
-
syncStats.added += fileStats.added;
|
|
719
|
-
syncStats.updated += fileStats.updated;
|
|
720
|
-
syncStats.removed += fileStats.removed;
|
|
989
|
+
if (ac.signal.aborted) {
|
|
990
|
+
throw ac.signal.reason instanceof Error
|
|
991
|
+
? ac.signal.reason
|
|
992
|
+
: new Error(String(ac.signal.reason || 'Audit job was cancelled during indexing'));
|
|
721
993
|
}
|
|
994
|
+
// Batch all per-file upserts into a single transaction (Amendment B2).
|
|
995
|
+
const entries = Object.entries(auditResult.metadata.fileToFunctionsMap).map(([filePath, functions]) => ({ filePath, currentFunctions: functions }));
|
|
996
|
+
const syncStats = await db.syncFileIndexBatch(entries);
|
|
722
997
|
indexingResult = {
|
|
723
998
|
success: true,
|
|
724
999
|
registered: syncStats.added + syncStats.updated,
|
|
@@ -771,7 +1046,11 @@ async function runAuditJob(jobId, args, defaults) {
|
|
|
771
1046
|
...(codeMapResult && { codeMap: codeMapResult }),
|
|
772
1047
|
};
|
|
773
1048
|
const resultId = await db.storeAuditResults(persisted, projectRootForStore);
|
|
774
|
-
|
|
1049
|
+
// Spec 41 R2 — converge on the one ledger write path: attach findings +
|
|
1050
|
+
// coverage to the pre-existing run, then close the lifecycle.
|
|
1051
|
+
const runInput = detectRunInput('audit.start', 'mcp', args.scope || 'all', projectRoot, PACKAGE_VERSION);
|
|
1052
|
+
writeAuditToLedger(db.rawDb, runInput, getAllViolations(auditResult), Date.now() - startedMs, 0, { runId: jobId, coverage: auditResult.metadata.coverage });
|
|
1053
|
+
patchAuditJob(db.rawDb, jobId, {
|
|
775
1054
|
status: 'completed',
|
|
776
1055
|
finishedAt: new Date().toISOString(),
|
|
777
1056
|
progress: { phase: 'completed', message: 'Audit completed' },
|
|
@@ -779,21 +1058,47 @@ async function runAuditJob(jobId, args, defaults) {
|
|
|
779
1058
|
});
|
|
780
1059
|
}
|
|
781
1060
|
catch (e) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
1061
|
+
// Write the full trace to stderr (the detached child's per-run log) so a
|
|
1062
|
+
// failure is diagnosable without re-running — the error row alone drops the
|
|
1063
|
+
// stack that says *where* the run died.
|
|
1064
|
+
try {
|
|
1065
|
+
process.stderr.write(`[code-auditor] job ${jobId} failed: ${e instanceof Error ? (e.stack || e.message) : String(e)}\n`);
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
// ignore — the log write is best-effort
|
|
1069
|
+
}
|
|
1070
|
+
// Record the failure through the singleton connection (best-effort — never
|
|
1071
|
+
// mask the original error, and the run already failed). If the error was
|
|
1072
|
+
// thrown before the DB handle resolved, getJobDb() itself throws and is
|
|
1073
|
+
// swallowed here.
|
|
1074
|
+
try {
|
|
1075
|
+
patchAuditJob(getJobDb(), jobId, {
|
|
1076
|
+
status: 'failed',
|
|
1077
|
+
finishedAt: new Date().toISOString(),
|
|
1078
|
+
error: e instanceof Error ? e.message : String(e),
|
|
1079
|
+
progress: { phase: 'failed', message: 'Audit failed' },
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
// best-effort — the run already failed; never mask the original error
|
|
1084
|
+
}
|
|
788
1085
|
}
|
|
789
1086
|
finally {
|
|
1087
|
+
if (heartbeat !== undefined) {
|
|
1088
|
+
clearInterval(heartbeat);
|
|
1089
|
+
}
|
|
790
1090
|
if (jobTimer !== undefined) {
|
|
791
1091
|
clearTimeout(jobTimer);
|
|
792
1092
|
}
|
|
793
1093
|
}
|
|
794
1094
|
}
|
|
795
|
-
export function getAuditJobStatus(jobId) {
|
|
796
|
-
|
|
1095
|
+
export async function getAuditJobStatus(jobId) {
|
|
1096
|
+
// Resolve the same DB the writer opened: startAuditJob/runAuditJob set the
|
|
1097
|
+
// singleton to the project-scoped DB, so read from the current singleton's
|
|
1098
|
+
// project root rather than cwd (which would flip the singleton away).
|
|
1099
|
+
const db = CodeIndexDB.getInstance(undefined, CodeIndexDB.currentProject);
|
|
1100
|
+
await db.initialize();
|
|
1101
|
+
const job = getAuditJob(db.rawDb, jobId);
|
|
797
1102
|
if (!job) {
|
|
798
1103
|
throw new ContextualError(`Audit job not found: ${jobId}`, {
|
|
799
1104
|
jobId,
|
|
@@ -822,7 +1127,7 @@ export async function getAuditResultsAsSarif(args) {
|
|
|
822
1127
|
hint: 'Call audit.start, poll audit.status until completed, then pass resultId to audit.results with format: "sarif".',
|
|
823
1128
|
});
|
|
824
1129
|
}
|
|
825
|
-
const db = CodeIndexDB.getInstance();
|
|
1130
|
+
const db = CodeIndexDB.getInstance(undefined, CodeIndexDB.currentProject);
|
|
826
1131
|
await db.initialize();
|
|
827
1132
|
const stored = await db.getAuditResults(resultId);
|
|
828
1133
|
if (!stored) {
|
|
@@ -867,7 +1172,7 @@ export async function getAuditResultsPage(args) {
|
|
|
867
1172
|
}
|
|
868
1173
|
const limit = Math.min(Math.max(0, Number(args.limit)) || 50, 100);
|
|
869
1174
|
const offset = Math.max(0, Number(args.offset) || 0);
|
|
870
|
-
const db = CodeIndexDB.getInstance();
|
|
1175
|
+
const db = CodeIndexDB.getInstance(undefined, CodeIndexDB.currentProject);
|
|
871
1176
|
await db.initialize();
|
|
872
1177
|
const auditResult = await db.getAuditResults(resultId);
|
|
873
1178
|
if (!auditResult) {
|
|
@@ -907,5 +1212,6 @@ export async function getAuditResultsPage(args) {
|
|
|
907
1212
|
export const __testables = {
|
|
908
1213
|
isRetryableShardError,
|
|
909
1214
|
mergeAnalyzerResult,
|
|
1215
|
+
mergeCoverage,
|
|
910
1216
|
};
|
|
911
1217
|
//# sourceMappingURL=mcpAuditJobs.js.map
|