code-auditor-mcp 3.4.9 → 3.4.11
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 +10 -3
- package/CHANGELOG.md +81 -0
- package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.js +2 -1
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +0 -2
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +74 -99
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.d.ts +1 -5
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +13 -69
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts +1 -6
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.js +16 -41
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts +20 -3
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +4 -165
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +43 -69
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +62 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +357 -3
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +81 -69
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +319 -325
- 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 +32 -37
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/auditRunner.d.ts +2 -7
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +306 -434
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +27 -5
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts +22 -0
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +92 -44
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/config/defaults.js +3 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/conventions/conventionMiner.d.ts +5 -4
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +65 -25
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/dataPaths.js +1 -1
- package/dist/dataPaths.js.map +1 -1
- package/dist/invariants/index.d.ts +1 -1
- package/dist/invariants/index.d.ts.map +1 -1
- package/dist/invariants/index.js +1 -1
- package/dist/invariants/index.js.map +1 -1
- package/dist/invariants/ruleEngine.d.ts +38 -3
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +42 -174
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts +2 -2
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js +3 -1
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +60 -16
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js +10 -6
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp.js +0 -0
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +4 -1
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/pipeline.d.ts +74 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +620 -0
- package/dist/pipeline.js.bak +465 -0
- package/dist/pipeline.js.bak2 +465 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/pipelineAdapters.d.ts +116 -0
- package/dist/pipelineAdapters.d.ts.map +1 -0
- package/dist/pipelineAdapters.js +1146 -0
- package/dist/pipelineAdapters.js.map +1 -0
- package/dist/pipelineTypes.d.ts +44 -0
- package/dist/pipelineTypes.d.ts.map +1 -0
- package/dist/pipelineTypes.js +118 -0
- package/dist/pipelineTypes.js.map +1 -0
- package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
- package/dist/reporting/csvReportGenerator.js +2 -1
- package/dist/reporting/csvReportGenerator.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +4 -2
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +2 -1
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/styles/cssAstExtractor.d.ts +46 -0
- package/dist/styles/cssAstExtractor.d.ts.map +1 -0
- package/dist/styles/cssAstExtractor.js +439 -0
- package/dist/styles/cssAstExtractor.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 +69 -16
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +4 -0
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindProbe.d.ts +2 -0
- package/dist/styles/tailwindProbe.d.ts.map +1 -1
- package/dist/styles/tailwindProbe.js +5 -5
- 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 +7 -0
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/types.d.ts +216 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +4 -0
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +7 -1
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/utils/reactDetection.d.ts.map +1 -1
- package/dist/utils/reactDetection.js +32 -0
- package/dist/utils/reactDetection.js.map +1 -1
- package/package.json +2 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline execution engine (Spec 24).
|
|
3
|
+
*
|
|
4
|
+
* Four sequential stages:
|
|
5
|
+
* 1. Traverse + Parse — one walk, one parse per file
|
|
6
|
+
* 2. Per-file visitors — each AST → all visitors in parallel
|
|
7
|
+
* 3. Corpus reducers — accumulated facts → cross-file analysis
|
|
8
|
+
* 4. Derived reducers — stage 2+3 facts → cross-domain analysis
|
|
9
|
+
*
|
|
10
|
+
* Stage position IS the dependency declaration — no topological sort needed.
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from 'fs/promises';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { performance } from 'perf_hooks';
|
|
15
|
+
import { AuditAbortedError, } from './types.js';
|
|
16
|
+
import { LanguageRegistry } from './languages/LanguageRegistry.js';
|
|
17
|
+
import { discoverFiles } from './utils/fileDiscovery.js';
|
|
18
|
+
import { resolvePathProfile } from './config/pathProfiles.js';
|
|
19
|
+
import { validateFactsDependencies } from './pipelineTypes.js';
|
|
20
|
+
// ── Stage 1: Traverse + Parse ──────────────────────────────────────────────
|
|
21
|
+
async function runStage1(config) {
|
|
22
|
+
const t0 = performance.now();
|
|
23
|
+
let readMs = 0;
|
|
24
|
+
let parseMs = 0;
|
|
25
|
+
// Discover files (or use explicit)
|
|
26
|
+
const projectRoot = config.projectRoot;
|
|
27
|
+
let files;
|
|
28
|
+
if (config.explicitFiles !== undefined) {
|
|
29
|
+
files = config.explicitFiles;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
files = await discoverFiles(projectRoot, {
|
|
33
|
+
excludeDirs: ['node_modules', '.next', 'dist', 'build', '.git', 'coverage', '.turbo'],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Group by language adapter
|
|
37
|
+
const registry = LanguageRegistry.getInstance();
|
|
38
|
+
const groups = new Map();
|
|
39
|
+
const orphans = [];
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const adapter = registry.getAdapterForFile(file);
|
|
42
|
+
if (adapter) {
|
|
43
|
+
const list = groups.get(adapter) ?? [];
|
|
44
|
+
list.push(file);
|
|
45
|
+
groups.set(adapter, list);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
orphans.push(file);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Parse each file once
|
|
52
|
+
const tuples = [];
|
|
53
|
+
let parsed = 0;
|
|
54
|
+
for (const [adapter, adapterFiles] of groups) {
|
|
55
|
+
for (const file of adapterFiles) {
|
|
56
|
+
// Check abort
|
|
57
|
+
if (config.abortSignal?.aborted) {
|
|
58
|
+
throw new AuditAbortedError('Audit aborted during stage 1');
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const r0 = performance.now();
|
|
62
|
+
const content = await readFile(file, 'utf-8');
|
|
63
|
+
readMs += performance.now() - r0;
|
|
64
|
+
const p0 = performance.now();
|
|
65
|
+
const ast = await adapter.parse(file, content);
|
|
66
|
+
parseMs += performance.now() - p0;
|
|
67
|
+
tuples.push({
|
|
68
|
+
kind: 'parsed',
|
|
69
|
+
file,
|
|
70
|
+
ast,
|
|
71
|
+
adapter,
|
|
72
|
+
sourceCode: content,
|
|
73
|
+
});
|
|
74
|
+
parsed++;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
// File read/parse errors — skip this file
|
|
78
|
+
// Individual files that fail to parse don't abort the entire run
|
|
79
|
+
if (config.progressCallback) {
|
|
80
|
+
config.progressCallback({
|
|
81
|
+
current: parsed,
|
|
82
|
+
total: files.length,
|
|
83
|
+
analyzer: 'pipeline',
|
|
84
|
+
phase: 'stage1',
|
|
85
|
+
file,
|
|
86
|
+
message: `Skipped: ${err.message}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Include orphan files as raw tuples — no LanguageAdapter available,
|
|
93
|
+
// but visitors that consume raw files can read sourceCode directly.
|
|
94
|
+
for (const file of orphans) {
|
|
95
|
+
try {
|
|
96
|
+
const r0 = performance.now();
|
|
97
|
+
const content = await readFile(file, 'utf-8');
|
|
98
|
+
readMs += performance.now() - r0;
|
|
99
|
+
tuples.push({
|
|
100
|
+
kind: 'raw',
|
|
101
|
+
file,
|
|
102
|
+
ast: null,
|
|
103
|
+
adapter: null,
|
|
104
|
+
sourceCode: content,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (config.progressCallback) {
|
|
109
|
+
config.progressCallback({
|
|
110
|
+
current: orphans.indexOf(file),
|
|
111
|
+
total: orphans.length,
|
|
112
|
+
analyzer: 'pipeline',
|
|
113
|
+
phase: 'stage1',
|
|
114
|
+
file,
|
|
115
|
+
message: `Raw file skipped (read error): ${err.message}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const orphanIncluded = tuples.filter(t => t.kind === 'raw').length;
|
|
121
|
+
if (orphanIncluded > 0 && config.progressCallback) {
|
|
122
|
+
config.progressCallback({
|
|
123
|
+
current: orphanIncluded,
|
|
124
|
+
total: orphanIncluded,
|
|
125
|
+
analyzer: 'pipeline',
|
|
126
|
+
phase: 'stage1',
|
|
127
|
+
message: `${orphanIncluded} file(s) included as raw (no language adapter)`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const duration = performance.now() - t0;
|
|
131
|
+
return {
|
|
132
|
+
tuples,
|
|
133
|
+
fileCount: tuples.length,
|
|
134
|
+
parseDurationMs: duration,
|
|
135
|
+
readDurationMs: readMs,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// ── Stage 2: Per-file visitors ─────────────────────────────────────────────
|
|
139
|
+
async function runStage2(tuples, visitors, config) {
|
|
140
|
+
const t0 = performance.now();
|
|
141
|
+
const visitorResults = new Map();
|
|
142
|
+
const allFacts = new Map();
|
|
143
|
+
const indexFacts = [];
|
|
144
|
+
const timingMap = new Map();
|
|
145
|
+
const errors = new Map();
|
|
146
|
+
// Initialize per-visitor state
|
|
147
|
+
for (const visitor of visitors) {
|
|
148
|
+
visitorResults.set(visitor.name, {
|
|
149
|
+
violations: [],
|
|
150
|
+
status: { status: 'visitor-ran', filesProcessed: 0 },
|
|
151
|
+
executionTime: 0,
|
|
152
|
+
analyzerName: visitor.name,
|
|
153
|
+
});
|
|
154
|
+
allFacts.set(visitor.name, {});
|
|
155
|
+
timingMap.set(visitor.name, 0);
|
|
156
|
+
errors.set(visitor.name, []);
|
|
157
|
+
}
|
|
158
|
+
const projectRoot = config.projectRoot;
|
|
159
|
+
const rawConfig = config.config ?? {};
|
|
160
|
+
const infra = rawConfig['_infra'] ?? {};
|
|
161
|
+
const pathProfiles = infra['pathProfiles'];
|
|
162
|
+
const severityOverrides = infra['severityOverrides'] ?? {};
|
|
163
|
+
// Iterate tuples
|
|
164
|
+
for (let i = 0; i < tuples.length; i++) {
|
|
165
|
+
const tuple = tuples[i];
|
|
166
|
+
// Check abort
|
|
167
|
+
if (config.abortSignal?.aborted) {
|
|
168
|
+
throw new AuditAbortedError('Audit aborted during stage 2');
|
|
169
|
+
}
|
|
170
|
+
// Progress
|
|
171
|
+
if (config.progressCallback && i % 10 === 0) {
|
|
172
|
+
config.progressCallback({
|
|
173
|
+
current: i,
|
|
174
|
+
total: tuples.length,
|
|
175
|
+
analyzer: 'pipeline',
|
|
176
|
+
phase: 'stage2',
|
|
177
|
+
file: tuple.file,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// Resolve path profiles for this file (non-analyzer-specific)
|
|
181
|
+
let fileInfra = infra;
|
|
182
|
+
let fileProfileNames = [];
|
|
183
|
+
let fileSeverityCap;
|
|
184
|
+
if (pathProfiles && pathProfiles.length > 0) {
|
|
185
|
+
const resolved = resolvePathProfile(tuple.file, projectRoot, pathProfiles);
|
|
186
|
+
if (Object.keys(resolved.overrides).length > 0) {
|
|
187
|
+
fileInfra = { ...infra, ...resolved.overrides };
|
|
188
|
+
}
|
|
189
|
+
fileProfileNames = resolved.matchedProfileNames;
|
|
190
|
+
fileSeverityCap = resolved.severityCap;
|
|
191
|
+
}
|
|
192
|
+
// Fan out to all visitors for this file — filter by declared extensions
|
|
193
|
+
const fileExt = path.extname(tuple.file);
|
|
194
|
+
for (const visitor of visitors) {
|
|
195
|
+
// Dispatch check: backward-compat visitors see only parsed tuples.
|
|
196
|
+
// Visitors that declare extensions see only tuples whose extension they consume.
|
|
197
|
+
if (visitor.extensions) {
|
|
198
|
+
if (!visitor.extensions.includes(fileExt))
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// No extensions declared → parsed tuples only (backward compat)
|
|
203
|
+
if (tuple.kind !== 'parsed')
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const visitorConfig = { ...(rawConfig[visitor.name] ?? {}), ...fileInfra };
|
|
207
|
+
const visitorContext = {
|
|
208
|
+
projectRoot,
|
|
209
|
+
filePath: tuple.file,
|
|
210
|
+
config: visitorConfig,
|
|
211
|
+
abortSignal: config.abortSignal,
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
const v0 = performance.now();
|
|
215
|
+
const result = await visitor.visit(tuple.ast, tuple.adapter, visitorContext, tuple.sourceCode);
|
|
216
|
+
const vMs = performance.now() - v0;
|
|
217
|
+
// Accumulate timing
|
|
218
|
+
timingMap.set(visitor.name, (timingMap.get(visitor.name) ?? 0) + vMs);
|
|
219
|
+
// Attach profile, severity overrides, analyzer name
|
|
220
|
+
const severityOrder = ['suggestion', 'warning', 'critical'];
|
|
221
|
+
const processedViolations = result.violations
|
|
222
|
+
.map((v) => ({
|
|
223
|
+
...v,
|
|
224
|
+
profile: fileProfileNames.length > 0
|
|
225
|
+
? fileProfileNames[fileProfileNames.length - 1]
|
|
226
|
+
: v.profile,
|
|
227
|
+
severity: (severityOverrides[v.rule] ?? v.severity),
|
|
228
|
+
}))
|
|
229
|
+
// Filter out violations whose severity was overridden to 'off' (Spec-11 R5)
|
|
230
|
+
.filter((v) => v.severity !== 'off')
|
|
231
|
+
// Apply severity cap from path profiles (Spec-20)
|
|
232
|
+
// Applied AFTER severityOverrides so path-level caps beat global promotions
|
|
233
|
+
.map((v) => {
|
|
234
|
+
if (fileSeverityCap) {
|
|
235
|
+
const capIndex = severityOrder.indexOf(fileSeverityCap);
|
|
236
|
+
if (capIndex >= 0 && severityOrder.indexOf(v.severity) > capIndex) {
|
|
237
|
+
return { ...v, severity: fileSeverityCap };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return v;
|
|
241
|
+
});
|
|
242
|
+
// Accumulate violations
|
|
243
|
+
const ar = visitorResults.get(visitor.name);
|
|
244
|
+
ar.violations.push(...processedViolations);
|
|
245
|
+
const prevFiles = ar.status.status === 'visitor-ran' ? ar.status.filesProcessed : 0;
|
|
246
|
+
ar.status = {
|
|
247
|
+
status: 'visitor-ran',
|
|
248
|
+
filesProcessed: prevFiles + 1,
|
|
249
|
+
};
|
|
250
|
+
// Accumulate facts
|
|
251
|
+
if (result.facts && Object.keys(result.facts).length > 0) {
|
|
252
|
+
const existing = allFacts.get(visitor.name) ?? {};
|
|
253
|
+
allFacts.set(visitor.name, { ...existing, ...result.facts });
|
|
254
|
+
}
|
|
255
|
+
// Collect index facts
|
|
256
|
+
if (result.indexFacts && result.indexFacts.length > 0) {
|
|
257
|
+
indexFacts.push(...result.indexFacts);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
// Visitor error on this file — collect but don't abort
|
|
262
|
+
const errs = errors.get(visitor.name);
|
|
263
|
+
errs.push({ file: tuple.file, error: err.message });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Finalize results
|
|
268
|
+
const totalDuration = performance.now() - t0;
|
|
269
|
+
for (const [name, result] of visitorResults) {
|
|
270
|
+
const visitorTime = timingMap.get(name) ?? 0;
|
|
271
|
+
result.executionTime = visitorTime;
|
|
272
|
+
const errList = errors.get(name);
|
|
273
|
+
if (errList && errList.length > 0) {
|
|
274
|
+
result.errors = errList;
|
|
275
|
+
}
|
|
276
|
+
// Convert visitors with declared extensions that matched zero files to notRun.
|
|
277
|
+
// These are infrastructure visitors (e.g. schema-prisma for a project with no
|
|
278
|
+
// .prisma files) — not an error, just nothing to do.
|
|
279
|
+
// Only convert when there are no errors — a visitor that matched files but
|
|
280
|
+
// errored on all of them is a dark-analyzer failure, not benign absence.
|
|
281
|
+
if (result.status.status === 'visitor-ran' &&
|
|
282
|
+
result.status.filesProcessed === 0 &&
|
|
283
|
+
(!result.errors || result.errors.length === 0)) {
|
|
284
|
+
const visitor = visitors.find(v => v.name === name);
|
|
285
|
+
if (visitor?.extensions && visitor.extensions.length > 0) {
|
|
286
|
+
result.status = {
|
|
287
|
+
status: 'notRun',
|
|
288
|
+
reason: `No files matched declared extensions: ${visitor.extensions.join(', ')}`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
visitorResults,
|
|
295
|
+
allFacts,
|
|
296
|
+
indexFacts,
|
|
297
|
+
visitorDurationMs: timingMap,
|
|
298
|
+
fileCount: tuples.length,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// ── Stage 3: Corpus reducers ───────────────────────────────────────────────
|
|
302
|
+
async function runStage3(allFacts, reducers, config, indexHandle) {
|
|
303
|
+
const t0 = performance.now();
|
|
304
|
+
const reducerResults = new Map();
|
|
305
|
+
const reducerFacts = new Map();
|
|
306
|
+
const factsObj = Object.fromEntries(allFacts);
|
|
307
|
+
// Count visitors that produced non-empty facts (not visitor keys in the map)
|
|
308
|
+
const factsConsumed = [...allFacts.values()].filter(f => Object.keys(f).length > 0).length;
|
|
309
|
+
const rawConfig = config.config ?? {};
|
|
310
|
+
const infra = rawConfig['_infra'] ?? {};
|
|
311
|
+
for (const reducer of reducers) {
|
|
312
|
+
if (config.abortSignal?.aborted) {
|
|
313
|
+
throw new AuditAbortedError(`Audit aborted during stage 3 (${reducer.name})`);
|
|
314
|
+
}
|
|
315
|
+
// Per-reducer namespaced config: analyzer namespace + infrastructure
|
|
316
|
+
const reducerConfig = { ...(rawConfig[reducer.name] ?? {}), ...infra };
|
|
317
|
+
const reducerContext = {
|
|
318
|
+
projectRoot: config.projectRoot,
|
|
319
|
+
config: reducerConfig,
|
|
320
|
+
indexHandle,
|
|
321
|
+
abortSignal: config.abortSignal,
|
|
322
|
+
};
|
|
323
|
+
try {
|
|
324
|
+
const r0 = performance.now();
|
|
325
|
+
const result = await reducer.reduce(factsObj, reducerContext);
|
|
326
|
+
const rMs = performance.now() - r0;
|
|
327
|
+
reducerResults.set(reducer.name, {
|
|
328
|
+
violations: result.violations,
|
|
329
|
+
status: { status: 'reducer-ran', factsConsumed: result.factsConsumed ?? factsConsumed },
|
|
330
|
+
executionTime: rMs,
|
|
331
|
+
analyzerName: reducer.name,
|
|
332
|
+
});
|
|
333
|
+
if (result.facts && Object.keys(result.facts).length > 0) {
|
|
334
|
+
reducerFacts.set(reducer.name, result.facts);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
reducerResults.set(reducer.name, {
|
|
339
|
+
violations: [],
|
|
340
|
+
status: {
|
|
341
|
+
status: 'notRun',
|
|
342
|
+
reason: `Reducer error: ${err.message}`,
|
|
343
|
+
},
|
|
344
|
+
executionTime: 0,
|
|
345
|
+
analyzerName: reducer.name,
|
|
346
|
+
errors: [{ file: '(reducer)', error: err.message }],
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
reducerResults,
|
|
352
|
+
reducerFacts,
|
|
353
|
+
durationMs: performance.now() - t0,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
// ── Stage 4: Derived reducers ──────────────────────────────────────────────
|
|
357
|
+
async function runStage4(allFacts, derivedReducers, config, indexHandle) {
|
|
358
|
+
const t0 = performance.now();
|
|
359
|
+
const derivedResults = new Map();
|
|
360
|
+
// Count visitors/reducers that produced non-empty facts (not property keys in the object)
|
|
361
|
+
const factsConsumed = Object.values(allFacts).filter(f => typeof f === 'object' && f !== null && Object.keys(f).length > 0).length;
|
|
362
|
+
const rawConfig = config.config ?? {};
|
|
363
|
+
const infra = rawConfig['_infra'] ?? {};
|
|
364
|
+
for (const dr of derivedReducers) {
|
|
365
|
+
if (config.abortSignal?.aborted) {
|
|
366
|
+
throw new AuditAbortedError(`Audit aborted during stage 4 (${dr.name})`);
|
|
367
|
+
}
|
|
368
|
+
// Per-reducer namespaced config: analyzer namespace + infrastructure
|
|
369
|
+
const reducerConfig = { ...(rawConfig[dr.name] ?? {}), ...infra };
|
|
370
|
+
const reducerContext = {
|
|
371
|
+
projectRoot: config.projectRoot,
|
|
372
|
+
config: reducerConfig,
|
|
373
|
+
indexHandle,
|
|
374
|
+
abortSignal: config.abortSignal,
|
|
375
|
+
};
|
|
376
|
+
try {
|
|
377
|
+
const r0 = performance.now();
|
|
378
|
+
const result = await dr.reduce(allFacts, reducerContext);
|
|
379
|
+
const rMs = performance.now() - r0;
|
|
380
|
+
derivedResults.set(dr.name, {
|
|
381
|
+
violations: result.violations,
|
|
382
|
+
status: { status: 'reducer-ran', factsConsumed: result.factsConsumed ?? factsConsumed },
|
|
383
|
+
executionTime: rMs,
|
|
384
|
+
analyzerName: dr.name,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
derivedResults.set(dr.name, {
|
|
389
|
+
violations: [],
|
|
390
|
+
status: {
|
|
391
|
+
status: 'notRun',
|
|
392
|
+
reason: `Derived reducer error: ${err.message}`,
|
|
393
|
+
},
|
|
394
|
+
executionTime: 0,
|
|
395
|
+
analyzerName: dr.name,
|
|
396
|
+
errors: [{ file: '(derived-reducer)', error: err.message }],
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
derivedResults,
|
|
402
|
+
durationMs: performance.now() - t0,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
// ── Main pipeline entry ────────────────────────────────────────────────────
|
|
406
|
+
/**
|
|
407
|
+
* Run the full 4-stage pipeline.
|
|
408
|
+
*
|
|
409
|
+
* @param config Pipeline configuration with visitors, reducers, and derived reducers.
|
|
410
|
+
* @param indexHandle Optional DB handle for reducers (in-memory overlay for scoped runs).
|
|
411
|
+
* @returns PipelineResult with analyzerResults and metadata.
|
|
412
|
+
*/
|
|
413
|
+
export async function runPipeline(config, indexHandle) {
|
|
414
|
+
const totalT0 = performance.now();
|
|
415
|
+
const stageTiming = {};
|
|
416
|
+
const visitors = config.visitors ?? [];
|
|
417
|
+
const reducers = config.reducers ?? [];
|
|
418
|
+
const derivedReducers = config.derivedReducers ?? [];
|
|
419
|
+
// ── Validate facts dependencies ──────────────────────────────────────────
|
|
420
|
+
const depErrors = validateFactsDependencies(visitors, reducers, derivedReducers);
|
|
421
|
+
if (depErrors.length > 0) {
|
|
422
|
+
throw new Error(`Pipeline facts dependency errors:\n${depErrors.map((e) => ` - ${e}`).join('\n')}`);
|
|
423
|
+
}
|
|
424
|
+
// ── Stage 1: Traverse + Parse ────────────────────────────────────────────
|
|
425
|
+
const stage1 = await runStage1(config);
|
|
426
|
+
stageTiming['stage1-parse'] = stage1.parseDurationMs;
|
|
427
|
+
if (config.progressCallback) {
|
|
428
|
+
config.progressCallback({
|
|
429
|
+
current: stage1.fileCount,
|
|
430
|
+
total: stage1.fileCount,
|
|
431
|
+
analyzer: 'pipeline',
|
|
432
|
+
phase: 'stage1-complete',
|
|
433
|
+
message: `${stage1.fileCount} files parsed in ${stage1.parseDurationMs.toFixed(0)}ms`,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// ── Stage 2: Per-file visitors ───────────────────────────────────────────
|
|
437
|
+
const stage2 = await runStage2(stage1.tuples, visitors, config);
|
|
438
|
+
stageTiming['stage2-visitors'] = performance.now() - totalT0 - stage1.parseDurationMs;
|
|
439
|
+
if (config.progressCallback) {
|
|
440
|
+
config.progressCallback({
|
|
441
|
+
current: stage2.fileCount,
|
|
442
|
+
total: stage2.fileCount,
|
|
443
|
+
analyzer: 'pipeline',
|
|
444
|
+
phase: 'stage2-complete',
|
|
445
|
+
message: `${visitors.length} visitors ran on ${stage2.fileCount} files`,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
// ── Flush index facts to DB ──────────────────────────────────────────────────
|
|
449
|
+
// Stage 2 visitors collect IndexFactsEntry records (function-index,
|
|
450
|
+
// schema-code etc.). Flush them now so downstream reducers and
|
|
451
|
+
// the onStage2Complete hook (which mines conventions from the functions
|
|
452
|
+
// table) can read the freshly-populated DB. Without this flush, cold runs
|
|
453
|
+
// drop conventions and cross-domain results because the post-pipeline
|
|
454
|
+
// writeIndexFactsToDb call at auditRunner.ts:545 happens too late.
|
|
455
|
+
if (indexHandle && stage2.indexFacts.length > 0) {
|
|
456
|
+
writeIndexFactsToDb(indexHandle, stage2.indexFacts);
|
|
457
|
+
// Clear flushed facts so the post-pipeline flush (auditRunner.ts:545)
|
|
458
|
+
// doesn't double-insert them.
|
|
459
|
+
stage2.indexFacts = [];
|
|
460
|
+
}
|
|
461
|
+
// ── Post-stage-2 setup hook ────────────────────────────────────────────────
|
|
462
|
+
// DB operations that depend on Stage 2 output (e.g. rebuilding function_calls
|
|
463
|
+
// from the functions table, mining conventions) run here before Stage 3.
|
|
464
|
+
if (config.onStage2Complete) {
|
|
465
|
+
await config.onStage2Complete({ allFacts: stage2.allFacts });
|
|
466
|
+
}
|
|
467
|
+
// ── Stage 3: Corpus reducers ─────────────────────────────────────────────
|
|
468
|
+
const stage3T0 = performance.now();
|
|
469
|
+
const stage3 = await runStage3(stage2.allFacts, reducers, config, indexHandle);
|
|
470
|
+
stageTiming['stage3-reducers'] = performance.now() - stage3T0;
|
|
471
|
+
// Merge stage 2 + stage 3 facts for stage 4
|
|
472
|
+
const combinedFacts = {};
|
|
473
|
+
for (const [name, facts] of stage2.allFacts) {
|
|
474
|
+
combinedFacts[name] = facts;
|
|
475
|
+
}
|
|
476
|
+
for (const [name, facts] of stage3.reducerFacts) {
|
|
477
|
+
combinedFacts[name] = facts;
|
|
478
|
+
}
|
|
479
|
+
// ── Stage 4: Derived reducers ────────────────────────────────────────────
|
|
480
|
+
const stage4T0 = performance.now();
|
|
481
|
+
const stage4 = await runStage4(combinedFacts, derivedReducers, config, indexHandle);
|
|
482
|
+
stageTiming['stage4-derived'] = performance.now() - stage4T0;
|
|
483
|
+
// ── Build result ─────────────────────────────────────────────────────────
|
|
484
|
+
const analyzerResults = {};
|
|
485
|
+
// Stage 2 results
|
|
486
|
+
for (const [name, result] of stage2.visitorResults) {
|
|
487
|
+
analyzerResults[name] = result;
|
|
488
|
+
}
|
|
489
|
+
// Stage 3 results
|
|
490
|
+
for (const [name, result] of stage3.reducerResults) {
|
|
491
|
+
analyzerResults[name] = result;
|
|
492
|
+
}
|
|
493
|
+
// Stage 4 results
|
|
494
|
+
for (const [name, result] of stage4.derivedResults) {
|
|
495
|
+
analyzerResults[name] = result;
|
|
496
|
+
}
|
|
497
|
+
// Collect diagnostics
|
|
498
|
+
const diagnostics = [];
|
|
499
|
+
for (const [name, result] of Object.entries(analyzerResults)) {
|
|
500
|
+
if (result.status && result.status.status === 'notRun') {
|
|
501
|
+
diagnostics.push({
|
|
502
|
+
analyzerName: name,
|
|
503
|
+
kind: 'not-run',
|
|
504
|
+
message: result.status.reason,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const totalDuration = performance.now() - totalT0;
|
|
509
|
+
return {
|
|
510
|
+
analyzerResults,
|
|
511
|
+
metadata: {
|
|
512
|
+
auditDuration: totalDuration,
|
|
513
|
+
filesAnalyzed: stage1.fileCount,
|
|
514
|
+
stageTiming,
|
|
515
|
+
scoped: config.isScoped,
|
|
516
|
+
diagnostics,
|
|
517
|
+
},
|
|
518
|
+
indexFacts: stage2.indexFacts,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Run pipeline with index fact handling.
|
|
523
|
+
*
|
|
524
|
+
* For scoped runs (changed, path-filtered), index facts are kept in-memory.
|
|
525
|
+
* For full runs (plain audit), index facts are written to persistent storage
|
|
526
|
+
* via the provided writeFn.
|
|
527
|
+
*/
|
|
528
|
+
// ── Index facts persistence ────────────────────────────────────────────────
|
|
529
|
+
/**
|
|
530
|
+
* Write IndexFactsEntry records to the DB via an IndexHandle.
|
|
531
|
+
*
|
|
532
|
+
* Handles special actions:
|
|
533
|
+
* - `_action: 'clear-by-file'` → DELETE FROM table WHERE file_path = ?
|
|
534
|
+
* - Normal entries → INSERT ... ON CONFLICT DO UPDATE (upsert)
|
|
535
|
+
*
|
|
536
|
+
* Spec 25 B4 — moved from analyzer-side CodeIndexDB calls to a post-pipeline
|
|
537
|
+
* write step so visitors never open the database directly.
|
|
538
|
+
*/
|
|
539
|
+
export function writeIndexFactsToDb(handle, facts) {
|
|
540
|
+
for (const fact of facts) {
|
|
541
|
+
const data = fact.data;
|
|
542
|
+
// Special action: clear rows by file path
|
|
543
|
+
if (data._action === 'clear-by-file') {
|
|
544
|
+
handle.run(`DELETE FROM ${fact.table} WHERE file_path = ?`, [data.file_path]);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
// Normal upsert
|
|
548
|
+
const columns = Object.keys(data).filter(k => !k.startsWith('_'));
|
|
549
|
+
const values = columns.map(k => data[k]);
|
|
550
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
551
|
+
if (fact.conflictKey) {
|
|
552
|
+
const keyCols = fact.conflictKey.split(',').map(s => s.trim());
|
|
553
|
+
const updateCols = columns.filter(c => !keyCols.includes(c));
|
|
554
|
+
const updates = updateCols.map(c => `"${c}" = excluded."${c}"`).join(', ');
|
|
555
|
+
handle.run(`INSERT INTO ${fact.table} ("${columns.join('", "')}") VALUES (${placeholders}) ON CONFLICT (${keyCols.join(', ')}) DO UPDATE SET ${updates}`, values);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
// Normal insert — no conflictKey (caller handles dedup via clear-by-file or similar)
|
|
559
|
+
handle.run(`INSERT INTO ${fact.table} ("${columns.join('", "')}") VALUES (${placeholders})`, values);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
export async function runPipelineWithIndex(config, indexHandle, writeIndexFacts) {
|
|
564
|
+
const result = await runPipeline(config, indexHandle);
|
|
565
|
+
// Spec 25 B4 — Write index facts when a persister is supplied.
|
|
566
|
+
// The pipeline's stage 2 collects IndexFactsEntry records from visitors;
|
|
567
|
+
// the caller provides the write mechanism (DB, overlay, etc.).
|
|
568
|
+
if (writeIndexFacts && result.indexFacts && result.indexFacts.length > 0) {
|
|
569
|
+
await writeIndexFacts(result.indexFacts);
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
result,
|
|
573
|
+
indexFacts: result.indexFacts ?? [],
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
// ── Status factory functions ──────────────────────────────────────────────
|
|
577
|
+
/**
|
|
578
|
+
* Canonical factory for visitor status objects.
|
|
579
|
+
* All code outside types.ts and pipeline.ts MUST use these factories
|
|
580
|
+
* instead of hand-constructing status discriminants.
|
|
581
|
+
*/
|
|
582
|
+
export function makeVisitorStatus(filesProcessed) {
|
|
583
|
+
return { status: 'visitor-ran', filesProcessed };
|
|
584
|
+
}
|
|
585
|
+
export function makeReducerStatus(factsConsumed) {
|
|
586
|
+
return { status: 'reducer-ran', factsConsumed };
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Accessor: extract filesProcessed from any AnalyzerStatus.
|
|
590
|
+
* Returns 0 for non-visitor statuses (reducer-ran, notRun, etc.).
|
|
591
|
+
*/
|
|
592
|
+
export function getFilesProcessed(status) {
|
|
593
|
+
if (status.status === 'visitor-ran') {
|
|
594
|
+
return status.filesProcessed;
|
|
595
|
+
}
|
|
596
|
+
return 0;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Accessor: extract factsConsumed from any AnalyzerStatus.
|
|
600
|
+
* Returns 0 for non-reducer statuses.
|
|
601
|
+
*/
|
|
602
|
+
export function getFactsConsumed(status) {
|
|
603
|
+
if (status.status === 'reducer-ran') {
|
|
604
|
+
return status.factsConsumed;
|
|
605
|
+
}
|
|
606
|
+
return 0;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Accessor: type-narrowed check whether this status is visitor-ran.
|
|
610
|
+
*/
|
|
611
|
+
export function isVisitorStatus(status) {
|
|
612
|
+
return status.status === 'visitor-ran';
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Accessor: type-narrowed check whether this status is reducer-ran.
|
|
616
|
+
*/
|
|
617
|
+
export function isReducerStatus(status) {
|
|
618
|
+
return status.status === 'reducer-ran';
|
|
619
|
+
}
|
|
620
|
+
//# sourceMappingURL=pipeline.js.map
|