code-auditor-mcp 3.7.0 → 3.8.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +43 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +122 -43
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +2 -2
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +6 -7
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +19 -18
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +9 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +63 -44
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +76 -28
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +73 -13
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +12 -2
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +30 -18
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -8
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +54 -45
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/functionConcerns.d.ts +9 -4
- package/dist/analyzers/universal/functionConcerns.d.ts.map +1 -1
- package/dist/analyzers/universal/functionConcerns.js +25 -11
- package/dist/analyzers/universal/functionConcerns.js.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.js +4 -4
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.js +124 -11
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
- package/dist/auditRouter.d.ts.map +1 -1
- package/dist/auditRouter.js +4 -6
- package/dist/auditRouter.js.map +1 -1
- package/dist/auditRunner.js +1 -1
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +284 -75
- package/dist/cli.js.map +1 -1
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +23 -0
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +3 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +30 -8
- package/dist/config/defaults.js.map +1 -1
- package/dist/conventions/conventionMiner.d.ts +18 -3
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +130 -12
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/daemon/core.d.ts +128 -0
- package/dist/daemon/core.d.ts.map +1 -0
- package/dist/daemon/core.js +700 -0
- package/dist/daemon/core.js.map +1 -0
- package/dist/daemon/lspServer.d.ts +51 -0
- package/dist/daemon/lspServer.d.ts.map +1 -0
- package/dist/daemon/lspServer.js +168 -0
- package/dist/daemon/lspServer.js.map +1 -0
- package/dist/daemon/main.d.ts +34 -0
- package/dist/daemon/main.d.ts.map +1 -0
- package/dist/daemon/main.js +147 -0
- package/dist/daemon/main.js.map +1 -0
- package/dist/daemon/resolve.d.ts +41 -0
- package/dist/daemon/resolve.d.ts.map +1 -0
- package/dist/daemon/resolve.js +90 -0
- package/dist/daemon/resolve.js.map +1 -0
- package/dist/daemon/socketClient.d.ts +21 -0
- package/dist/daemon/socketClient.d.ts.map +1 -0
- package/dist/daemon/socketClient.js +60 -0
- package/dist/daemon/socketClient.js.map +1 -0
- package/dist/daemon/socketServer.d.ts +33 -0
- package/dist/daemon/socketServer.d.ts.map +1 -0
- package/dist/daemon/socketServer.js +106 -0
- package/dist/daemon/socketServer.js.map +1 -0
- package/dist/daemon/types.d.ts +78 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/types.js +10 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/dataPaths.d.ts +21 -0
- package/dist/dataPaths.d.ts.map +1 -1
- package/dist/dataPaths.js +27 -0
- package/dist/dataPaths.js.map +1 -1
- package/dist/hooks/core.d.ts.map +1 -1
- package/dist/hooks/core.js +21 -4
- package/dist/hooks/core.js.map +1 -1
- package/dist/languages/RuntimeManager.js +10 -0
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/go/analyzer +0 -0
- package/dist/languages/go/analyzer-src/analyzer.go +166 -35
- package/dist/languages/go/analyzer-src/solid.go +80 -172
- package/dist/languages/go/analyzer-src/types.go +18 -18
- package/dist/ledger.d.ts +1 -1
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +3 -3
- package/dist/ledger.js.map +1 -1
- package/dist/mcpAuditJobs.js +1 -1
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/nextFileIncremental.d.ts +9 -3
- package/dist/nextFileIncremental.d.ts.map +1 -1
- package/dist/nextFileIncremental.js +32 -6
- package/dist/nextFileIncremental.js.map +1 -1
- package/dist/pipeline.d.ts +26 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +86 -10
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +1 -1
- package/dist/pipelineAdapters.js +1 -1
- package/dist/reporting/csvReportGenerator.js +1 -1
- package/dist/ruleAliases.d.ts.map +1 -1
- package/dist/ruleAliases.js +41 -2
- package/dist/ruleAliases.js.map +1 -1
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/code-auditor/SKILL.md +6 -6
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec 50 — `DaemonCore`: the daemon's heart. Owns the store and the pipeline
|
|
3
|
+
* (R1); the LSP and Unix-socket faces are adapters over it.
|
|
4
|
+
*
|
|
5
|
+
* The core reuses the Spec 41 incremental machinery wholesale: `hashAndStatFiles`
|
|
6
|
+
* / `diffFiles` / `splitFindings` / `mergeFindings` from `nextFileIncremental.ts`.
|
|
7
|
+
* The seed is a full `runAuditDispatch` (the same entry `code-audit audit` uses,
|
|
8
|
+
* so counts match — acceptance 10); a warm re-audit is a scoped dispatch merged
|
|
9
|
+
* per-analyzer, exactly the `next-file` warm path made continuous.
|
|
10
|
+
*
|
|
11
|
+
* Concurrency (R5) uses the Spec 41 lease: a `findings_ledger_runs` row with
|
|
12
|
+
* `status='running'`, PID + hostname + start-time, heartbeated. A second daemon
|
|
13
|
+
* that cannot bind the socket (Face B) exits; a crashed daemon's lease is
|
|
14
|
+
* reclaimed by the next start via `reclaimStaleRunning`.
|
|
15
|
+
*/
|
|
16
|
+
import { EventEmitter } from 'node:events';
|
|
17
|
+
import { watch, statSync, existsSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { hostname } from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { runAuditDispatch } from '../auditRouter.js';
|
|
22
|
+
import { CodeIndexDB } from '../codeIndexDB.js';
|
|
23
|
+
import { PACKAGE_VERSION } from '../constants.js';
|
|
24
|
+
import { createLedgerRun, writeAuditToLedger, patchLedgerRun, hashFileSet, reclaimStaleRunning, } from '../ledger.js';
|
|
25
|
+
import { findConfigFileUp, loadConfig } from '../config/configLoader.js';
|
|
26
|
+
import { discoverFilesDetailed, KNOWN_SOURCE_EXTENSIONS } from '../utils/fileDiscovery.js';
|
|
27
|
+
import { initParsers, LanguageRegistry } from '../languages/index.js';
|
|
28
|
+
import { hashAndStatFiles, diffFiles, splitFindings, mergeFindings, } from '../nextFileIncremental.js';
|
|
29
|
+
/** Extensions that carry table/DDL definitions — a change can shift the schema catalog. */
|
|
30
|
+
const SCHEMA_DEFINITION_EXTENSIONS = new Set(['.sql', '.prisma']);
|
|
31
|
+
/** Daemon lease TTL (longer than the 30s audit-job lease: a long-lived idle process). */
|
|
32
|
+
const DAEMON_LEASE_TTL_MS = Number(process.env.CODE_AUDITOR_DAEMON_LEASE_TTL_MS) || 60_000;
|
|
33
|
+
/** Debounce window for coalescing a burst of watcher events into one re-audit. */
|
|
34
|
+
const WATCH_DEBOUNCE_MS = 300;
|
|
35
|
+
/** Clamp for `retryAfterMs` so it is never absurdly short or long. */
|
|
36
|
+
const RETRY_AFTER_MIN_MS = 250;
|
|
37
|
+
const RETRY_AFTER_MAX_MS = 30_000;
|
|
38
|
+
export class DaemonCore extends EventEmitter {
|
|
39
|
+
projectRoot;
|
|
40
|
+
configName;
|
|
41
|
+
idleTimeoutMs;
|
|
42
|
+
status = 'starting';
|
|
43
|
+
snapshot = null;
|
|
44
|
+
progress = { filesIndexed: 0, filesTotal: 0, sourceTotal: 0, orphanTotal: 0 };
|
|
45
|
+
seedStartedAt = 0;
|
|
46
|
+
// Phase machine for `retryAfterMs`: the seed runs files → reducers → derived →
|
|
47
|
+
// finalize, and each phase is priced on its own observed rate so the estimate
|
|
48
|
+
// covers time-to-ready (never collapsing to "done" during the reducer tail).
|
|
49
|
+
phase = 'files';
|
|
50
|
+
phaseCurrent = 0;
|
|
51
|
+
phaseTotal = 0;
|
|
52
|
+
phaseStartedAt = 0;
|
|
53
|
+
orphanStartedAt = 0;
|
|
54
|
+
seenOrphan = false;
|
|
55
|
+
db = null;
|
|
56
|
+
leaseRunId = null;
|
|
57
|
+
heartbeat = null;
|
|
58
|
+
idleTimer = null;
|
|
59
|
+
watcher = null;
|
|
60
|
+
watchDebounce = null;
|
|
61
|
+
pendingWatchPaths = new Set();
|
|
62
|
+
reindexing = false;
|
|
63
|
+
lastActivityAt = Date.now();
|
|
64
|
+
activeConnections = 0;
|
|
65
|
+
shutdownRequested = false;
|
|
66
|
+
config = null;
|
|
67
|
+
constructor(options) {
|
|
68
|
+
super();
|
|
69
|
+
this.projectRoot = path.resolve(options.projectRoot);
|
|
70
|
+
this.configName = options.configName;
|
|
71
|
+
this.idleTimeoutMs = options.idleTimeoutMs;
|
|
72
|
+
}
|
|
73
|
+
/** Called by each face when a client connects/disconnects (idle tracking). */
|
|
74
|
+
registerConnection() {
|
|
75
|
+
this.activeConnections++;
|
|
76
|
+
this.markActivity();
|
|
77
|
+
}
|
|
78
|
+
unregisterConnection() {
|
|
79
|
+
this.activeConnections = Math.max(0, this.activeConnections - 1);
|
|
80
|
+
this.markActivity();
|
|
81
|
+
}
|
|
82
|
+
/** Faces call this on any client activity so the idle timer resets (R5). */
|
|
83
|
+
markActivity() {
|
|
84
|
+
this.lastActivityAt = Date.now();
|
|
85
|
+
}
|
|
86
|
+
getState() {
|
|
87
|
+
const state = {
|
|
88
|
+
status: this.status,
|
|
89
|
+
retryAfterMs: null,
|
|
90
|
+
throughputUnknown: false,
|
|
91
|
+
};
|
|
92
|
+
if (this.status === 'indexing' || this.status === 'reindexing') {
|
|
93
|
+
state.progress = {
|
|
94
|
+
...this.progress,
|
|
95
|
+
phase: this.phase,
|
|
96
|
+
phaseCurrent: this.phaseCurrent,
|
|
97
|
+
phaseTotal: this.phaseTotal,
|
|
98
|
+
};
|
|
99
|
+
state.retryAfterMs = this.deriveRetryAfterMs();
|
|
100
|
+
state.throughputUnknown = state.retryAfterMs === null;
|
|
101
|
+
}
|
|
102
|
+
return state;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* R3/R6 — derive `retryAfterMs` from observed progress across the seed's whole
|
|
106
|
+
* lifetime (files → reducers → derived → finalize), never a constant. Each
|
|
107
|
+
* phase is priced on its own observed rate; at a phase boundary the rate is
|
|
108
|
+
* unknown yet, so the value clamps conservatively instead of collapsing to
|
|
109
|
+
* "done" while work remains.
|
|
110
|
+
*/
|
|
111
|
+
deriveRetryAfterMs() {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (this.phase === 'files')
|
|
114
|
+
return this.deriveFilesEta(now);
|
|
115
|
+
return this.derivePhaseEta(now);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Files phase — the two-population split. Source files (parsed, ~11ms each)
|
|
119
|
+
* are priced at the source rate; orphan files (raw, mostly no visitor) at the
|
|
120
|
+
* orphan rate. Discovery classifies both up front, so the split is known
|
|
121
|
+
* before the first sample. During the source phase the orphan rate is not yet
|
|
122
|
+
* observed, so orphans are priced at the source rate (the honest fallback —
|
|
123
|
+
* conservative, never an underestimate); once the source phase completes, the
|
|
124
|
+
* orphan tail is priced on its own (much faster) rate.
|
|
125
|
+
*/
|
|
126
|
+
deriveFilesEta(now) {
|
|
127
|
+
const elapsedSec = (now - this.seedStartedAt) / 1000;
|
|
128
|
+
const { filesIndexed, filesTotal, sourceTotal } = this.progress;
|
|
129
|
+
if (filesIndexed <= 0 || elapsedSec <= 0)
|
|
130
|
+
return null; // throughput unknown yet
|
|
131
|
+
// Degenerate case: no source files — the whole corpus is orphan, priced at
|
|
132
|
+
// the cumulative rate (there is no source phase to complete first).
|
|
133
|
+
if (sourceTotal <= 0) {
|
|
134
|
+
const rate = filesIndexed / elapsedSec;
|
|
135
|
+
if (rate <= 0)
|
|
136
|
+
return null;
|
|
137
|
+
return this.clampRetry(((filesTotal - filesIndexed) / rate) * 1000);
|
|
138
|
+
}
|
|
139
|
+
const sourceIndexed = Math.min(filesIndexed, sourceTotal);
|
|
140
|
+
const orphanIndexed = Math.max(0, filesIndexed - sourceTotal);
|
|
141
|
+
if (sourceIndexed < sourceTotal) {
|
|
142
|
+
// Source phase: orphans priced at the source rate (fallback). This equals
|
|
143
|
+
// the cumulative estimate — pessimistic early, honest about what it knows.
|
|
144
|
+
const sourceRate = sourceIndexed / elapsedSec;
|
|
145
|
+
if (sourceRate <= 0)
|
|
146
|
+
return null;
|
|
147
|
+
const remaining = filesTotal - sourceIndexed; // source remaining + all orphans
|
|
148
|
+
return this.clampRetry((remaining / sourceRate) * 1000);
|
|
149
|
+
}
|
|
150
|
+
// Orphan phase: source is done; price the orphan tail on its own rate.
|
|
151
|
+
// (`orphanStartedAt` is set the first time `filesIndexed >= sourceTotal`; a
|
|
152
|
+
// still-unset boundary can only mean `orphanIndexed` is 0, which is handled
|
|
153
|
+
// by the `sourceIndexed < sourceTotal` branch above.)
|
|
154
|
+
const orphanElapsedSec = (now - this.orphanStartedAt) / 1000;
|
|
155
|
+
if (orphanElapsedSec <= 0)
|
|
156
|
+
return RETRY_AFTER_MAX_MS;
|
|
157
|
+
const orphanRate = orphanIndexed / orphanElapsedSec;
|
|
158
|
+
if (orphanRate <= 0)
|
|
159
|
+
return RETRY_AFTER_MAX_MS;
|
|
160
|
+
const orphanRemaining = this.progress.orphanTotal - orphanIndexed;
|
|
161
|
+
if (orphanRemaining <= 0)
|
|
162
|
+
return RETRY_AFTER_MIN_MS;
|
|
163
|
+
return this.clampRetry((orphanRemaining / orphanRate) * 1000);
|
|
164
|
+
}
|
|
165
|
+
/** Reducer/derived/finalize phases — uniform unit, priced on the phase's own window. */
|
|
166
|
+
derivePhaseEta(now) {
|
|
167
|
+
const { phaseCurrent, phaseTotal, phaseStartedAt } = this;
|
|
168
|
+
if (phaseTotal <= 0)
|
|
169
|
+
return RETRY_AFTER_MAX_MS; // no work priced in this phase — conservative
|
|
170
|
+
if (phaseCurrent <= 0)
|
|
171
|
+
return RETRY_AFTER_MAX_MS; // rate unknown yet, work known to remain
|
|
172
|
+
const phaseElapsedSec = (now - phaseStartedAt) / 1000;
|
|
173
|
+
if (phaseElapsedSec <= 0)
|
|
174
|
+
return RETRY_AFTER_MAX_MS;
|
|
175
|
+
const rate = phaseCurrent / phaseElapsedSec;
|
|
176
|
+
if (rate <= 0)
|
|
177
|
+
return RETRY_AFTER_MAX_MS;
|
|
178
|
+
const remaining = phaseTotal - phaseCurrent;
|
|
179
|
+
const rawMs = remaining <= 0 ? 0 : (remaining / rate) * 1000;
|
|
180
|
+
const eta = this.clampRetry(rawMs);
|
|
181
|
+
// The 250ms floor means "almost ready". That is only ever true in the final
|
|
182
|
+
// phase: in reducers/derived a later phase has not started, so a floor-level
|
|
183
|
+
// estimate would understate time-to-ready and must stay conservative.
|
|
184
|
+
if (this.phase !== 'finalize' && eta === RETRY_AFTER_MIN_MS)
|
|
185
|
+
return RETRY_AFTER_MAX_MS;
|
|
186
|
+
return eta;
|
|
187
|
+
}
|
|
188
|
+
clampRetry(ms) {
|
|
189
|
+
return Math.min(RETRY_AFTER_MAX_MS, Math.max(RETRY_AFTER_MIN_MS, Math.round(ms)));
|
|
190
|
+
}
|
|
191
|
+
/** Map pipeline + local progress events onto the phase machine (R3). */
|
|
192
|
+
onSeedProgress(p) {
|
|
193
|
+
const ph = p.phase;
|
|
194
|
+
if (ph === 'stage2') {
|
|
195
|
+
// File-level numerator — the pipeline also emits stage1 parse-failure
|
|
196
|
+
// events, but stage2's `current` is the authoritative (monotonic) file
|
|
197
|
+
// index, so track it exclusively and take a max for safety.
|
|
198
|
+
this.progress.filesIndexed = Math.max(this.progress.filesIndexed, p.current ?? 0);
|
|
199
|
+
this.assertSourceBeforeOrphan(p);
|
|
200
|
+
// Detect the source→orphan boundary (ordering invariant: source yields first).
|
|
201
|
+
if (this.orphanStartedAt === 0 && this.progress.sourceTotal > 0 && this.progress.filesIndexed >= this.progress.sourceTotal) {
|
|
202
|
+
this.orphanStartedAt = Date.now();
|
|
203
|
+
}
|
|
204
|
+
this.setPhase('files', this.progress.filesIndexed, this.progress.filesTotal);
|
|
205
|
+
}
|
|
206
|
+
else if (ph === 'stage3') {
|
|
207
|
+
this.setPhase('reducers', p.current ?? 0, p.total ?? 0);
|
|
208
|
+
}
|
|
209
|
+
else if (ph === 'stage4') {
|
|
210
|
+
this.setPhase('derived', p.current ?? 0, p.total ?? 0);
|
|
211
|
+
}
|
|
212
|
+
else if (ph === 'stage4-complete') {
|
|
213
|
+
// Pipeline is done; the daemon's own finalize (hash + persist) and any
|
|
214
|
+
// audit-runner post-pipeline awaits remain. Enter the finalize phase with
|
|
215
|
+
// no priced unit so the estimate clamps conservatively rather than
|
|
216
|
+
// reporting "done" at the end of the reducer tail.
|
|
217
|
+
this.setPhase('finalize', 0, 0);
|
|
218
|
+
}
|
|
219
|
+
else if (ph === 'finalize') {
|
|
220
|
+
this.setPhase('finalize', p.current ?? 0, p.total ?? 0);
|
|
221
|
+
}
|
|
222
|
+
// Ignore stage1 / stage1-complete / stage2-complete and any unknown phase.
|
|
223
|
+
this.emitState();
|
|
224
|
+
}
|
|
225
|
+
setPhase(next, current, total) {
|
|
226
|
+
if (next !== this.phase) {
|
|
227
|
+
this.phase = next;
|
|
228
|
+
this.phaseStartedAt = Date.now();
|
|
229
|
+
}
|
|
230
|
+
this.phaseCurrent = current;
|
|
231
|
+
this.phaseTotal = total;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Ordering invariant (R3 accuracy guard): stage 1 yields source files before
|
|
235
|
+
* orphans. The two-population split above interprets `filesIndexed` as "source
|
|
236
|
+
* done up to `sourceTotal`, then orphans" — a future reorder that interleaved
|
|
237
|
+
* the two loops would silently corrupt the estimate. Assert it loudly: throw
|
|
238
|
+
* the moment a source file is reported after an orphan has been seen.
|
|
239
|
+
*/
|
|
240
|
+
assertSourceBeforeOrphan(p) {
|
|
241
|
+
if (p.file == null)
|
|
242
|
+
return;
|
|
243
|
+
const isSource = LanguageRegistry.getInstance().getAdapterForFile(p.file) !== null;
|
|
244
|
+
if (!isSource) {
|
|
245
|
+
this.seenOrphan = true;
|
|
246
|
+
}
|
|
247
|
+
else if (this.seenOrphan) {
|
|
248
|
+
throw new Error(`code-auditor daemon: stage-1 ordering violated — source file ${p.file} ` +
|
|
249
|
+
`yielded after orphan files; the two-population retryAfterMs estimate is invalid`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** All current findings + R4 stale-file report. */
|
|
253
|
+
getFindings() {
|
|
254
|
+
const staleFiles = this.checkStaleness();
|
|
255
|
+
return {
|
|
256
|
+
status: this.status,
|
|
257
|
+
violations: this.snapshot ? flattenSnapshot(this.snapshot) : [],
|
|
258
|
+
staleFiles,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** Per-file findings (diagnostics) + R4 staleness for those files. */
|
|
262
|
+
getDiagnostics(files) {
|
|
263
|
+
const staleFiles = this.checkStaleness(files);
|
|
264
|
+
const diagnostics = [];
|
|
265
|
+
if (this.snapshot) {
|
|
266
|
+
for (const file of files) {
|
|
267
|
+
const key = path.relative(this.projectRoot, file);
|
|
268
|
+
for (const v of this.snapshot.visitorFindings[key] ?? []) {
|
|
269
|
+
diagnostics.push(v);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { status: this.status, diagnostics, staleFiles };
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* True if a watcher-flagged path belongs to the audited corpus (R5). The
|
|
277
|
+
* watcher reports every change under the root — including the SQLite DB, WAL,
|
|
278
|
+
* and SHM files this process writes to `node_modules/.cache` — so the
|
|
279
|
+
* reconcile gate must filter to source extensions and never infra dirs, or a
|
|
280
|
+
* single persist would re-audit the corpus and loop.
|
|
281
|
+
*/
|
|
282
|
+
isSourcePath(abs) {
|
|
283
|
+
const rel = path.relative(this.projectRoot, abs);
|
|
284
|
+
if (!rel || rel === '.' || rel.startsWith('..') || path.isAbsolute(rel))
|
|
285
|
+
return false;
|
|
286
|
+
const parts = rel.split(path.sep);
|
|
287
|
+
if (parts.some((p) => p === 'node_modules' || p === '.git' || p === '.cache'))
|
|
288
|
+
return false;
|
|
289
|
+
return KNOWN_SOURCE_EXTENSIONS.includes(path.extname(abs).toLowerCase());
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* R4 — validate that the served findings are current. Cheap-first: compare the
|
|
293
|
+
* recorded mtime; only when it diverges (or the file is new/missing) is the
|
|
294
|
+
* content re-hashed. `files` narrows the check to a specific read; `undefined`
|
|
295
|
+
* checks the whole recorded set (count + newest mtime fast path, full hash on
|
|
296
|
+
* divergence).
|
|
297
|
+
*/
|
|
298
|
+
checkStaleness(files) {
|
|
299
|
+
if (!this.snapshot)
|
|
300
|
+
return [];
|
|
301
|
+
const stale = new Set();
|
|
302
|
+
if (files && files.length > 0) {
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
const abs = path.isAbsolute(file) ? file : path.join(this.projectRoot, file);
|
|
305
|
+
const key = path.relative(this.projectRoot, abs);
|
|
306
|
+
const rec = this.snapshot.files[key];
|
|
307
|
+
try {
|
|
308
|
+
const st = statSync(abs);
|
|
309
|
+
if (rec && st.mtimeMs === rec.mtimeMs)
|
|
310
|
+
continue; // mtime unchanged → current
|
|
311
|
+
// mtime changed (or file is new): full hash to confirm.
|
|
312
|
+
const hash = sha256(readFileSync(abs));
|
|
313
|
+
if (!rec || hash !== rec.hash)
|
|
314
|
+
stale.add(key);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Missing on disk: if we had recorded it, it's gone — stale; if never
|
|
318
|
+
// recorded, it isn't ours to serve.
|
|
319
|
+
if (rec)
|
|
320
|
+
stale.add(key);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return [...stale];
|
|
324
|
+
}
|
|
325
|
+
// Whole-set cheap pass (R4): stat each recorded file and compare its current
|
|
326
|
+
// mtime to the snapshot's recorded mtime. Any divergence (or a missing file)
|
|
327
|
+
// means the served set may be stale, so fall through to the full content-hash
|
|
328
|
+
// diff. This is O(n) stat — no readFileSync — so the common "nothing changed"
|
|
329
|
+
// read pays one stat per file, not one hash. A *content* change is always
|
|
330
|
+
// reflected in the mtime of a normal write; the mtime-preserved case is the
|
|
331
|
+
// job of the next-file `assertNoStaleFiles` gate, not this live read path.
|
|
332
|
+
const recorded = this.snapshot.files;
|
|
333
|
+
const rels = Object.keys(recorded);
|
|
334
|
+
let diverged = false;
|
|
335
|
+
for (const rel of rels) {
|
|
336
|
+
try {
|
|
337
|
+
const st = statSync(path.join(this.projectRoot, rel));
|
|
338
|
+
if (st.mtimeMs !== recorded[rel].mtimeMs) {
|
|
339
|
+
diverged = true;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
stale.add(rel); // missing on disk
|
|
345
|
+
diverged = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (diverged || stale.size > 0) {
|
|
349
|
+
const current = hashAndStatFiles(rels.map((r) => path.join(this.projectRoot, r)), this.projectRoot);
|
|
350
|
+
const diff = diffFiles(recorded, current);
|
|
351
|
+
for (const rel of [...diff.changed, ...diff.deleted])
|
|
352
|
+
stale.add(rel);
|
|
353
|
+
}
|
|
354
|
+
return [...stale];
|
|
355
|
+
}
|
|
356
|
+
/** Start: init parsers, claim the lease, seed, watch. Resolves once seed completes. */
|
|
357
|
+
async start() {
|
|
358
|
+
this.status = 'starting';
|
|
359
|
+
await initParsers();
|
|
360
|
+
const configPath = await findConfigFileUp(this.projectRoot);
|
|
361
|
+
// Mirror the runner (`auditRunner.run`): load project config only when a
|
|
362
|
+
// `.codeauditor.json` exists. Loading defaults here would normalize their
|
|
363
|
+
// `includePaths`/`excludePaths` globs against *this process's* cwd (the
|
|
364
|
+
// install dir), not the served project — the seed would then discover zero
|
|
365
|
+
// files (R4's file-set hash would be empty) even though the pipeline's own
|
|
366
|
+
// discovery (defaults against the project root) finds the corpus.
|
|
367
|
+
this.config = configPath ? await loadConfig({ configPath }) : null;
|
|
368
|
+
this.db = CodeIndexDB.getInstance(undefined, this.projectRoot);
|
|
369
|
+
await this.db.initialize();
|
|
370
|
+
this.claimLease();
|
|
371
|
+
this.startIdleTimer();
|
|
372
|
+
await this.seed();
|
|
373
|
+
this.status = 'ready';
|
|
374
|
+
this.markActivity();
|
|
375
|
+
this.emitState();
|
|
376
|
+
this.startWatcher();
|
|
377
|
+
}
|
|
378
|
+
claimLease() {
|
|
379
|
+
if (!this.db)
|
|
380
|
+
return;
|
|
381
|
+
// Reclaim any stale daemon lease from a prior crashed instance (R5).
|
|
382
|
+
try {
|
|
383
|
+
reclaimStaleRunning(this.db.rawDb, this.projectRoot, DAEMON_LEASE_TTL_MS);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// best-effort — the socket bind is the primary exclusivity guard.
|
|
387
|
+
}
|
|
388
|
+
const runInput = {
|
|
389
|
+
gitDirty: false,
|
|
390
|
+
toolVersion: PACKAGE_VERSION,
|
|
391
|
+
command: 'daemon',
|
|
392
|
+
surface: 'daemon',
|
|
393
|
+
scope: 'all',
|
|
394
|
+
target: this.projectRoot,
|
|
395
|
+
};
|
|
396
|
+
this.leaseRunId = createLedgerRun(this.db.rawDb, runInput, {
|
|
397
|
+
status: 'running',
|
|
398
|
+
projectRoot: this.projectRoot,
|
|
399
|
+
});
|
|
400
|
+
const now = new Date().toISOString();
|
|
401
|
+
patchLedgerRun(this.db.rawDb, this.leaseRunId, {
|
|
402
|
+
startedAt: now,
|
|
403
|
+
heartbeatAt: now,
|
|
404
|
+
runnerPid: process.pid,
|
|
405
|
+
runnerPidStartedAt: new Date(Date.now() - process.uptime() * 1000).toISOString(),
|
|
406
|
+
runnerHost: hostname(),
|
|
407
|
+
});
|
|
408
|
+
const heartbeatMs = Math.max(5000, Math.floor(DAEMON_LEASE_TTL_MS / 3));
|
|
409
|
+
this.heartbeat = setInterval(() => {
|
|
410
|
+
try {
|
|
411
|
+
if (this.leaseRunId && this.db) {
|
|
412
|
+
patchLedgerRun(this.db.rawDb, this.leaseRunId, { heartbeatAt: new Date().toISOString() });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// best-effort heartbeat
|
|
417
|
+
}
|
|
418
|
+
}, heartbeatMs);
|
|
419
|
+
this.heartbeat.unref?.();
|
|
420
|
+
}
|
|
421
|
+
startIdleTimer() {
|
|
422
|
+
this.idleTimer = setInterval(() => this.checkIdle(), 1000);
|
|
423
|
+
this.idleTimer.unref?.();
|
|
424
|
+
}
|
|
425
|
+
checkIdle() {
|
|
426
|
+
if (this.shutdownRequested)
|
|
427
|
+
return;
|
|
428
|
+
if (this.activeConnections > 0)
|
|
429
|
+
return;
|
|
430
|
+
if (this.status !== 'ready')
|
|
431
|
+
return;
|
|
432
|
+
if (Date.now() - this.lastActivityAt < this.idleTimeoutMs)
|
|
433
|
+
return;
|
|
434
|
+
void this.shutdown('idle timeout');
|
|
435
|
+
}
|
|
436
|
+
async seed() {
|
|
437
|
+
this.status = 'indexing';
|
|
438
|
+
this.seedStartedAt = Date.now();
|
|
439
|
+
this.markActivity();
|
|
440
|
+
const discovered = await discoverFilesDetailed(this.projectRoot, {
|
|
441
|
+
includePaths: this.config?.includePaths,
|
|
442
|
+
excludePaths: this.config?.excludePaths,
|
|
443
|
+
});
|
|
444
|
+
// Classify the corpus up front (the same `getAdapterForFile` the pipeline's
|
|
445
|
+
// stage 1 uses) so the two-population ETA knows the source/orphan split
|
|
446
|
+
// before the first sample.
|
|
447
|
+
const registry = LanguageRegistry.getInstance();
|
|
448
|
+
let sourceTotal = 0;
|
|
449
|
+
for (const file of discovered.files) {
|
|
450
|
+
if (registry.getAdapterForFile(file))
|
|
451
|
+
sourceTotal++;
|
|
452
|
+
}
|
|
453
|
+
const orphanTotal = discovered.files.length - sourceTotal;
|
|
454
|
+
this.progress = {
|
|
455
|
+
filesIndexed: 0,
|
|
456
|
+
filesTotal: discovered.files.length,
|
|
457
|
+
sourceTotal,
|
|
458
|
+
orphanTotal,
|
|
459
|
+
};
|
|
460
|
+
// Reset the phase machine for this seed.
|
|
461
|
+
this.phase = 'files';
|
|
462
|
+
this.phaseCurrent = 0;
|
|
463
|
+
this.phaseTotal = discovered.files.length;
|
|
464
|
+
this.phaseStartedAt = Date.now();
|
|
465
|
+
this.orphanStartedAt = 0;
|
|
466
|
+
this.seenOrphan = false;
|
|
467
|
+
this.emitState();
|
|
468
|
+
const start = Date.now();
|
|
469
|
+
const result = await runAuditDispatch({
|
|
470
|
+
projectRoot: this.projectRoot,
|
|
471
|
+
configName: this.configName,
|
|
472
|
+
progressCallback: (p) => this.onSeedProgress(p),
|
|
473
|
+
});
|
|
474
|
+
const durationMs = Date.now() - start;
|
|
475
|
+
// Finalize phase: split + hash + persist are the last work before `ready`.
|
|
476
|
+
this.setPhase('finalize', 0, discovered.files.length);
|
|
477
|
+
this.emitState();
|
|
478
|
+
const split = splitFindings(result.analyzerResults, this.projectRoot);
|
|
479
|
+
const files = hashAndStatFiles(discovered.files, this.projectRoot, (done) => {
|
|
480
|
+
this.setPhase('finalize', done, discovered.files.length);
|
|
481
|
+
this.emitState();
|
|
482
|
+
});
|
|
483
|
+
this.snapshot = {
|
|
484
|
+
version: 1,
|
|
485
|
+
projectRoot: this.projectRoot,
|
|
486
|
+
files,
|
|
487
|
+
visitorFindings: split.visitorFindings,
|
|
488
|
+
corpusFindings: split.corpusFindings,
|
|
489
|
+
schemaFindings: split.schemaFindings,
|
|
490
|
+
};
|
|
491
|
+
this.persistCurrent(flattenSnapshot(this.snapshot), durationMs);
|
|
492
|
+
}
|
|
493
|
+
/** Persist the current findings to the ledger (durable snapshot for the CLI). */
|
|
494
|
+
persistCurrent(violations, durationMs) {
|
|
495
|
+
if (!this.db)
|
|
496
|
+
return;
|
|
497
|
+
const runInput = {
|
|
498
|
+
gitDirty: false,
|
|
499
|
+
toolVersion: PACKAGE_VERSION,
|
|
500
|
+
command: 'daemon-audit',
|
|
501
|
+
surface: 'daemon',
|
|
502
|
+
scope: 'all',
|
|
503
|
+
target: this.projectRoot,
|
|
504
|
+
};
|
|
505
|
+
const runId = writeAuditToLedger(this.db.rawDb, runInput, violations, durationMs, 0);
|
|
506
|
+
if (this.snapshot) {
|
|
507
|
+
const fsh = hashFileSet(Object.keys(this.snapshot.files).map((r) => path.join(this.projectRoot, r)), this.projectRoot);
|
|
508
|
+
patchLedgerRun(this.db.rawDb, runId, {
|
|
509
|
+
contentHash: fsh.contentHash,
|
|
510
|
+
filesCount: fsh.filesCount,
|
|
511
|
+
fileManifestJson: JSON.stringify(fsh.manifest),
|
|
512
|
+
});
|
|
513
|
+
if (this.leaseRunId) {
|
|
514
|
+
patchLedgerRun(this.db.rawDb, this.leaseRunId, {
|
|
515
|
+
contentHash: fsh.contentHash,
|
|
516
|
+
filesCount: fsh.filesCount,
|
|
517
|
+
fileManifestJson: JSON.stringify(fsh.manifest),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
startWatcher() {
|
|
523
|
+
try {
|
|
524
|
+
this.watcher = watch(this.projectRoot, { recursive: true }, (eventType, filename) => {
|
|
525
|
+
if (!filename)
|
|
526
|
+
return; // unknown change — R4 read-time check is the backstop
|
|
527
|
+
const abs = path.join(this.projectRoot, filename.toString());
|
|
528
|
+
// Ignore infra/non-source noise before it counts as activity or queues a
|
|
529
|
+
// reconcile. The daemon's own heartbeat writes the lease row into the
|
|
530
|
+
// SQLite DB under `node_modules/.cache` every ~20s; without this filter
|
|
531
|
+
// that would (a) reset the idle timer forever and (b) re-audit on every
|
|
532
|
+
// heartbeat. Only a source-path change is a corpus change.
|
|
533
|
+
if (!this.isSourcePath(abs))
|
|
534
|
+
return;
|
|
535
|
+
this.markActivity();
|
|
536
|
+
this.pendingWatchPaths.add(abs);
|
|
537
|
+
if (this.watchDebounce)
|
|
538
|
+
clearTimeout(this.watchDebounce);
|
|
539
|
+
this.watchDebounce = setTimeout(() => void this.reconcile(), WATCH_DEBOUNCE_MS);
|
|
540
|
+
this.watchDebounce.unref?.();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
// Linux < 20 (or other) may not support recursive watch; the R4 read-time
|
|
545
|
+
// staleness check remains the correctness backstop. Log and continue.
|
|
546
|
+
console.error('[code-auditor-daemon] recursive fs.watch unavailable; relying on read-time staleness');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/** Warm re-audit of the files the watcher flagged. */
|
|
550
|
+
async reconcile() {
|
|
551
|
+
if (this.shutdownRequested || !this.snapshot)
|
|
552
|
+
return;
|
|
553
|
+
const paths = [...this.pendingWatchPaths];
|
|
554
|
+
this.pendingWatchPaths.clear();
|
|
555
|
+
if (paths.length === 0)
|
|
556
|
+
return;
|
|
557
|
+
if (this.reindexing) {
|
|
558
|
+
// A reconcile is already running — requeue and it will pick these up via
|
|
559
|
+
// the next diff, or via read-time staleness.
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
this.reindexing = true;
|
|
563
|
+
const prevStatus = this.status;
|
|
564
|
+
this.status = 'reindexing';
|
|
565
|
+
this.emitState();
|
|
566
|
+
this.markActivity();
|
|
567
|
+
try {
|
|
568
|
+
// Classify each affected path as changed/added/deleted against the record.
|
|
569
|
+
const changed = [];
|
|
570
|
+
const added = [];
|
|
571
|
+
const deleted = [];
|
|
572
|
+
const freshFiles = {};
|
|
573
|
+
for (const abs of paths) {
|
|
574
|
+
// R5 — the watcher sees *everything* under the root, including the
|
|
575
|
+
// SQLite DB + WAL/SHM files this daemon writes to `node_modules/.cache`.
|
|
576
|
+
// A bare "something changed" signal would re-audit (near the full
|
|
577
|
+
// corpus) on every persist and loop. Only source paths are a corpus
|
|
578
|
+
// change; drop infra + non-source noise before classification.
|
|
579
|
+
if (!this.isSourcePath(abs))
|
|
580
|
+
continue;
|
|
581
|
+
const key = path.relative(this.projectRoot, abs);
|
|
582
|
+
const rec = this.snapshot.files[key];
|
|
583
|
+
if (!existsSync(abs)) {
|
|
584
|
+
if (rec)
|
|
585
|
+
deleted.push(key);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const rec2 = hashAndStatFiles([abs], this.projectRoot);
|
|
589
|
+
const entry = rec2[key];
|
|
590
|
+
if (!entry)
|
|
591
|
+
continue;
|
|
592
|
+
freshFiles[key] = entry;
|
|
593
|
+
if (!rec)
|
|
594
|
+
added.push(key);
|
|
595
|
+
else if (rec.hash !== entry.hash)
|
|
596
|
+
changed.push(key);
|
|
597
|
+
}
|
|
598
|
+
// Update the recorded file set for the affected files.
|
|
599
|
+
for (const key of Object.keys(freshFiles))
|
|
600
|
+
this.snapshot.files[key] = freshFiles[key];
|
|
601
|
+
for (const key of deleted)
|
|
602
|
+
delete this.snapshot.files[key];
|
|
603
|
+
if (changed.length === 0 && added.length === 0 && deleted.length === 0) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const scopeFiles = [...changed, ...added].map((r) => path.join(this.projectRoot, r));
|
|
607
|
+
const start = Date.now();
|
|
608
|
+
const result = await runAuditDispatch({
|
|
609
|
+
projectRoot: this.projectRoot,
|
|
610
|
+
configName: this.configName,
|
|
611
|
+
scope: scopeFiles.length > 0 ? scopeFiles : undefined,
|
|
612
|
+
});
|
|
613
|
+
const durationMs = Date.now() - start;
|
|
614
|
+
const fresh = splitFindings(result.analyzerResults, this.projectRoot);
|
|
615
|
+
// Schema-catalog hazard (mirrors next-file): a scoped run rebuilds the
|
|
616
|
+
// known-tables catalog from only the changed files, so a table-definition
|
|
617
|
+
// change can falsely flag unchanged query files `unknown-table`. Escalate
|
|
618
|
+
// to a full re-seed rather than serve silently-wrong schema findings.
|
|
619
|
+
const schemaCatalogTouched = (result.metadata?.tableCatalog?.length ?? 0) > 0;
|
|
620
|
+
const schemaDefinitionChanged = [...changed, ...added, ...deleted].some((rel) => SCHEMA_DEFINITION_EXTENSIONS.has(rel.slice(rel.lastIndexOf('.'))));
|
|
621
|
+
if (schemaCatalogTouched || schemaDefinitionChanged) {
|
|
622
|
+
await this.seed();
|
|
623
|
+
this.emitFindings();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const merged = mergeFindings({
|
|
627
|
+
cachedVisitor: this.snapshot.visitorFindings,
|
|
628
|
+
freshVisitor: fresh.visitorFindings,
|
|
629
|
+
cachedCorpus: this.snapshot.corpusFindings,
|
|
630
|
+
freshCorpus: fresh.corpusFindings,
|
|
631
|
+
// Schema findings are fact-based and rebuilt from a full corpus; a scoped
|
|
632
|
+
// run only sees the changed files' facts, so its schema output is partial.
|
|
633
|
+
// Preserve the cached set (mirrors `runNextFile`) — the escalation above
|
|
634
|
+
// already re-seeded when a schema-definition file changed.
|
|
635
|
+
freshSchema: this.snapshot.schemaFindings,
|
|
636
|
+
changed,
|
|
637
|
+
added,
|
|
638
|
+
deleted,
|
|
639
|
+
});
|
|
640
|
+
this.snapshot.visitorFindings = merged.visitorFindings;
|
|
641
|
+
this.snapshot.corpusFindings = merged.corpusFindings;
|
|
642
|
+
this.snapshot.schemaFindings = merged.schemaFindings;
|
|
643
|
+
this.persistCurrent(flattenSnapshot(this.snapshot), durationMs);
|
|
644
|
+
this.emitFindings();
|
|
645
|
+
}
|
|
646
|
+
finally {
|
|
647
|
+
this.reindexing = false;
|
|
648
|
+
if (!this.shutdownRequested)
|
|
649
|
+
this.status = prevStatus;
|
|
650
|
+
this.markActivity();
|
|
651
|
+
this.emitState();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
emitState() {
|
|
655
|
+
const state = this.getState();
|
|
656
|
+
this.emit('state', state);
|
|
657
|
+
}
|
|
658
|
+
emitFindings() {
|
|
659
|
+
if (this.snapshot)
|
|
660
|
+
this.emit('findings', this.snapshot);
|
|
661
|
+
}
|
|
662
|
+
async shutdown(reason = 'shutdown') {
|
|
663
|
+
if (this.shutdownRequested)
|
|
664
|
+
return;
|
|
665
|
+
this.shutdownRequested = true;
|
|
666
|
+
this.status = 'shutting-down';
|
|
667
|
+
this.emitState();
|
|
668
|
+
if (this.watchDebounce)
|
|
669
|
+
clearTimeout(this.watchDebounce);
|
|
670
|
+
if (this.watcher)
|
|
671
|
+
this.watcher.close();
|
|
672
|
+
if (this.heartbeat)
|
|
673
|
+
clearInterval(this.heartbeat);
|
|
674
|
+
if (this.idleTimer)
|
|
675
|
+
clearInterval(this.idleTimer);
|
|
676
|
+
if (this.db && this.leaseRunId) {
|
|
677
|
+
try {
|
|
678
|
+
patchLedgerRun(this.db.rawDb, this.leaseRunId, {
|
|
679
|
+
status: 'completed',
|
|
680
|
+
finishedAt: new Date().toISOString(),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
// best-effort
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
this.emit('shutdown', reason);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function sha256(data) {
|
|
691
|
+
return createHash('sha256').update(data).digest('hex');
|
|
692
|
+
}
|
|
693
|
+
function flattenSnapshot(s) {
|
|
694
|
+
return [
|
|
695
|
+
...Object.values(s.visitorFindings).flat(),
|
|
696
|
+
...s.corpusFindings,
|
|
697
|
+
...s.schemaFindings,
|
|
698
|
+
];
|
|
699
|
+
}
|
|
700
|
+
//# sourceMappingURL=core.js.map
|