code-auditor-mcp 3.4.9 → 3.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +10 -3
- package/CHANGELOG.md +81 -0
- package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.js +2 -1
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +0 -2
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +74 -99
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.d.ts +1 -5
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +13 -69
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts +1 -6
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.js +16 -41
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts +20 -3
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +4 -165
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +43 -69
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +62 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +357 -3
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +81 -69
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +319 -325
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +32 -37
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/auditRunner.d.ts +2 -7
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +306 -434
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +27 -5
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts +22 -0
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +92 -44
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/config/defaults.js +3 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/conventions/conventionMiner.d.ts +5 -4
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +65 -25
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/dataPaths.js +1 -1
- package/dist/dataPaths.js.map +1 -1
- package/dist/invariants/index.d.ts +1 -1
- package/dist/invariants/index.d.ts.map +1 -1
- package/dist/invariants/index.js +1 -1
- package/dist/invariants/index.js.map +1 -1
- package/dist/invariants/ruleEngine.d.ts +38 -3
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +42 -174
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts +2 -2
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js +3 -1
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +60 -16
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js +10 -6
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp.js +0 -0
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +4 -1
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/pipeline.d.ts +74 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +620 -0
- package/dist/pipeline.js.bak +465 -0
- package/dist/pipeline.js.bak2 +465 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/pipelineAdapters.d.ts +116 -0
- package/dist/pipelineAdapters.d.ts.map +1 -0
- package/dist/pipelineAdapters.js +1146 -0
- package/dist/pipelineAdapters.js.map +1 -0
- package/dist/pipelineTypes.d.ts +44 -0
- package/dist/pipelineTypes.d.ts.map +1 -0
- package/dist/pipelineTypes.js +118 -0
- package/dist/pipelineTypes.js.map +1 -0
- package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
- package/dist/reporting/csvReportGenerator.js +2 -1
- package/dist/reporting/csvReportGenerator.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +4 -2
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +2 -1
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/styles/cssAstExtractor.d.ts +46 -0
- package/dist/styles/cssAstExtractor.d.ts.map +1 -0
- package/dist/styles/cssAstExtractor.js +439 -0
- package/dist/styles/cssAstExtractor.js.map +1 -0
- package/dist/styles/styleExtractor.d.ts +2 -2
- package/dist/styles/styleExtractor.d.ts.map +1 -1
- package/dist/styles/styleExtractor.js +69 -16
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +4 -0
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindProbe.d.ts +2 -0
- package/dist/styles/tailwindProbe.d.ts.map +1 -1
- package/dist/styles/tailwindProbe.js +5 -5
- package/dist/styles/tailwindProbe.js.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts +3 -0
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js +7 -0
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/types.d.ts +216 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +4 -0
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +7 -1
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/utils/reactDetection.d.ts.map +1 -1
- package/dist/utils/reactDetection.js +32 -0
- package/dist/utils/reactDetection.js.map +1 -1
- package/package.json +2 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
package/dist/auditRunner.js
CHANGED
|
@@ -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
|
-
|
|
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 = {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
671
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
1276
|
-
|
|
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
|
});
|