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
@@ -18,22 +18,14 @@ import { loadBaseline, matchFindings, hashBaseline } from './baseline.js';
18
18
  import { computeImpact, LATENCY_BUDGET_MS } from './graph/blastRadius.js';
19
19
  // Import universal analyzers
20
20
  import { initializeLanguages } from './languages/index.js';
21
- import { UniversalSOLIDAnalyzer } from './analyzers/universal/UniversalSOLIDAnalyzer.js';
22
- import { UniversalDRYAnalyzer } from './analyzers/universal/UniversalDRYAnalyzer.js';
23
- import { UniversalDataAccessAnalyzer } from './analyzers/universal/UniversalDataAccessAnalyzer.js';
24
- import { UniversalDocumentationAnalyzer } from './analyzers/universal/UniversalDocumentationAnalyzer.js';
25
- import { UniversalSchemaAnalyzer } from './analyzers/universal/UniversalSchemaAnalyzer.js';
26
- import { UniversalStylesAnalyzer } from './analyzers/universal/UniversalStylesAnalyzer.js';
27
- import { UniversalConventionsAnalyzer } from './analyzers/universal/UniversalConventionsAnalyzer.js';
28
- import { CrossDomainAnalyzer } from './analyzers/crossDomain/CrossDomainAnalyzer.js';
29
21
  import { initializeOrmAdapters } from './analyzers/orm/index.js';
30
- import { DEFAULT_ANALYZER_CONFIGS } from './config/defaults.js';
31
22
  import { syncStyleIndex } from './styles/styleIndexer.js';
32
- import { reactAnalyzer } from './analyzers/reactAnalyzer.js';
33
- import { invariantsAnalyzer } from './analyzers/invariantsAnalyzer.js';
34
23
  import { hasRules } from './invariants/ruleEngine.js';
35
24
  import { CodeIndexDB } from './codeIndexDB.js';
36
25
  import { writeAuditToLedger, detectRunInput } from './ledger.js';
26
+ // Pipeline imports (Spec 25 — pipeline replaces hand-rolled analyzer loop)
27
+ import { runPipeline, writeIndexFactsToDb, makeVisitorStatus, getFilesProcessed, isVisitorStatus } from './pipeline.js';
28
+ import { createSolidVisitor, createDryVisitor, createDataAccessVisitor, createDocumentationVisitor, createFunctionIndexVisitor, createFileSourcesVisitor, createStylesCssVisitor, createReactVisitor, createStylesReducer, createConventionsReducer, createCrossDomainReducer, createInvariantsReducer, createSchemaSqlVisitor, createSchemaCodeVisitor, createSchemaPrismaVisitor, createSchemaJsonVisitor, createSchemaReducer, } from './pipelineAdapters.js';
37
29
  // Package version — read once at module load
38
30
  const __auditRunnerDirname = path.dirname(fileURLToPath(import.meta.url));
39
31
  const _pkg = JSON.parse(readFileSync(path.join(__auditRunnerDirname, '..', 'package.json'), 'utf-8'));
@@ -42,338 +34,24 @@ const TOOL_VERSION = String(_pkg.version || '0.0.0');
42
34
  initializeLanguages();
43
35
  // Initialize ORM adapters for cross-domain schema extraction (Spec 15 R2)
44
36
  initializeOrmAdapters();
