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.
Files changed (130) hide show
  1. package/.claude-plugin/marketplace.json +10 -3
  2. package/CHANGELOG.md +81 -0
  3. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  4. package/dist/analyzers/analyzerUtils.js +2 -1
  5. package/dist/analyzers/analyzerUtils.js.map +1 -1
  6. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +0 -2
  7. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  8. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +74 -99
  9. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  10. package/dist/analyzers/documentationAnalyzer.d.ts +1 -5
  11. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  12. package/dist/analyzers/documentationAnalyzer.js +13 -69
  13. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  14. package/dist/analyzers/invariantsAnalyzer.d.ts +1 -6
  15. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  16. package/dist/analyzers/invariantsAnalyzer.js +16 -41
  17. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  18. package/dist/analyzers/reactAnalyzer.d.ts +20 -3
  19. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  20. package/dist/analyzers/reactAnalyzer.js +4 -165
  21. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  22. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  23. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +43 -69
  24. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  25. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +62 -0
  26. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  27. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +357 -3
  28. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  29. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +81 -69
  30. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  31. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +319 -325
  32. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  33. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  34. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +32 -37
  35. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  36. package/dist/auditRunner.d.ts +2 -7
  37. package/dist/auditRunner.d.ts.map +1 -1
  38. package/dist/auditRunner.js +306 -434
  39. package/dist/auditRunner.js.map +1 -1
  40. package/dist/cli.js +27 -5
  41. package/dist/cli.js.map +1 -1
  42. package/dist/codeIndexDB.d.ts +22 -0
  43. package/dist/codeIndexDB.d.ts.map +1 -1
  44. package/dist/codeIndexDB.js +92 -44
  45. package/dist/codeIndexDB.js.map +1 -1
  46. package/dist/config/defaults.js +3 -3
  47. package/dist/config/defaults.js.map +1 -1
  48. package/dist/conventions/conventionMiner.d.ts +5 -4
  49. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  50. package/dist/conventions/conventionMiner.js +65 -25
  51. package/dist/conventions/conventionMiner.js.map +1 -1
  52. package/dist/dataPaths.js +1 -1
  53. package/dist/dataPaths.js.map +1 -1
  54. package/dist/invariants/index.d.ts +1 -1
  55. package/dist/invariants/index.d.ts.map +1 -1
  56. package/dist/invariants/index.js +1 -1
  57. package/dist/invariants/index.js.map +1 -1
  58. package/dist/invariants/ruleEngine.d.ts +38 -3
  59. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  60. package/dist/invariants/ruleEngine.js +42 -174
  61. package/dist/invariants/ruleEngine.js.map +1 -1
  62. package/dist/languages/UniversalAnalyzer.d.ts +2 -2
  63. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  64. package/dist/languages/UniversalAnalyzer.js +3 -1
  65. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  66. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
  67. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  68. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +60 -16
  69. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  70. package/dist/mcp-tools-shared.d.ts.map +1 -1
  71. package/dist/mcp-tools-shared.js +10 -6
  72. package/dist/mcp-tools-shared.js.map +1 -1
  73. package/dist/mcp.js +0 -0
  74. package/dist/mcpAuditJobs.d.ts.map +1 -1
  75. package/dist/mcpAuditJobs.js +4 -1
  76. package/dist/mcpAuditJobs.js.map +1 -1
  77. package/dist/pipeline.d.ts +74 -0
  78. package/dist/pipeline.d.ts.map +1 -0
  79. package/dist/pipeline.js +620 -0
  80. package/dist/pipeline.js.bak +465 -0
  81. package/dist/pipeline.js.bak2 +465 -0
  82. package/dist/pipeline.js.map +1 -0
  83. package/dist/pipelineAdapters.d.ts +116 -0
  84. package/dist/pipelineAdapters.d.ts.map +1 -0
  85. package/dist/pipelineAdapters.js +1146 -0
  86. package/dist/pipelineAdapters.js.map +1 -0
  87. package/dist/pipelineTypes.d.ts +44 -0
  88. package/dist/pipelineTypes.d.ts.map +1 -0
  89. package/dist/pipelineTypes.js +118 -0
  90. package/dist/pipelineTypes.js.map +1 -0
  91. package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
  92. package/dist/reporting/csvReportGenerator.js +2 -1
  93. package/dist/reporting/csvReportGenerator.js.map +1 -1
  94. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  95. package/dist/reporting/jsonReportGenerator.js +4 -2
  96. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  97. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  98. package/dist/reporting/sarifReportGenerator.js +2 -1
  99. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  100. package/dist/styles/cssAstExtractor.d.ts +46 -0
  101. package/dist/styles/cssAstExtractor.d.ts.map +1 -0
  102. package/dist/styles/cssAstExtractor.js +439 -0
  103. package/dist/styles/cssAstExtractor.js.map +1 -0
  104. package/dist/styles/styleExtractor.d.ts +2 -2
  105. package/dist/styles/styleExtractor.d.ts.map +1 -1
  106. package/dist/styles/styleExtractor.js +69 -16
  107. package/dist/styles/styleExtractor.js.map +1 -1
  108. package/dist/styles/styleIndexer.d.ts.map +1 -1
  109. package/dist/styles/styleIndexer.js +4 -0
  110. package/dist/styles/styleIndexer.js.map +1 -1
  111. package/dist/styles/tailwindProbe.d.ts +2 -0
  112. package/dist/styles/tailwindProbe.d.ts.map +1 -1
  113. package/dist/styles/tailwindProbe.js +5 -5
  114. package/dist/styles/tailwindProbe.js.map +1 -1
  115. package/dist/styles/tailwindUtilityExpander.d.ts +3 -0
  116. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  117. package/dist/styles/tailwindUtilityExpander.js +7 -0
  118. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  119. package/dist/types.d.ts +216 -11
  120. package/dist/types.d.ts.map +1 -1
  121. package/dist/types.js.map +1 -1
  122. package/dist/utils/fileDiscovery.d.ts +4 -0
  123. package/dist/utils/fileDiscovery.d.ts.map +1 -1
  124. package/dist/utils/fileDiscovery.js +7 -1
  125. package/dist/utils/fileDiscovery.js.map +1 -1
  126. package/dist/utils/reactDetection.d.ts.map +1 -1
  127. package/dist/utils/reactDetection.js +32 -0
  128. package/dist/utils/reactDetection.js.map +1 -1
  129. package/package.json +2 -4
  130. 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