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
|
@@ -0,0 +1,465 @@
|
|
|
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 { performance } from 'perf_hooks';
|
|
14
|
+
import { AuditAbortedError, } from './types.js';
|
|
15
|
+
import { LanguageRegistry } from './languages/LanguageRegistry.js';
|
|
16
|
+
import { discoverFiles } from './utils/fileDiscovery.js';
|
|
17
|
+
import { resolvePathProfile } from './config/pathProfiles.js';
|
|
18
|
+
import { validateFactsDependencies } from './pipelineTypes.js';
|
|
19
|
+
// ── Stage 1: Traverse + Parse ──────────────────────────────────────────────
|
|
20
|
+
async function runStage1(config) {
|
|
21
|
+
const t0 = performance.now();
|
|
22
|
+
let readMs = 0;
|
|
23
|
+
let parseMs = 0;
|
|
24
|
+
// Discover files (or use explicit)
|
|
25
|
+
const projectRoot = config.projectRoot;
|
|
26
|
+
let files;
|
|
27
|
+
if (config.explicitFiles && config.explicitFiles.length > 0) {
|
|
28
|
+
files = config.explicitFiles;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
files = await discoverFiles(projectRoot, {
|
|
32
|
+
excludeDirs: ['node_modules', '.next', 'dist', 'build', '.git', 'coverage', '.turbo'],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Group by language adapter
|
|
36
|
+
const registry = LanguageRegistry.getInstance();
|
|
37
|
+
const groups = new Map();
|
|
38
|
+
const orphans = [];
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const adapter = registry.getAdapterForFile(file);
|
|
41
|
+
if (adapter) {
|
|
42
|
+
const list = groups.get(adapter) ?? [];
|
|
43
|
+
list.push(file);
|
|
44
|
+
groups.set(adapter, list);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
orphans.push(file);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Parse each file once
|
|
51
|
+
const tuples = [];
|
|
52
|
+
let parsed = 0;
|
|
53
|
+
for (const [adapter, adapterFiles] of groups) {
|
|
54
|
+
for (const file of adapterFiles) {
|
|
55
|
+
// Check abort
|
|
56
|
+
if (config.abortSignal?.aborted) {
|
|
57
|
+
throw new AuditAbortedError('Audit aborted during stage 1');
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const r0 = performance.now();
|
|
61
|
+
const content = await readFile(file, 'utf-8');
|
|
62
|
+
readMs += performance.now() - r0;
|
|
63
|
+
const p0 = performance.now();
|
|
64
|
+
const ast = await adapter.parse(file, content);
|
|
65
|
+
parseMs += performance.now() - p0;
|
|
66
|
+
tuples.push({
|
|
67
|
+
file,
|
|
68
|
+
ast,
|
|
69
|
+
adapter,
|
|
70
|
+
sourceCode: content,
|
|
71
|
+
});
|
|
72
|
+
parsed++;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// File read/parse errors — skip this file
|
|
76
|
+
// Individual files that fail to parse don't abort the entire run
|
|
77
|
+
if (config.progressCallback) {
|
|
78
|
+
config.progressCallback({
|
|
79
|
+
current: parsed,
|
|
80
|
+
total: files.length,
|
|
81
|
+
analyzer: 'pipeline',
|
|
82
|
+
phase: 'stage1',
|
|
83
|
+
file,
|
|
84
|
+
message: `Skipped: ${err.message}`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const duration = performance.now() - t0;
|
|
91
|
+
// Log orphan files (no matching language adapter)
|
|
92
|
+
const orphanCount = orphans.length;
|
|
93
|
+
if (orphanCount > 0 && config.progressCallback) {
|
|
94
|
+
config.progressCallback({
|
|
95
|
+
current: orphanCount,
|
|
96
|
+
total: orphans.length,
|
|
97
|
+
analyzer: 'pipeline',
|
|
98
|
+
phase: 'stage1',
|
|
99
|
+
message: `${orphanCount} file(s) skipped (no language adapter)`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
tuples,
|
|
104
|
+
fileCount: tuples.length + orphanCount,
|
|
105
|
+
parseDurationMs: duration,
|
|
106
|
+
readDurationMs: readMs,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// ── Stage 2: Per-file visitors ─────────────────────────────────────────────
|
|
110
|
+
async function runStage2(tuples, visitors, config) {
|
|
111
|
+
const t0 = performance.now();
|
|
112
|
+
const visitorResults = new Map();
|
|
113
|
+
const allFacts = new Map();
|
|
114
|
+
const indexFacts = [];
|
|
115
|
+
const timingMap = new Map();
|
|
116
|
+
const errors = new Map();
|
|
117
|
+
// Initialize per-visitor state
|
|
118
|
+
for (const visitor of visitors) {
|
|
119
|
+
visitorResults.set(visitor.name, {
|
|
120
|
+
violations: [],
|
|
121
|
+
filesProcessed: 0,
|
|
122
|
+
status: { status: 'ran', filesProcessed: 0 },
|
|
123
|
+
executionTime: 0,
|
|
124
|
+
analyzerName: visitor.name,
|
|
125
|
+
});
|
|
126
|
+
allFacts.set(visitor.name, {});
|
|
127
|
+
timingMap.set(visitor.name, 0);
|
|
128
|
+
errors.set(visitor.name, []);
|
|
129
|
+
}
|
|
130
|
+
const projectRoot = config.projectRoot;
|
|
131
|
+
const pathProfiles = config.config?.pathProfiles;
|
|
132
|
+
const rawConfig = config.config ?? {};
|
|
133
|
+
const severityOverrides = rawConfig.severityOverrides ?? {};
|
|
134
|
+
// Iterate tuples
|
|
135
|
+
for (let i = 0; i < tuples.length; i++) {
|
|
136
|
+
const tuple = tuples[i];
|
|
137
|
+
// Check abort
|
|
138
|
+
if (config.abortSignal?.aborted) {
|
|
139
|
+
throw new AuditAbortedError('Audit aborted during stage 2');
|
|
140
|
+
}
|
|
141
|
+
// Progress
|
|
142
|
+
if (config.progressCallback && i % 10 === 0) {
|
|
143
|
+
config.progressCallback({
|
|
144
|
+
current: i,
|
|
145
|
+
total: tuples.length,
|
|
146
|
+
analyzer: 'pipeline',
|
|
147
|
+
phase: 'stage2',
|
|
148
|
+
file: tuple.file,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Resolve path profiles for this file
|
|
152
|
+
let fileConfig = rawConfig;
|
|
153
|
+
let fileProfileNames = [];
|
|
154
|
+
let fileSeverityCap;
|
|
155
|
+
if (pathProfiles && pathProfiles.length > 0) {
|
|
156
|
+
const resolved = resolvePathProfile(tuple.file, projectRoot, pathProfiles);
|
|
157
|
+
if (Object.keys(resolved.overrides).length > 0) {
|
|
158
|
+
fileConfig = { ...rawConfig, ...resolved.overrides };
|
|
159
|
+
}
|
|
160
|
+
fileProfileNames = resolved.matchedProfileNames;
|
|
161
|
+
fileSeverityCap = resolved.severityCap;
|
|
162
|
+
}
|
|
163
|
+
const visitorContext = {
|
|
164
|
+
projectRoot,
|
|
165
|
+
config: fileConfig,
|
|
166
|
+
abortSignal: config.abortSignal,
|
|
167
|
+
};
|
|
168
|
+
// Fan out to all visitors for this file
|
|
169
|
+
for (const visitor of visitors) {
|
|
170
|
+
try {
|
|
171
|
+
const v0 = performance.now();
|
|
172
|
+
const result = await visitor.visit(tuple.ast, tuple.adapter, visitorContext, tuple.sourceCode);
|
|
173
|
+
const vMs = performance.now() - v0;
|
|
174
|
+
// Accumulate timing
|
|
175
|
+
timingMap.set(visitor.name, (timingMap.get(visitor.name) ?? 0) + vMs);
|
|
176
|
+
// Attach profile, severity overrides, analyzer name
|
|
177
|
+
const severityOrder = ['suggestion', 'warning', 'critical'];
|
|
178
|
+
const processedViolations = result.violations
|
|
179
|
+
.map((v) => ({
|
|
180
|
+
...v,
|
|
181
|
+
analyzer: v.analyzer ?? visitor.name,
|
|
182
|
+
profile: fileProfileNames.length > 0
|
|
183
|
+
? fileProfileNames[fileProfileNames.length - 1]
|
|
184
|
+
: v.profile,
|
|
185
|
+
severity: severityOverrides[v.rule] ?? v.severity,
|
|
186
|
+
}))
|
|
187
|
+
// Filter out violations whose severity was overridden to 'off' (Spec-11 R5)
|
|
188
|
+
.filter((v) => v.severity !== 'off')
|
|
189
|
+
// Apply severity cap from path profiles (Spec-20)
|
|
190
|
+
// Applied AFTER severityOverrides so path-level caps beat global promotions
|
|
191
|
+
.map((v) => {
|
|
192
|
+
if (fileSeverityCap) {
|
|
193
|
+
const capIndex = severityOrder.indexOf(fileSeverityCap);
|
|
194
|
+
if (capIndex >= 0 && severityOrder.indexOf(v.severity) > capIndex) {
|
|
195
|
+
return { ...v, severity: fileSeverityCap };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return v;
|
|
199
|
+
});
|
|
200
|
+
// Accumulate violations
|
|
201
|
+
const ar = visitorResults.get(visitor.name);
|
|
202
|
+
ar.violations.push(...processedViolations);
|
|
203
|
+
ar.status = {
|
|
204
|
+
status: 'ran',
|
|
205
|
+
filesProcessed: (ar.status.filesProcessed ?? 0) + 1,
|
|
206
|
+
};
|
|
207
|
+
// Accumulate facts
|
|
208
|
+
if (result.facts && Object.keys(result.facts).length > 0) {
|
|
209
|
+
const existing = allFacts.get(visitor.name) ?? {};
|
|
210
|
+
allFacts.set(visitor.name, { ...existing, ...result.facts });
|
|
211
|
+
}
|
|
212
|
+
// Collect index facts
|
|
213
|
+
if (result.indexFacts && result.indexFacts.length > 0) {
|
|
214
|
+
indexFacts.push(...result.indexFacts);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
// Visitor error on this file — collect but don't abort
|
|
219
|
+
const errs = errors.get(visitor.name);
|
|
220
|
+
errs.push({ file: tuple.file, error: err.message });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Finalize results
|
|
225
|
+
const totalDuration = performance.now() - t0;
|
|
226
|
+
for (const [name, result] of visitorResults) {
|
|
227
|
+
const visitorTime = timingMap.get(name) ?? 0;
|
|
228
|
+
result.executionTime = visitorTime;
|
|
229
|
+
// Sync filesProcessed from status (which tracks per-file increments)
|
|
230
|
+
// to the top-level field (which runZeroFilesDiagnostics reads directly)
|
|
231
|
+
result.filesProcessed = result.status.filesProcessed;
|
|
232
|
+
const errList = errors.get(name);
|
|
233
|
+
if (errList && errList.length > 0) {
|
|
234
|
+
result.errors = errList;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
visitorResults,
|
|
239
|
+
allFacts,
|
|
240
|
+
indexFacts,
|
|
241
|
+
visitorDurationMs: timingMap,
|
|
242
|
+
fileCount: tuples.length,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// ── Stage 3: Corpus reducers ───────────────────────────────────────────────
|
|
246
|
+
async function runStage3(allFacts, reducers, config, indexHandle) {
|
|
247
|
+
const t0 = performance.now();
|
|
248
|
+
const reducerResults = new Map();
|
|
249
|
+
const reducerFacts = new Map();
|
|
250
|
+
const factsObj = Object.fromEntries(allFacts);
|
|
251
|
+
const factsConsumed = allFacts.size;
|
|
252
|
+
const reducerContext = {
|
|
253
|
+
projectRoot: config.projectRoot,
|
|
254
|
+
config: config.config ?? {},
|
|
255
|
+
indexHandle,
|
|
256
|
+
abortSignal: config.abortSignal,
|
|
257
|
+
};
|
|
258
|
+
for (const reducer of reducers) {
|
|
259
|
+
if (config.abortSignal?.aborted) {
|
|
260
|
+
throw new AuditAbortedError(`Audit aborted during stage 3 (${reducer.name})`);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const r0 = performance.now();
|
|
264
|
+
const result = await reducer.reduce(factsObj, reducerContext);
|
|
265
|
+
const rMs = performance.now() - r0;
|
|
266
|
+
reducerResults.set(reducer.name, {
|
|
267
|
+
violations: result.violations.map((v) => ({
|
|
268
|
+
...v,
|
|
269
|
+
analyzer: v.analyzer ?? reducer.name,
|
|
270
|
+
})),
|
|
271
|
+
status: { status: 'ran', factsConsumed },
|
|
272
|
+
filesProcessed: 0,
|
|
273
|
+
executionTime: rMs,
|
|
274
|
+
analyzerName: reducer.name,
|
|
275
|
+
});
|
|
276
|
+
if (result.facts && Object.keys(result.facts).length > 0) {
|
|
277
|
+
reducerFacts.set(reducer.name, result.facts);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
reducerResults.set(reducer.name, {
|
|
282
|
+
violations: [],
|
|
283
|
+
status: {
|
|
284
|
+
status: 'notRun',
|
|
285
|
+
reason: `Reducer error: ${err.message}`,
|
|
286
|
+
},
|
|
287
|
+
filesProcessed: 0,
|
|
288
|
+
executionTime: 0,
|
|
289
|
+
analyzerName: reducer.name,
|
|
290
|
+
errors: [{ file: '(reducer)', error: err.message }],
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
reducerResults,
|
|
296
|
+
reducerFacts,
|
|
297
|
+
durationMs: performance.now() - t0,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// ── Stage 4: Derived reducers ──────────────────────────────────────────────
|
|
301
|
+
async function runStage4(allFacts, derivedReducers, config, indexHandle) {
|
|
302
|
+
const t0 = performance.now();
|
|
303
|
+
const derivedResults = new Map();
|
|
304
|
+
const factsConsumed = Object.keys(allFacts).length;
|
|
305
|
+
const reducerContext = {
|
|
306
|
+
projectRoot: config.projectRoot,
|
|
307
|
+
config: config.config ?? {},
|
|
308
|
+
indexHandle,
|
|
309
|
+
abortSignal: config.abortSignal,
|
|
310
|
+
};
|
|
311
|
+
for (const dr of derivedReducers) {
|
|
312
|
+
if (config.abortSignal?.aborted) {
|
|
313
|
+
throw new AuditAbortedError(`Audit aborted during stage 4 (${dr.name})`);
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const r0 = performance.now();
|
|
317
|
+
const result = await dr.reduce(allFacts, reducerContext);
|
|
318
|
+
const rMs = performance.now() - r0;
|
|
319
|
+
derivedResults.set(dr.name, {
|
|
320
|
+
violations: result.violations.map((v) => ({
|
|
321
|
+
...v,
|
|
322
|
+
analyzer: v.analyzer ?? dr.name,
|
|
323
|
+
})),
|
|
324
|
+
status: { status: 'ran', factsConsumed },
|
|
325
|
+
filesProcessed: 0,
|
|
326
|
+
executionTime: rMs,
|
|
327
|
+
analyzerName: dr.name,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
derivedResults.set(dr.name, {
|
|
332
|
+
violations: [],
|
|
333
|
+
status: {
|
|
334
|
+
status: 'notRun',
|
|
335
|
+
reason: `Derived reducer error: ${err.message}`,
|
|
336
|
+
},
|
|
337
|
+
filesProcessed: 0,
|
|
338
|
+
executionTime: 0,
|
|
339
|
+
analyzerName: dr.name,
|
|
340
|
+
errors: [{ file: '(derived-reducer)', error: err.message }],
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
derivedResults,
|
|
346
|
+
durationMs: performance.now() - t0,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
// ── Main pipeline entry ────────────────────────────────────────────────────
|
|
350
|
+
/**
|
|
351
|
+
* Run the full 4-stage pipeline.
|
|
352
|
+
*
|
|
353
|
+
* @param config Pipeline configuration with visitors, reducers, and derived reducers.
|
|
354
|
+
* @param indexHandle Optional DB handle for reducers (in-memory overlay for scoped runs).
|
|
355
|
+
* @returns PipelineResult with analyzerResults and metadata.
|
|
356
|
+
*/
|
|
357
|
+
export async function runPipeline(config, indexHandle) {
|
|
358
|
+
const totalT0 = performance.now();
|
|
359
|
+
const stageTiming = {};
|
|
360
|
+
const visitors = config.visitors ?? [];
|
|
361
|
+
const reducers = config.reducers ?? [];
|
|
362
|
+
const derivedReducers = config.derivedReducers ?? [];
|
|
363
|
+
// ── Validate facts dependencies ──────────────────────────────────────────
|
|
364
|
+
const depErrors = validateFactsDependencies(visitors, reducers, derivedReducers);
|
|
365
|
+
if (depErrors.length > 0) {
|
|
366
|
+
throw new Error(`Pipeline facts dependency errors:\n${depErrors.map((e) => ` - ${e}`).join('\n')}`);
|
|
367
|
+
}
|
|
368
|
+
// ── Stage 1: Traverse + Parse ────────────────────────────────────────────
|
|
369
|
+
const stage1 = await runStage1(config);
|
|
370
|
+
stageTiming['stage1-parse'] = stage1.parseDurationMs;
|
|
371
|
+
if (config.progressCallback) {
|
|
372
|
+
config.progressCallback({
|
|
373
|
+
current: stage1.fileCount,
|
|
374
|
+
total: stage1.fileCount,
|
|
375
|
+
analyzer: 'pipeline',
|
|
376
|
+
phase: 'stage1-complete',
|
|
377
|
+
message: `${stage1.fileCount} files parsed in ${stage1.parseDurationMs.toFixed(0)}ms`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// ── Stage 2: Per-file visitors ───────────────────────────────────────────
|
|
381
|
+
const stage2 = await runStage2(stage1.tuples, visitors, config);
|
|
382
|
+
stageTiming['stage2-visitors'] = performance.now() - totalT0 - stage1.parseDurationMs;
|
|
383
|
+
if (config.progressCallback) {
|
|
384
|
+
config.progressCallback({
|
|
385
|
+
current: stage2.fileCount,
|
|
386
|
+
total: stage2.fileCount,
|
|
387
|
+
analyzer: 'pipeline',
|
|
388
|
+
phase: 'stage2-complete',
|
|
389
|
+
message: `${visitors.length} visitors ran on ${stage2.fileCount} files`,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
// ── Stage 3: Corpus reducers ─────────────────────────────────────────────
|
|
393
|
+
const stage3T0 = performance.now();
|
|
394
|
+
const stage3 = await runStage3(stage2.allFacts, reducers, config, indexHandle);
|
|
395
|
+
stageTiming['stage3-reducers'] = performance.now() - stage3T0;
|
|
396
|
+
// Merge stage 2 + stage 3 facts for stage 4
|
|
397
|
+
const combinedFacts = {};
|
|
398
|
+
for (const [name, facts] of stage2.allFacts) {
|
|
399
|
+
combinedFacts[name] = facts;
|
|
400
|
+
}
|
|
401
|
+
for (const [name, facts] of stage3.reducerFacts) {
|
|
402
|
+
combinedFacts[name] = facts;
|
|
403
|
+
}
|
|
404
|
+
// ── Stage 4: Derived reducers ────────────────────────────────────────────
|
|
405
|
+
const stage4T0 = performance.now();
|
|
406
|
+
const stage4 = await runStage4(combinedFacts, derivedReducers, config, indexHandle);
|
|
407
|
+
stageTiming['stage4-derived'] = performance.now() - stage4T0;
|
|
408
|
+
// ── Build result ─────────────────────────────────────────────────────────
|
|
409
|
+
const analyzerResults = {};
|
|
410
|
+
// Stage 2 results
|
|
411
|
+
for (const [name, result] of stage2.visitorResults) {
|
|
412
|
+
analyzerResults[name] = result;
|
|
413
|
+
}
|
|
414
|
+
// Stage 3 results
|
|
415
|
+
for (const [name, result] of stage3.reducerResults) {
|
|
416
|
+
analyzerResults[name] = result;
|
|
417
|
+
}
|
|
418
|
+
// Stage 4 results
|
|
419
|
+
for (const [name, result] of stage4.derivedResults) {
|
|
420
|
+
analyzerResults[name] = result;
|
|
421
|
+
}
|
|
422
|
+
// Collect diagnostics
|
|
423
|
+
const diagnostics = [];
|
|
424
|
+
for (const [name, result] of Object.entries(analyzerResults)) {
|
|
425
|
+
if (result.status && result.status.status === 'notRun') {
|
|
426
|
+
diagnostics.push({
|
|
427
|
+
analyzerName: name,
|
|
428
|
+
kind: 'not-run',
|
|
429
|
+
message: result.status.reason,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const totalDuration = performance.now() - totalT0;
|
|
434
|
+
return {
|
|
435
|
+
analyzerResults,
|
|
436
|
+
metadata: {
|
|
437
|
+
auditDuration: totalDuration,
|
|
438
|
+
filesAnalyzed: stage1.fileCount,
|
|
439
|
+
stageTiming,
|
|
440
|
+
scoped: config.isScoped,
|
|
441
|
+
diagnostics,
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Run pipeline with index fact handling.
|
|
447
|
+
*
|
|
448
|
+
* For scoped runs (changed, path-filtered), index facts are kept in-memory.
|
|
449
|
+
* For full runs (plain audit), index facts are written to persistent storage
|
|
450
|
+
* via the provided writeFn.
|
|
451
|
+
*/
|
|
452
|
+
export async function runPipelineWithIndex(config, indexHandle, writeIndexFacts) {
|
|
453
|
+
const result = await runPipeline(config, indexHandle);
|
|
454
|
+
// Index facts are accumulated during stage 2 and need to be extracted
|
|
455
|
+
// Since runStage2 is internal, we expose a separate path that re-runs
|
|
456
|
+
// or we can modify runPipeline to pass index facts through.
|
|
457
|
+
//
|
|
458
|
+
// For now, we use runPipeline and the caller handles index facts
|
|
459
|
+
// separately through the visitor results.
|
|
460
|
+
return {
|
|
461
|
+
result,
|
|
462
|
+
indexFacts: [],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
//# sourceMappingURL=pipeline.js.map
|