45
- /**
46
- * Shared progress callback adapter: translates universal analyzer's (number) into
47
- * the object-based callback that auditRunner passes downstream.
48
- */
49
- function createProgressAdapter(analyzerName, progressCallback) {
50
- if (!progressCallback)
51
- return undefined;
52
- return (progress) => {
53
- progressCallback({
54
- current: Math.floor(progress * 100),
55
- total: 100,
56
- analyzer: analyzerName,
57
- phase: 'analyzing',
58
- });
59
- };
60
- }
61
- /**
62
- * Default analyzer registry
63
- */
64
- export const DEFAULT_ANALYZERS = {
65
- 'solid': {
66
- name: 'solid',
67
- description: 'Detects violations of SOLID principles',
68
- category: 'architecture',
69
- analyze: async (files, config, options, progressCallback) => {
70
- const analyzer = new UniversalSOLIDAnalyzer();
71
- const universalConfig = { skipTestFiles: true };
72
- if (config.maxMethodsPerClass !== undefined)
73
- universalConfig.maxMethodsPerClass = config.maxMethodsPerClass;
74
- if (config.maxLinesPerMethod !== undefined)
75
- universalConfig.maxLinesPerMethod = config.maxLinesPerMethod;
76
- if (config.maxParametersPerMethod !== undefined)
77
- universalConfig.maxParametersPerMethod = config.maxParametersPerMethod;
78
- if (config.maxClassComplexity !== undefined)
79
- universalConfig.maxClassComplexity = config.maxClassComplexity; // deprecated — use maxMethodComplexity
80
- // R5.1: Per-method cyclomatic complexity (true McCC)
81
- if (config.maxMethodComplexity !== undefined)
82
- universalConfig.maxMethodComplexity = config.maxMethodComplexity;
83
- // R5.2: Class-level aggregation thresholds
84
- if (config.classMethodsThreshold !== undefined)
85
- universalConfig.classMethodsThreshold = config.classMethodsThreshold;
86
- if (config.classAggregateComplexity !== undefined)
87
- universalConfig.classAggregateComplexity = config.classAggregateComplexity;
88
- if (config.maxInterfaceMembers !== undefined)
89
- universalConfig.maxInterfaceMembers = config.maxInterfaceMembers;
90
- if (config.checkDependencyInversion !== undefined)
91
- universalConfig.checkDependencyInversion = config.checkDependencyInversion;
92
- if (config.checkInterfaceSegregation !== undefined)
93
- universalConfig.checkInterfaceSegregation = config.checkInterfaceSegregation;
94
- if (config.checkLiskovSubstitution !== undefined)
95
- universalConfig.checkLiskovSubstitution = config.checkLiskovSubstitution;
96
- // Forward base-class properties (Spec-20 path profiles + severity overrides)
97
- if (config.severityOverrides !== undefined)
98
- universalConfig.severityOverrides = config.severityOverrides;
99
- if (config.pathProfiles !== undefined)
100
- universalConfig.pathProfiles = config.pathProfiles;
101
- if (config.projectRoot !== undefined)
102
- universalConfig.projectRoot = config.projectRoot;
103
- const result = await analyzer.analyze(files, universalConfig, {
104
- progressCallback: createProgressAdapter('solid', progressCallback),
105
- ...options,
106
- });
107
- return {
108
- ...result,
109
- violations: result.violations.map(v => ({
110
- ...v,
111
- principle: v.rule,
112
- analyzer: 'solid',
113
- })),
114
- };
115
- },
116
- },
117
- 'dry': {
118
- name: 'dry',
119
- description: 'Detects code duplication across the codebase',
120
- category: 'maintainability',
121
- analyze: async (files, config, options, progressCallback) => {
122
- const analyzer = new UniversalDRYAnalyzer();
123
- return analyzer.analyze(files, config, {
124
- progressCallback: createProgressAdapter('dry', progressCallback),
125
- ...options,
126
- });
127
- },
128
- },
129
- 'data-access': {
130
- name: 'data-access',
131
- description: 'Analyzes database access patterns and data layer interactions',
132
- category: 'security',
133
- analyze: async (files, config, options, progressCallback) => {
134
- const analyzer = new UniversalDataAccessAnalyzer();
135
- const universalConfig = {};
136
- if (config.databases !== undefined)
137
- universalConfig.databases = config.databases;
138
- if (config.organizationPatterns !== undefined)
139
- universalConfig.organizationPatterns = config.organizationPatterns;
140
- if (config.tablePatterns !== undefined)
141
- universalConfig.tablePatterns = config.tablePatterns;
142
- if (config.performanceThresholds !== undefined)
143
- universalConfig.performanceThresholds = config.performanceThresholds;
144
- if (config.securityPatterns !== undefined)
145
- universalConfig.securityPatterns = config.securityPatterns;
146
- if (config.checkOrgFilters !== undefined)
147
- universalConfig.checkOrgFilters = config.checkOrgFilters;
148
- if (config.checkSQLInjection !== undefined)
149
- universalConfig.checkSQLInjection = config.checkSQLInjection;
150
- if (config.checkPerformance !== undefined)
151
- universalConfig.checkPerformance = config.checkPerformance;
152
- // Forward base-class properties (Spec-20 path profiles + severity overrides)
153
- if (config.severityOverrides !== undefined)
154
- universalConfig.severityOverrides = config.severityOverrides;
155
- if (config.pathProfiles !== undefined)
156
- universalConfig.pathProfiles = config.pathProfiles;
157
- if (config.projectRoot !== undefined)
158
- universalConfig.projectRoot = config.projectRoot;
159
- // Spec 21: Forward shared provenance timing accumulator
160
- if (config._provenanceTiming !== undefined)
161
- universalConfig._provenanceTiming = config._provenanceTiming;
162
- return analyzer.analyze(files, universalConfig, {
163
- progressCallback: createProgressAdapter('data-access', progressCallback),
164
- ...options,
165
- });
166
- },
167
- },
168
- 'react': reactAnalyzer,
169
- 'documentation': {
170
- name: 'documentation',
171
- description: 'Analyzes documentation quality across the codebase',
172
- category: 'documentation',
173
- analyze: async (files, config, options, progressCallback) => {
174
- const analyzer = new UniversalDocumentationAnalyzer();
175
- const universalConfig = {
176
- requireFunctionDocs: config.requireFunctionDocs ?? true,
177
- requireClassDocs: config.requireComponentDocs ?? true,
178
- // requireFileDocs is DEPRECATED — use fileHeaders instead (default false per R1.5)
179
- requireFileDocs: config.requireFileDocs ?? true, // kept for back-compat
180
- requireParamDocs: config.requireParamDocs ?? true,
181
- requireReturnDocs: config.requireReturnDocs ?? true,
182
- minDescriptionLength: config.minDescriptionLength ?? 10,
183
- // checkExportedOnly is DEPRECATED — use scope instead
184
- checkExportedOnly: config.checkExportedOnly ?? false,
185
- exemptPatterns: config.exemptPatterns ?? ['test', 'spec', '\\.d\\.ts$', 'mock', 'fixture'],
186
- // Spec-17 additions
187
- scope: config.scope ?? 'public', // R1.2, R1.4
188
- docsMinLines: config.docsMinLines ?? 5, // R1.3
189
- fileHeaders: config.fileHeaders ?? config.requireFileDocs ?? false, // R1.5
190
- headerSkipGlobs: config.headerSkipGlobs, // R1.5 (undefined → analyzer default)
191
- };
192
- // Forward base-class properties (Spec-20 path profiles + severity overrides)
193
- if (config.severityOverrides !== undefined)
194
- universalConfig.severityOverrides = config.severityOverrides;
195
- if (config.pathProfiles !== undefined)
196
- universalConfig.pathProfiles = config.pathProfiles;
197
- if (config.projectRoot !== undefined)
198
- universalConfig.projectRoot = config.projectRoot;
199
- return analyzer.analyze(files, universalConfig, {
200
- progressCallback: createProgressAdapter('documentation', progressCallback),
201
- ...options,
202
- });
203
- },
204
- },
205
- 'invariants': invariantsAnalyzer,
206
- 'schema': {
207
- name: 'schema',
208
- description: 'Analyzes code against database schemas',
209
- category: 'database',
210
- analyze: async (files, config, options, progressCallback) => {
211
- const analyzer = new UniversalSchemaAnalyzer();
212
- let schemas = [];
213
- try {
214
- const db = CodeIndexDB.getInstance(undefined, config.projectRoot);
215
- await db.initialize();
216
- const loadedSchemas = await db.getAllSchemas();
217
- schemas = loadedSchemas.map((loaded) => {
218
- const schema = loaded.schema;
219
- return {
220
- name: schema.name,
221
- tables: schema.databases.flatMap((database) => database.tables.map((table) => ({
222
- name: table.name,
223
- columns: table.columns || [],
224
- }))),
225
- };
226
- });
227
- }
228
- catch {
229
- // If database isn't available, continue without schemas
230
- }
231
- const universalConfig = {
232
- checkMissingReferences: config.checkMissingReferences ?? true,
233
- checkNamingConventions: config.checkNamingConventions ?? true,
234
- detectUnusedTables: config.detectUnusedTables ?? false,
235
- validateQueryPatterns: config.validateQueryPatterns ?? true,
236
- maxQueriesPerFunction: config.maxQueriesPerFunction ?? 5,
237
- requiredSchemas: config.requiredSchemas ?? [],
238
- schemas,
239
- // Spec-17 R2 additions — AST-based SQL context detection
240
- sqlTagNames: config.sqlTagNames ?? ['sql', 'db'], // R2.1
241
- dbReceiverNames: config.dbReceiverNames, // R2.2 (let analyzer use defaults)
242
- dbCallMethods: config.dbCallMethods, // R2.2
243
- dbBindingNames: config.dbBindingNames ?? ['env.DB'], // R2.2
244
- fileGateGlobs: config.fileGateGlobs, // R2.2 (let analyzer use defaults)
245
- };
246
- // Forward base-class properties (Spec-20 path profiles + severity overrides)
247
- if (config.severityOverrides !== undefined)
248
- universalConfig.severityOverrides = config.severityOverrides;
249
- if (config.pathProfiles !== undefined)
250
- universalConfig.pathProfiles = config.pathProfiles;
251
- if (config.projectRoot !== undefined)
252
- universalConfig.projectRoot = config.projectRoot;
253
- // Spec 21: Forward shared provenance timing accumulator
254
- if (config._provenanceTiming !== undefined)
255
- universalConfig._provenanceTiming = config._provenanceTiming;
256
- const result = await analyzer.analyze(files, universalConfig);
257
- return {
258
- ...result,
259
- violations: result.violations.map(v => ({
260
- ...v,
261
- schemaType: v.rule,
262
- details: v.message,
263
- analyzer: 'schema',
264
- })),
265
- };
266
- },
267
- },
268
- 'styles': {
269
- name: 'styles',
270
- description: 'Detects style fragmentation, value drift, token bypass, dead classes, off-scale values, mechanism fragmentation, and z-index sprawl',
271
- category: 'style',
272
- analyze: async (files, config, options, progressCallback) => {
273
- const analyzer = new UniversalStylesAnalyzer();
274
- const universalConfig = {};
275
- if (config.minCorpus !== undefined)
276
- universalConfig.minCorpus = config.minCorpus;
277
- if (config.colorDeltaE !== undefined)
278
- universalConfig.colorDeltaE = config.colorDeltaE;
279
- if (config.outlierMaxShare !== undefined)
280
- universalConfig.outlierMaxShare = config.outlierMaxShare;
281
- if (config.modeMinCount !== undefined)
282
- universalConfig.modeMinCount = config.modeMinCount;
283
- if (config.scaleProperties !== undefined)
284
- universalConfig.scaleProperties = config.scaleProperties;
285
- if (config.zIndexMaxDistinct !== undefined)
286
- universalConfig.zIndexMaxDistinct = config.zIndexMaxDistinct;
287
- if (config.mechanismFragmentationMinMechanisms !== undefined)
288
- universalConfig.mechanismFragmentationMinMechanisms = config.mechanismFragmentationMinMechanisms;
289
- if (config.declarationSetMinDeclarations !== undefined)
290
- universalConfig.declarationSetMinDeclarations = config.declarationSetMinDeclarations;
291
- if (config.declarationSetSimilarityThreshold !== undefined)
292
- universalConfig.declarationSetSimilarityThreshold = config.declarationSetSimilarityThreshold;
293
- // Forward base-class properties (Spec-20 path profiles + severity overrides)
294
- if (config.severityOverrides !== undefined)
295
- universalConfig.severityOverrides = config.severityOverrides;
296
- if (config.pathProfiles !== undefined)
297
- universalConfig.pathProfiles = config.pathProfiles;
298
- if (config.projectRoot !== undefined)
299
- universalConfig.projectRoot = config.projectRoot;
300
- return analyzer.analyze(files, universalConfig, {
301
- progressCallback: createProgressAdapter('styles', progressCallback),
302
- ...options,
303
- });
304
- },
305
- },
306
- 'conventions': {
307
- name: 'conventions',
308
- description: 'Mines codebase conventions and flags deviations at suggestion severity',
309
- category: 'style',
310
- analyze: async (files, config, options, progressCallback) => {
311
- const analyzer = new UniversalConventionsAnalyzer();
312
- const universalConfig = {};
313
- if (config.minCorpus !== undefined)
314
- universalConfig.minCorpus = config.minCorpus;
315
- if (config.pairConfidence !== undefined)
316
- universalConfig.pairConfidence = config.pairConfidence;
317
- if (config.modeShare !== undefined)
318
- universalConfig.modeShare = config.modeShare;
319
- if (config.maxConventionsPerDomain !== undefined)
320
- universalConfig.maxConventionsPerDomain = config.maxConventionsPerDomain;
321
- // Forward base-class properties (Spec-20 path profiles + severity overrides)
322
- if (config.severityOverrides !== undefined)
323
- universalConfig.severityOverrides = config.severityOverrides;
324
- if (config.pathProfiles !== undefined)
325
- universalConfig.pathProfiles = config.pathProfiles;
326
- if (config.projectRoot !== undefined)
327
- universalConfig.projectRoot = config.projectRoot;
328
- return analyzer.analyze(files, universalConfig, {
329
- progressCallback: createProgressAdapter('conventions', progressCallback),
330
- ...options,
331
- });
332
- },
333
- },
334
- 'cross-domain': {
335
- name: 'cross-domain',
336
- description: 'Detects cross-domain issues (schema lifecycle, validation bypass, coverage gaps)',
337
- category: 'architecture',
338
- analyze: async (files, config, options, progressCallback) => {
339
- const analyzer = new CrossDomainAnalyzer();
340
- const cxDefaults = DEFAULT_ANALYZER_CONFIGS.crossDomain;
341
- const universalConfig = {
342
- // R1 — Schema lifecycle: enabled by default, overridable.
343
- // Use defaults when the user doesn't configure these explicitly.
344
- schemaLifecycle: config.schemaLifecycle ?? cxDefaults.schemaLifecycle,
345
- // R3 — Validation-bypass: enabled by default with Spec 21 provenance-based
346
- // detection. Detector gates on truthiness so we must supply a default.
347
- validatorBypass: config.validatorBypass ?? cxDefaults.validatorBypass,
348
- // R4 — Coverage: enabled by default. Reads coverage_data when measured
349
- // coverage has been imported, falls back to static-reach otherwise.
350
- coverage: config.coverage ?? cxDefaults.coverage,
351
- };
352
- // Forward base-class properties (Spec-20 path profiles + severity overrides)
353
- if (config.severityOverrides !== undefined)
354
- universalConfig.severityOverrides = config.severityOverrides;
355
- if (config.pathProfiles !== undefined)
356
- universalConfig.pathProfiles = config.pathProfiles;
357
- if (config.projectRoot !== undefined)
358
- universalConfig.projectRoot = config.projectRoot;
359
- return analyzer.analyze(files, universalConfig, {
360
- progressCallback: createProgressAdapter('cross-domain', progressCallback),
361
- ...options,
362
- });
363
- },
364
- },
365
- };
37
+ /** Schema sub-visitor names — shown as separate rows in the CLI table. */
38
+ const SCHEMA_SUB_VISITORS = ['schema-sql', 'schema-code', 'schema-prisma', 'schema-json'];
366
39
  /**
367
40
  * Create an audit runner with the given options
368
41
  */
369
42
  export function createAuditRunner(options = {}) {
370
- const analyzerRegistry = { ...DEFAULT_ANALYZERS };
371
- /**
372
- * Register a custom analyzer
373
- */
374
- function registerAnalyzer(analyzer) {
375
- analyzerRegistry[analyzer.name] = analyzer;
376
- }
43
+ const analyzerRegistry = {
44
+ solid: { name: 'solid' },
45
+ dry: { name: 'dry' },
46
+ 'data-access': { name: 'data-access' },
47
+ react: { name: 'react' },
48
+ documentation: { name: 'documentation' },
49
+ invariants: { name: 'invariants' },
50
+ schema: { name: 'schema' },
51
+ styles: { name: 'styles' },
52
+ conventions: { name: 'conventions' },
53
+ 'cross-domain': { name: 'cross-domain' },
54
+ };
377
55
  /**
378
56
  * Load configuration from file
379
57
  */
@@ -424,7 +102,7 @@ export function createAuditRunner(options = {}) {
424
102
  // Changed scope: detect modified files
425
103
  const db = CodeIndexDB.getInstance(undefined, mergedOptions.projectRoot || process.cwd());
426
104
  await db.initialize();
427
- const modifiedFiles = mergedOptions.explicitFiles?.length
105
+ const modifiedFiles = mergedOptions.explicitFiles !== undefined
428
106
  ? mergedOptions.explicitFiles
429
107
  : await db.detectModifiedFiles(path.resolve(mergedOptions.projectRoot || process.cwd()));
430
108
  files = [...new Set(modifiedFiles.map((f) => path.resolve(f)))].sort();
@@ -507,7 +185,7 @@ export function createAuditRunner(options = {}) {
507
185
  handoffRemaining.push(...files.slice(maxPerRun));
508
186
  files = files.slice(0, maxPerRun);
509
187
  }
510
- const root = mergedOptions.projectRoot || process.cwd();
188
+ const root = path.resolve(mergedOptions.projectRoot || process.cwd());
511
189
  logMcpInfo('discovery', 'file discovery finished', {
512
190
  projectRoot: path.resolve(root),
513
191
  totalFiles: files.length,
@@ -624,99 +302,267 @@ export function createAuditRunner(options = {}) {
624
302
  ? Math.min(Math.floor(requestedConcurrency), enabledAnalyzers.length || 1)
625
303
  : 1;
626
304
  logMcpInfo('analysis', 'analyzer concurrency', { analyzerConcurrency });
627
- let cursor = 0;
628
- const runAnalyzer = async () => {
629
- while (true) {
630
- throwIfAborted(mergedOptions.abortSignal);
631
- const index = cursor++;
632
- if (index >= enabledAnalyzers.length)
633
- return;
634
- const analyzerName = enabledAnalyzers[index];
635
- const analyzer = analyzerRegistry[analyzerName];
636
- logMcpDebug('analysis', `starting analyzer ${analyzerName}`, { found: !!analyzer });
637
- if (!analyzer) {
638
- logMcpInfo('analysis', `unknown analyzer skipped: ${analyzerName}`, {});
639
- continue;
305
+ // ── Initialize CodeIndexDB for analyzer DB access ────────────────────
306
+ // Styles, conventions, and cross-domain analyzers read config.indexHandle
307
+ // for DB access. The singleton is shared — initialize once before the
308
+ // analyzer run loop so ensureInitialized() is a no-op for each call.
309
+ let auditIndex;
310
+ try {
311
+ auditIndex = CodeIndexDB.getInstance(undefined, root);
312
+ await auditIndex.initialize();
313
+ logMcpInfo('analysis', 'code index initialized for analyzers', { isInitialized: auditIndex.isInitialized, dbPath: auditIndex.dbPath });
314
+ }
315
+ catch (err) {
316
+ // Non-fatal: analyzers that need indexHandle will skip DB-dependent
317
+ // checks and report "No index handle available" in their errors.
318
+ logMcpInfo('analysis', 'failed to initialize code index for analyzers (continuing)', {
319
+ error: err instanceof Error ? err.message : String(err)
320
+ });
321
+ }
322
+ // ── Analyzer execution ──────────────────────────────────────────────
323
+ // Spec 25 R3: The pipeline replaces the hand-rolled concurrent
324
+ // analyzer loop. Visitors run per-file in stage 2, corpus reducers
325
+ // run in stage 3, and derived reducers (cross-domain) run in stage 4.
326
+ // Schema runs outside the pipeline because it needs ALL file types (SQL, JSON, TS).
327
+ // Invariants runs inside the pipeline as a Stage 3 reducer (Spec 41 Part C).
328
+ // Build IndexHandle for pipeline reducers
329
+ let pipelineIndexHandle;
330
+ if (auditIndex) {
331
+ pipelineIndexHandle = {
332
+ query: (sql, params) => auditIndex.query(sql, params),
333
+ count: (table, where, params) => auditIndex.count(table, where, params),
334
+ tableHasRows: (table) => auditIndex.tableHasRows(table),
335
+ run: (sql, params) => auditIndex.rawDb.prepare(sql).run(...(params ?? [])),
336
+ exec: (sql) => auditIndex.rawDb.exec(sql),
337
+ getMeta: (key) => auditIndex.getMeta(key),
338
+ getUntestedTopDecile: (td) => auditIndex.getUntestedTopDecile(td),
339
+ };
340
+ }
341
+ // ── 1. Build pipeline adapters ───────────────────────────────────────
342
+ const pipelineVisitors = [];
343
+ const pipelineReducers = [];
344
+ const pipelineDerivedReducers = [];
345
+ let dryBundle;
346
+ let reactBundle;
347
+ // Always-on infrastructure: function-index visitor populates the
348
+ // `functions` table so conventions + cross-domain reducers have data
349
+ // even on a cold run with no prior index sync.
350
+ pipelineVisitors.push(createFunctionIndexVisitor());
351
+ // file-sources visitor records per-file source code so Stage 3 reducers
352
+ // can access it without calling readFileSync().
353
+ pipelineVisitors.push(createFileSourcesVisitor());
354
+ // styles-css visitor — AST-extracts .css files into style_* tables (Spec 26 Phase 2)
355
+ if (enabledAnalyzers.includes('styles'))
356
+ pipelineVisitors.push(createStylesCssVisitor());
357
+ if (enabledAnalyzers.includes('solid'))
358
+ pipelineVisitors.push(createSolidVisitor());
359
+ if (enabledAnalyzers.includes('dry')) {
360
+ dryBundle = createDryVisitor(fullFunctionIndex);
361
+ pipelineVisitors.push(dryBundle.visitor);
362
+ }
363
+ if (enabledAnalyzers.includes('data-access'))
364
+ pipelineVisitors.push(createDataAccessVisitor());
365
+ if (enabledAnalyzers.includes('react')) {
366
+ reactBundle = createReactVisitor();
367
+ pipelineVisitors.push(reactBundle.visitor);
368
+ }
369
+ if (enabledAnalyzers.includes('documentation'))
370
+ pipelineVisitors.push(createDocumentationVisitor());
371
+ if (enabledAnalyzers.includes('styles'))
372
+ pipelineReducers.push(createStylesReducer());
373
+ if (enabledAnalyzers.includes('conventions'))
374
+ pipelineReducers.push(createConventionsReducer());
375
+ if (enabledAnalyzers.includes('invariants'))
376
+ pipelineReducers.push(createInvariantsReducer());
377
+ if (enabledAnalyzers.includes('cross-domain'))
378
+ pipelineDerivedReducers.push(createCrossDomainReducer());
379
+ if (enabledAnalyzers.includes('schema')) {
380
+ pipelineVisitors.push(createSchemaSqlVisitor());
381
+ pipelineVisitors.push(createSchemaCodeVisitor());
382
+ pipelineVisitors.push(createSchemaPrismaVisitor());
383
+ pipelineVisitors.push(createSchemaJsonVisitor());
384
+ pipelineReducers.push(createSchemaReducer());
385
+ }
386
+ // ── 2. Safeguard warnings ────────────────────────────────────────────
387
+ if (auditIndex) {
388
+ if (enabledAnalyzers.includes('cross-domain')) {
389
+ const suCount = auditIndex.count('schema_usage');
390
+ if (suCount === 0) {
391
+ console.warn('[code-audit] ⚠ cross-domain analyzer requires schema_usage data. '
392
+ + 'This is populated during a full audit run by the schema analyzer. '
393
+ + 'If this warning persists, run a full "code-audit audit --path ." first.');
640
394
  }
641
- reportProgress(mergedOptions, {
642
- phase: 'analysis',
643
- analyzer: analyzerName,
644
- message: `Running ${analyzerName} analyzer...`
645
- });
646
- // Build analyzer config; inject full index for DRY on scoped runs.
647
- // Skip pathProfiles for invariants (Spec-20 R3): no backdoor
648
- // around declared lawsinvariants severity is absolute.
649
- const isInvariants = analyzerName === 'invariants';
650
- const analyzerConfig = {
651
- ...(mergedOptions.analyzerConfigs?.[analyzerName] || {}),
395
+ }
396
+ }
397
+ // ── 3. Run pipeline ──────────────────────────────────────────────────
398
+ const hasPipelineAnalyzers = pipelineVisitors.length > 0
399
+ || pipelineReducers.length > 0
400
+ || pipelineDerivedReducers.length > 0;
401
+ if (hasPipelineAnalyzers) {
402
+ // Build per-analyzer namespaced config no flat Object.assign merge.
403
+ // Each analyzer gets its own namespace; _infra holds shared infrastructure keys
404
+ // that every visitor/reducer receives alongside its own config.
405
+ const pipelineAnalyzerConfig = {};
406
+ for (const name of enabledAnalyzers) {
407
+ pipelineAnalyzerConfig[name] = { ...(mergedOptions.analyzerConfigs?.[name] ?? {}) };
408
+ }
409
+ // Pass invariant rules from .codeauditor.json into the invariants pipeline config.
410
+ // The rules field lives at the top level of the loaded config (not under analyzerConfigs).
411
+ if (enabledAnalyzers.includes('invariants') && mergedOptions.rules) {
412
+ pipelineAnalyzerConfig['invariants'] = {
413
+ ...(pipelineAnalyzerConfig['invariants'] ?? {}),
414
+ rules: mergedOptions.rules,
652
415
  };
653
- if (!isInvariants) {
654
- analyzerConfig.pathProfiles = mergedOptions.pathProfiles;
655
- analyzerConfig.projectRoot = root;
656
- analyzerConfig.severityOverrides = mergedOptions.severityOverrides;
657
- }
658
- if (analyzerName === 'dry' && isScoped && fullFunctionIndex) {
659
- analyzerConfig.fullFunctionIndex = fullFunctionIndex;
416
+ }
417
+ // Cross-domain config lives at the top level (not analyzerConfigs)
418
+ if (enabledAnalyzers.includes('cross-domain') && mergedOptions.crossDomain) {
419
+ pipelineAnalyzerConfig['cross-domain'] = {
420
+ ...(pipelineAnalyzerConfig['cross-domain'] ?? {}),
421
+ ...mergedOptions.crossDomain,
422
+ };
423
+ }
424
+ // Schema config: all table discovery flows through visitors and facts;
425
+ // the reducer builds the known-tables catalog solely from fact data.
426
+ if (enabledAnalyzers.includes('schema')) {
427
+ const scConfig = mergedOptions.analyzerConfigs?.schema ?? {};
428
+ const schemaConfig = {
429
+ ...(pipelineAnalyzerConfig['schema'] ?? {}),
430
+ sqlTagNames: scConfig.sqlTagNames ?? ['sql', 'db'],
431
+ dbReceiverNames: scConfig.dbReceiverNames,
432
+ dbCallMethods: scConfig.dbCallMethods,
433
+ dbBindingNames: scConfig.dbBindingNames ?? ['env.DB'],
434
+ fileGateGlobs: scConfig.fileGateGlobs,
435
+ // External schema references — the pipeline reducer gates unknown-table
436
+ // detection on these being non-empty (fail-open law: no external
437
+ // authority means the rule cannot accuse).
438
+ schemas: scConfig.schemas,
439
+ knownTables: scConfig.knownTables,
440
+ };
441
+ pipelineAnalyzerConfig['schema'] = schemaConfig;
442
+ // Pipeline resolves config by visitor name (rawConfig[visitor.name]).
443
+ // The schema-* sub-visitors need the same namespace as the schema reducer.
444
+ for (const sub of ['schema-code', 'schema-sql', 'schema-prisma', 'schema-json']) {
445
+ pipelineAnalyzerConfig[sub] = schemaConfig;
660
446
  }
661
- // Spec 21: Pass shared provenance timing accumulator to analyzers
662
- // that call buildProvenanceContext() (data-access and schema).
663
- // The analyzers accumulate per-file timing into this object;
664
- // we read it back after all analyzers complete.
665
- // Spec 15: cross-domain config lives at top-level crossDomain,
666
- // not under analyzerConfigs — merge it in so the analyzer sees it.
667
- if (analyzerName === 'cross-domain' && mergedOptions.crossDomain) {
668
- Object.assign(analyzerConfig, mergedOptions.crossDomain);
447
+ }
448
+ // Global/infrastructure settings passed to all pipeline stages
449
+ pipelineAnalyzerConfig['_infra'] = {
450
+ pathProfiles: mergedOptions.pathProfiles,
451
+ severityOverrides: mergedOptions.severityOverrides,
452
+ projectRoot: root,
453
+ _provenanceTiming: provenanceTiming,
454
+ files,
455
+ };
456
+ const pipelineConfig = {
457
+ projectRoot: root,
458
+ explicitFiles: files,
459
+ visitors: pipelineVisitors,
460
+ reducers: pipelineReducers,
461
+ derivedReducers: pipelineDerivedReducers,
462
+ config: pipelineAnalyzerConfig,
463
+ abortSignal: mergedOptions.abortSignal,
464
+ progressCallback: (progress) => {
465
+ throwIfAborted(mergedOptions.abortSignal);
466
+ reportProgress(mergedOptions, progress);
467
+ },
468
+ isScoped,
469
+ onStage2Complete: async (ctx) => {
470
+ // Post-stage-2 setup: rebuild function_calls from the functions table
471
+ // (populated by the function-index visitor), then mine conventions.
472
+ if (auditIndex && enabledAnalyzers.includes('conventions')) {
473
+ try {
474
+ await auditIndex.updateDependencyGraph();
475
+ }
476
+ catch (err) {
477
+ logMcpInfo('analysis', 'updateDependencyGraph failed (non-fatal)', {
478
+ error: err instanceof Error ? err.message : String(err)
479
+ });
480
+ }
481
+ try {
482
+ // Build a source provider from the file-sources infrastructure
483
+ // visitor fact — avoids readFileSync inside the pipeline.
484
+ const fileSources = ctx?.allFacts.get('file-sources');
485
+ const getSource = fileSources
486
+ ? (filePath) => fileSources[filePath]
487
+ : undefined;
488
+ auditIndex.mineAllConventions(root, getSource);
489
+ }
490
+ catch (err) {
491
+ logMcpInfo('analysis', 'convention mining failed (non-fatal)', {
492
+ error: err instanceof Error ? err.message : String(err)
493
+ });
494
+ }
495
+ }
496
+ },
497
+ };
498
+ try {
499
+ logMcpInfo('analysis', 'running pipeline', { visitorCount: pipelineVisitors.length, reducerCount: pipelineReducers.length, derivedReducerCount: pipelineDerivedReducers.length, fileCount: files.length });
500
+ const pipelineResult = await runPipeline(pipelineConfig, pipelineIndexHandle);
501
+ // Write index facts (schema_usage from schema visitor, etc.)
502
+ if (pipelineIndexHandle && pipelineResult.indexFacts && pipelineResult.indexFacts.length > 0) {
503
+ writeIndexFactsToDb(pipelineIndexHandle, pipelineResult.indexFacts);
669
504
  }
670
- if (analyzerName === 'data-access' || analyzerName === 'schema') {
671
- analyzerConfig._provenanceTiming = provenanceTiming;
505
+ // Pull in pipeline results
506
+ for (const [name, ar] of Object.entries(pipelineResult.analyzerResults)) {
507
+ analyzerResults[name] = ar;
508
+ logMcpDebug('analysis', `pipeline: ${name} completed`, {
509
+ violations: ar.violations?.length ?? 0,
510
+ status: ar.status?.status,
511
+ });
672
512
  }
673
- logMcpInfo('analysis', `running ${analyzerName}`, { fileCount: files.length });
674
- try {
675
- const result = await analyzer.analyze(files, analyzerConfig, mergedOptions, (progress) => {
676
- throwIfAborted(mergedOptions.abortSignal);
677
- reportProgress(mergedOptions, {
678
- phase: 'analysis',
679
- analyzer: analyzerName,
680
- current: progress.current,
681
- total: progress.total,
682
- message: `Analyzing ${progress.file}...`
683
- });
684
- if (isMcpDebugEnabled() && progress.current && progress.total) {
685
- if (progress.current % 100 === 0 || progress.current === progress.total) {
686
- logMcpDebug('analysis', `${analyzerName} file progress`, {
687
- current: progress.current,
688
- total: progress.total,
689
- file: progress.file
690
- });
691
- }
513
+ // ── Schema sub-visitors: keep separate rows for visibility ────
514
+ // Schema runs as 4 Stage 2 visitors + 1 Stage 3 reducer. Each visitor
515
+ // keeps its own violations and status row so the CLI displays per-visitor
516
+ // file/fact counts. Violations are NOT merged — the reducer only carries
517
+ // cross-file violations (unknown-table, JSON schema validation).
518
+ // Sub-visitors are added to orderedAnalyzerResults below (line ~650).
519
+ // ── React finalization (cross-component checks) ──────────────────
520
+ if (reactBundle) {
521
+ try {
522
+ const reactResult = analyzerResults['react'];
523
+ if (reactResult) {
524
+ const extraViolations = await reactBundle.finalizeCrossComponent(pipelineAnalyzerConfig);
525
+ reactResult.violations.push(...extraViolations);
692
526
  }
693
- });
694
- logMcpDebug('analysis', `${analyzerName} completed`, {
695
- violations: result.violations?.length ?? 0
696
- });
697
- throwIfAborted(mergedOptions.abortSignal);
698
- analyzerResults[analyzerName] = result;
527
+ }
528
+ catch (err) {
529
+ logMcpInfo('analysis', 'react finalization failed (non-fatal)', {
530
+ error: err instanceof Error ? err.message : String(err)
531
+ });
532
+ }
699
533
  }
700
- catch (error) {
701
- if (error instanceof AuditAbortedError || error instanceof AuditHandoffError) {
702
- throw error;
534
+ }
535
+ catch (error) {
536
+ if (error instanceof AuditAbortedError || error instanceof AuditHandoffError) {
537
+ throw error;
538
+ }
539
+ // Pipeline failure — populate error results for all pipeline analyzers.
540
+ // Exclude infrastructure visitors (function-index) that don't produce violations.
541
+ const pipelineAnalyzerNames = new Set([
542
+ ...pipelineVisitors.map(v => v.name),
543
+ ...pipelineReducers.map(r => r.name),
544
+ ...pipelineDerivedReducers.map(r => r.name),
545
+ ]);
546
+ pipelineAnalyzerNames.delete('function-index');
547
+ for (const name of pipelineAnalyzerNames) {
548
+ if (!analyzerResults[name]) {
549
+ analyzerResults[name] = {
550
+ violations: [],
551
+ status: makeVisitorStatus(0),
552
+ executionTime: 0,
553
+ analyzerName: name,
554
+ errors: [{ file: 'pipeline', error: error.message }],
555
+ };
703
556
  }
704
- reportError(mergedOptions, error, `${analyzerName} analyzer`);
705
- analyzerResults[analyzerName] = {
706
- violations: [],
707
- filesProcessed: 0,
708
- executionTime: 0,
709
- errors: [{ file: 'analyzer', error: error.message }]
710
- };
711
557
  }
558
+ reportError(mergedOptions, error, 'pipeline');
712
559
  }
713
- };
714
- await Promise.all(Array.from({ length: analyzerConcurrency }, () => runAnalyzer()));
560
+ }
715
561
  // ── Zero-files diagnostic ─────────────────────────────────────────────
716
562
  // Extracted to runZeroFilesDiagnostics() for testability. Runs against the
717
563
  // raw analyzerResults before truthiness filtering so the "no result" pass
718
564
  // catches analyzers skipped by the registry, abort, or handoff exceptions.
719
- const zeroFilesDiagnostics = runZeroFilesDiagnostics(enabledAnalyzers, analyzerResults);
565
+ const zeroFilesDiagnostics = runZeroFilesDiagnostics(enabledAnalyzers, analyzerResults, files.length);
720
566
  for (const w of zeroFilesDiagnostics) {
721
567
  console.warn(w.message);
722
568
  }
@@ -727,6 +573,15 @@ export function createAuditRunner(options = {}) {
727
573
  if (analyzerResults[analyzerName]) {
728
574
  orderedAnalyzerResults[analyzerName] = analyzerResults[analyzerName];
729
575
  }
576
+ // When schema is enabled, insert sub-visitor rows after the schema entry
577
+ // so the CLI table shows per-visitor file/fact counts.
578
+ if (analyzerName === 'schema') {
579
+ for (const subName of SCHEMA_SUB_VISITORS) {
580
+ if (analyzerResults[subName]) {
581
+ orderedAnalyzerResults[subName] = analyzerResults[subName];
582
+ }
583
+ }
584
+ }
730
585
  }
731
586
  // ── Spec 13 R5 Phase 1: Persist seeded DRY pairs ──────────────────
732
587
  // Store pairs from DRY analysis in dry_pair_history for divergence tracking.
@@ -734,8 +589,7 @@ export function createAuditRunner(options = {}) {
734
589
  // NOT content-hash-based — so a diverging clone stays the same pair.
735
590
  let dryPersistRunId = null;
736
591
  try {
737
- const dryResult = orderedAnalyzerResults['dry'];
738
- const dryPairs = dryResult?.dryPairs;
592
+ const dryPairs = dryBundle?.getDryPairs();
739
593
  if (dryPairs && dryPairs.length > 0) {
740
594
  const indexDb = CodeIndexDB.getInstance(undefined, root);
741
595
  await indexDb.initialize();
@@ -919,8 +773,9 @@ export function createAuditRunner(options = {}) {
919
773
  else {
920
774
  orderedAnalyzerResults['dry'] = {
921
775
  violations: divergingViolations,
922
- filesProcessed: 0,
776
+ status: makeVisitorStatus(0),
923
777
  executionTime: 0,
778
+ analyzerName: 'dry',
924
779
  };
925
780
  }
926
781
  }
@@ -996,7 +851,6 @@ export function createAuditRunner(options = {}) {
996
851
  return generateReport(result, format);
997
852
  }
998
853
  return {
999
- registerAnalyzer,
1000
854
  loadConfiguration,
1001
855
  run,
1002
856
  generateReport: generateReportForResult
@@ -1016,7 +870,7 @@ function throwIfAborted(signal) {
1016
870
  }
1017
871
  async function discoverProjectFiles(options) {
1018
872
  const rootDir = path.resolve(options.projectRoot || process.cwd());
1019
- if (options.explicitFiles && options.explicitFiles.length > 0) {
873
+ if (options.explicitFiles !== undefined) {
1020
874
  return [...new Set(options.explicitFiles.map((f) => path.resolve(f)))].sort();
1021
875
  }
1022
876
  return discoverFiles(rootDir, {
@@ -1096,9 +950,15 @@ async function resolveFilesScope(options, scopeFiles) {
1096
950
  result.add(m);
1097
951
  }
1098
952
  else {
1099
- // Direct file path
953
+ // Direct file path — skip files that don't exist
1100
954
  const resolved = path.isAbsolute(item) ? item : path.resolve(rootDir, item);
1101
- result.add(resolved);
955
+ try {
956
+ await fs.stat(resolved);
957
+ result.add(resolved);
958
+ }
959
+ catch {
960
+ // File doesn't exist — skip silently
961
+ }
1102
962
  }
1103
963
  }
1104
964
  return [...result].sort();
@@ -1123,13 +983,17 @@ function hasInvariantRules(projectDir) {
1123
983
  * Get list of enabled analyzers
1124
984
  */
1125
985
  function getEnabledAnalyzers(options, registry) {
986
+ const projectDir = options.projectRoot || process.cwd();
1126
987
  // Explicit array (including empty = run no analyzers, e.g. index-only harness)
1127
988
  if (options.enabledAnalyzers !== undefined) {
989
+ // Auto-disable invariants when no rules are configured (Spec 05 R3.1)
990
+ if (options.enabledAnalyzers.includes('invariants') && !hasRules(options) && !hasInvariantRules(projectDir)) {
991
+ return options.enabledAnalyzers.filter(a => a !== 'invariants');
992
+ }
1128
993
  return options.enabledAnalyzers;
1129
994
  }
1130
995
  const allAnalyzers = Object.keys(registry);
1131
996
  // Auto-disable invariants when no rules are configured (Spec 05 R3.1)
1132
- const projectDir = options.projectRoot || process.cwd();
1133
997
  if (!hasRules(options) && !hasInvariantRules(projectDir)) {
1134
998
  return allAnalyzers.filter(a => a !== 'invariants');
1135
999
  }
@@ -1166,7 +1030,7 @@ function generateSummary(analyzerResults, filesAnalyzed) {
1166
1030
  }
1167
1031
  byAnalyzer[analyzer] = {
1168
1032
  violations: analyzerViolations,
1169
- filesProcessed: result.filesProcessed,
1033
+ filesProcessed: getFilesProcessed(result.status),
1170
1034
  fatalErrors: result.errors ? result.errors.length : 0,
1171
1035
  };
1172
1036
  }
@@ -1257,7 +1121,10 @@ function reportError(options, error, context) {
1257
1121
  * Pass 2: Every analyzer with a result entry but filesProcessed === 0 and no
1258
1122
  * errors fires a warning — the analyzer ran but matched zero source files.
1259
1123
  */
1260
- export function runZeroFilesDiagnostics(enabledAnalyzers, analyzerResults) {
1124
+ export function runZeroFilesDiagnostics(enabledAnalyzers, analyzerResults, totalFiles) {
1125
+ // When there were zero files to process, zero-files is expected, not a bug.
1126
+ if (totalFiles === 0)
1127
+ return [];
1261
1128
  const warnings = [];
1262
1129
  // Pass 1: enabled but absent from results
1263
1130
  for (const analyzerName of enabledAnalyzers) {
@@ -1270,14 +1137,19 @@ export function runZeroFilesDiagnostics(enabledAnalyzers, analyzerResults) {
1270
1137
  });
1271
1138
  }
1272
1139
  }
1273
- // Pass 2: filesProcessed = 0 with no errors
1140
+ // Pass 2: filesProcessed = 0 a dark-analyzer failure regardless of errors.
1141
+ // Visitors with declared extensions that matched zero files in the corpus are
1142
+ // benign (converted to notRun by the pipeline), but a visitor that was dispatched
1143
+ // files and still shows visitor-ran + 0 files is broken — whether it errored or
1144
+ // silently returned.
1274
1145
  for (const [analyzerName, result] of Object.entries(analyzerResults)) {
1275
- if (result.filesProcessed === 0 &&
1276
- (!result.errors || result.errors.length === 0)) {
1146
+ if (isVisitorStatus(result.status) && getFilesProcessed(result.status) === 0) {
1147
+ const errCount = result.errors?.length ?? 0;
1148
+ const errDetail = errCount > 0 ? ` (${errCount} file error(s))` : '';
1277
1149
  warnings.push({
1278
1150
  analyzerName,
1279
1151
  kind: 'zero-files',
1280
- message: `⚠️ ${analyzerName} analyzer: filesProcessed = 0. ` +
1152
+ message: `⚠️ ${analyzerName} analyzer: filesProcessed = 0${errDetail}. ` +
1281
1153
  `The analyzer ran but matched zero source files. Check file extensions, ` +
1282
1154
  `scanner configuration, and project structure.`,
1283
1155
  });