code-auditor-mcp 3.4.18 → 3.5.0

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 (171) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +68 -0
  3. package/README.md +5 -5
  4. package/dist/analyzers/applicability.d.ts +13 -0
  5. package/dist/analyzers/applicability.d.ts.map +1 -1
  6. package/dist/analyzers/applicability.js +37 -0
  7. package/dist/analyzers/applicability.js.map +1 -1
  8. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  9. package/dist/analyzers/cross-language/APIContractAnalyzer.js +23 -2
  10. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  11. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +52 -9
  12. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  13. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +228 -55
  14. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  15. package/dist/analyzers/cross-language/SchemaValidator.d.ts +19 -12
  16. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  17. package/dist/analyzers/cross-language/SchemaValidator.js +78 -111
  18. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  19. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +0 -6
  20. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  21. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +85 -73
  22. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  23. package/dist/analyzers/documentationAnalyzer.js +5 -5
  24. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  25. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  26. package/dist/analyzers/invariantsAnalyzer.js +1 -0
  27. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  28. package/dist/analyzers/provenance.d.ts +7 -9
  29. package/dist/analyzers/provenance.d.ts.map +1 -1
  30. package/dist/analyzers/provenance.js +88 -124
  31. package/dist/analyzers/provenance.js.map +1 -1
  32. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  33. package/dist/analyzers/reactAnalyzer.js +120 -21
  34. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  35. package/dist/analyzers/ruleRegistry.d.ts +21 -0
  36. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  37. package/dist/analyzers/ruleRegistry.js +61 -12
  38. package/dist/analyzers/ruleRegistry.js.map +1 -1
  39. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  40. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +60 -24
  41. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  42. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  43. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +5 -2
  44. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  45. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  46. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +82 -71
  47. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  48. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +2 -1
  49. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  50. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +23 -12
  51. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  52. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +11 -1
  53. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  54. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +60 -22
  55. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  56. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -3
  57. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  58. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +258 -87
  59. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  60. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
  61. package/dist/analyzers/universal/schema/codeAnalysis.js +7 -1
  62. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  63. package/dist/analyzers/universal/schema/jsonSchema.js +7 -11
  64. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
  65. package/dist/auditRouter.d.ts +41 -0
  66. package/dist/auditRouter.d.ts.map +1 -0
  67. package/dist/auditRouter.js +185 -0
  68. package/dist/auditRouter.js.map +1 -0
  69. package/dist/auditRunner.d.ts.map +1 -1
  70. package/dist/auditRunner.js +93 -57
  71. package/dist/auditRunner.js.map +1 -1
  72. package/dist/cli.js +239 -18
  73. package/dist/cli.js.map +1 -1
  74. package/dist/codeIndexDB.d.ts.map +1 -1
  75. package/dist/codeIndexDB.js +35 -1
  76. package/dist/codeIndexDB.js.map +1 -1
  77. package/dist/componentScanner.d.ts.map +1 -1
  78. package/dist/componentScanner.js +2 -1
  79. package/dist/componentScanner.js.map +1 -1
  80. package/dist/config/configLoader.d.ts +9 -0
  81. package/dist/config/configLoader.d.ts.map +1 -1
  82. package/dist/config/configLoader.js +30 -2
  83. package/dist/config/configLoader.js.map +1 -1
  84. package/dist/config/defaults.d.ts.map +1 -1
  85. package/dist/config/defaults.js +9 -2
  86. package/dist/config/defaults.js.map +1 -1
  87. package/dist/functionScanner.d.ts.map +1 -1
  88. package/dist/functionScanner.js +1 -17
  89. package/dist/functionScanner.js.map +1 -1
  90. package/dist/invariants/ruleEngine.d.ts +8 -0
  91. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  92. package/dist/invariants/ruleEngine.js +35 -16
  93. package/dist/invariants/ruleEngine.js.map +1 -1
  94. package/dist/languages/LanguageOrchestrator.d.ts +9 -10
  95. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  96. package/dist/languages/LanguageOrchestrator.js +135 -101
  97. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  98. package/dist/languages/RuntimeManager.d.ts +28 -1
  99. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  100. package/dist/languages/RuntimeManager.js +158 -47
  101. package/dist/languages/RuntimeManager.js.map +1 -1
  102. package/dist/languages/go/GoAdapter.d.ts +2 -0
  103. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  104. package/dist/languages/go/GoAdapter.js +15 -22
  105. package/dist/languages/go/GoAdapter.js.map +1 -1
  106. package/dist/languages/go/analyzer +0 -0
  107. package/dist/languages/go/analyzer-src/analyzer.go +510 -0
  108. package/dist/languages/go/analyzer-src/go.mod +3 -0
  109. package/dist/languages/go/analyzer-src/indexer.go +268 -0
  110. package/dist/languages/go/analyzer-src/parser.go +461 -0
  111. package/dist/languages/go/analyzer-src/solid.go +395 -0
  112. package/dist/languages/go/analyzer-src/testconventions.go +25 -0
  113. package/dist/languages/go/analyzer-src/types.go +148 -0
  114. package/dist/languages/go/go.mod +7 -0
  115. package/dist/languages/go/main.go +203 -0
  116. package/dist/languages/testConventions.d.ts +35 -0
  117. package/dist/languages/testConventions.d.ts.map +1 -0
  118. package/dist/languages/testConventions.js +51 -0
  119. package/dist/languages/testConventions.js.map +1 -0
  120. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
  121. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  122. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +25 -18
  123. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  124. package/dist/mcp-tools/whitelistTools.d.ts +1 -1
  125. package/dist/mcp-tools-shared.d.ts +0 -3
  126. package/dist/mcp-tools-shared.d.ts.map +1 -1
  127. package/dist/mcp-tools-shared.js +8 -117
  128. package/dist/mcp-tools-shared.js.map +1 -1
  129. package/dist/mcp.js +3 -1
  130. package/dist/mcp.js.map +1 -1
  131. package/dist/mcpAuditJobs.d.ts.map +1 -1
  132. package/dist/mcpAuditJobs.js +13 -0
  133. package/dist/mcpAuditJobs.js.map +1 -1
  134. package/dist/nextFile.d.ts +21 -0
  135. package/dist/nextFile.d.ts.map +1 -0
  136. package/dist/nextFile.js +51 -0
  137. package/dist/nextFile.js.map +1 -0
  138. package/dist/nextFileIncremental.d.ts +141 -0
  139. package/dist/nextFileIncremental.d.ts.map +1 -0
  140. package/dist/nextFileIncremental.js +314 -0
  141. package/dist/nextFileIncremental.js.map +1 -0
  142. package/dist/pipeline.d.ts.map +1 -1
  143. package/dist/pipeline.js +22 -0
  144. package/dist/pipeline.js.map +1 -1
  145. package/dist/pipelineAdapters.d.ts +17 -0
  146. package/dist/pipelineAdapters.d.ts.map +1 -1
  147. package/dist/pipelineAdapters.js +791 -40
  148. package/dist/pipelineAdapters.js.map +1 -1
  149. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  150. package/dist/reporting/jsonReportGenerator.js +11 -30
  151. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  152. package/dist/styles/styleIndexer.d.ts +7 -0
  153. package/dist/styles/styleIndexer.d.ts.map +1 -1
  154. package/dist/styles/styleIndexer.js +44 -61
  155. package/dist/styles/styleIndexer.js.map +1 -1
  156. package/dist/types/crossLanguage.d.ts +1 -0
  157. package/dist/types/crossLanguage.d.ts.map +1 -1
  158. package/dist/types.d.ts +27 -4
  159. package/dist/types.d.ts.map +1 -1
  160. package/dist/types.js.map +1 -1
  161. package/dist/utils/fileDiscovery.d.ts +9 -0
  162. package/dist/utils/fileDiscovery.d.ts.map +1 -1
  163. package/dist/utils/fileDiscovery.js +18 -0
  164. package/dist/utils/fileDiscovery.js.map +1 -1
  165. package/package.json +6 -4
  166. package/plugin/.claude-plugin/plugin.json +1 -1
  167. package/plugin/README.md +6 -6
  168. package/plugin/hooks/hooks.json +4 -0
  169. package/plugin/scripts/hook-self-audit.sh +107 -0
  170. package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +55 -9
  171. package/plugin/skills/code-auditor/SKILL.md +20 -21
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Audit router — the single entry point for choosing between the two analysis
3
+ * paths: the tree-sitter TypeScript pipeline (`createAuditRunner`) and the Go
4
+ * subprocess orchestrator (`LanguageOrchestrator`).
5
+ *
6
+ * The CLI and the MCP `audit.run` tool both need the same decision — "are there
7
+ * Go files? route to the Go subprocess; otherwise use the TS pipeline" — and
8
+ * for months they did not share it: `handleAudit` checked for `.go` and routed
9
+ * to the orchestrator, while the CLI called `createAuditRunner` directly and
10
+ * fed Go ASTs to TypeScript-tuned analyzers (the 762-finding gin result). Two
11
+ * copies, one silently wrong — the same shape as the duplicate
12
+ * `getLanguageFromPath`, one layer up.
13
+ *
14
+ * This module is that decision, in exactly one place. Both surfaces call
15
+ * `runAuditDispatch`; a future refactor that unwires one of them can no longer
16
+ * silently break the other.
17
+ */
18
+ import * as path from 'path';
19
+ import { promises as fs } from 'fs';
20
+ import { createAuditRunner } from './auditRunner.js';
21
+ import { RuntimeManager } from './languages/RuntimeManager.js';
22
+ import { LanguageOrchestrator } from './languages/LanguageOrchestrator.js';
23
+ import { CodeIndexDB } from './codeIndexDB.js';
24
+ import { makeVisitorStatus } from './pipeline.js';
25
+ import { DEFAULT_EXCLUDED_ANY_DEPTH_DIRS } from './utils/fileDiscovery.js';
26
+ /**
27
+ * True when `target` (a file or directory) is, or contains, a file with the
28
+ * given extension. Walked recursively for directories.
29
+ *
30
+ * The walk skips the same any-depth excluded dirs discovery skips
31
+ * (`node_modules`, `.git`, `dist`, …). Without that, a single `.go` file
32
+ * buried in a dependency's `node_modules` (e.g. `flatted`'s golang port) would
33
+ * route an otherwise pure-TS project through the Go subprocess — which drops
34
+ * every non-Go analyzer and collapses the audit to a fraction of its findings.
35
+ */
36
+ export async function hasFilesWithExtension(target, ext) {
37
+ try {
38
+ const stats = await fs.stat(target);
39
+ if (stats.isFile()) {
40
+ return path.extname(target).toLowerCase() === ext;
41
+ }
42
+ const entries = await fs.readdir(target, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ if (entry.isFile()) {
45
+ if (path.extname(entry.name).toLowerCase() === ext)
46
+ return true;
47
+ }
48
+ else if (entry.isDirectory()) {
49
+ if (DEFAULT_EXCLUDED_ANY_DEPTH_DIRS.has(entry.name))
50
+ continue;
51
+ if (await hasFilesWithExtension(path.join(target, entry.name), ext))
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ /**
62
+ * Convert a polyglot analysis result to the legacy `AuditResult` shape the
63
+ * reporting and CLI surfaces consume.
64
+ */
65
+ export function convertPolyglotToAuditResult(polyglotResult, auditPath) {
66
+ const violations = polyglotResult.violations || [];
67
+ const criticalIssues = violations.filter((v) => v.severity === 'critical').length;
68
+ const warnings = violations.filter((v) => v.severity === 'warning').length;
69
+ const suggestions = violations.filter((v) => v.severity === 'suggestion').length;
70
+ const totalFiles = polyglotResult.metrics?.totalFiles || 0;
71
+ const executionTime = polyglotResult.metrics?.executionTime || 0;
72
+ // Bucket violations by their category for the summary. The Go subprocess
73
+ // labels findings with `category` (open-closed, import-style, …); the
74
+ // TypeScript pipeline uses `type` for the same purpose. Fall back to the
75
+ // analyzer label so a categoryless finding still lands in a real bucket
76
+ // rather than vanishing into an empty `violationsByCategory`.
77
+ const violationsByCategory = {};
78
+ for (const v of violations) {
79
+ const category = v.category || v.type || v.analyzer || 'unknown';
80
+ violationsByCategory[category] = (violationsByCategory[category] || 0) + 1;
81
+ }
82
+ const topIssues = Object.entries(violationsByCategory)
83
+ .sort(([, a], [, b]) => b - a)
84
+ .slice(0, 5)
85
+ .map(([type, count]) => ({ type, count }));
86
+ // Build analyzerResults dynamically from the actual analyzer labels on the
87
+ // violations. The Go subprocess labels findings with the analyzer that
88
+ // produced them (`solid`, and potentially `imports`/`errors`/`goroutines`/
89
+ // `channels`) — never `go`. A hardcoded `go` bucket filtering
90
+ // `v.analyzer === 'go'` was therefore structurally always zero.
91
+ const analyzerResults = {};
92
+ for (const v of violations) {
93
+ const key = v.analyzer || 'unknown';
94
+ if (!analyzerResults[key]) {
95
+ analyzerResults[key] = {
96
+ violations: [],
97
+ status: makeVisitorStatus(totalFiles),
98
+ executionTime,
99
+ analyzerName: key,
100
+ };
101
+ }
102
+ analyzerResults[key].violations.push(v);
103
+ }
104
+ // Surface languages discovered on disk but skipped (no runtime), plus any
105
+ // subprocess errors, as diagnostics — so "Go files present, Go toolchain
106
+ // not found" reaches the report instead of collapsing into a silent zero.
107
+ const diagnostics = [];
108
+ for (const na of polyglotResult.notApplicable || []) {
109
+ diagnostics.push({ analyzerName: na.language, kind: 'notApplicable', message: na.reason });
110
+ }
111
+ for (const err of polyglotResult.errors || []) {
112
+ diagnostics.push({
113
+ analyzerName: err.language || 'polyglot',
114
+ kind: err.type || 'error',
115
+ message: err.message,
116
+ });
117
+ }
118
+ return {
119
+ timestamp: new Date(),
120
+ summary: {
121
+ totalViolations: violations.length,
122
+ criticalIssues,
123
+ warnings,
124
+ suggestions,
125
+ totalFiles,
126
+ violationsByCategory,
127
+ topIssues,
128
+ },
129
+ analyzerResults,
130
+ recommendations: [],
131
+ metadata: {
132
+ auditDuration: executionTime,
133
+ filesAnalyzed: totalFiles,
134
+ analyzersRun: Object.keys(analyzerResults),
135
+ fileToFunctionsMap: polyglotResult.indexEntries
136
+ ? createFileToFunctionsMap(polyglotResult.indexEntries)
137
+ : {},
138
+ ...(diagnostics.length > 0 && { diagnostics }),
139
+ },
140
+ };
141
+ }
142
+ function createFileToFunctionsMap(indexEntries) {
143
+ const fileMap = {};
144
+ for (const entry of indexEntries) {
145
+ if (!fileMap[entry.file])
146
+ fileMap[entry.file] = [];
147
+ fileMap[entry.file].push(entry);
148
+ }
149
+ return fileMap;
150
+ }
151
+ /**
152
+ * Run an audit, routing to the Go subprocess when the target contains Go files
153
+ * and to the TypeScript tree-sitter pipeline otherwise. The single place both
154
+ * the CLI and MCP surfaces make that decision.
155
+ */
156
+ export async function runAuditDispatch(options) {
157
+ // Resolve to an absolute path up front. The Go subprocess is spawned with a
158
+ // cwd of its own (dist/languages/go), so relative file paths produced by
159
+ // discovery (e.g. `bench/real/gin/auth.go`) would not resolve from its cwd —
160
+ // the subprocess errors "no such file or directory" and the whole audit
161
+ // collapses to zero findings. Absolute paths make discovery absolute, which
162
+ // works for both the in-process TS half and the Go subprocess.
163
+ const projectRoot = path.resolve(options.projectRoot || process.cwd());
164
+ const hasGo = await hasFilesWithExtension(projectRoot, '.go');
165
+ if (!hasGo) {
166
+ const runner = createAuditRunner(options);
167
+ return runner.run();
168
+ }
169
+ // Go present → the polyglot orchestrator, which runs the Go subprocess for
170
+ // `.go` files and the node runtime for any TypeScript/JavaScript alongside.
171
+ const runtimeManager = new RuntimeManager();
172
+ await runtimeManager.initialize();
173
+ const codeIndex = CodeIndexDB.getInstance();
174
+ await codeIndex.initialize();
175
+ const orchestrator = new LanguageOrchestrator(runtimeManager, codeIndex);
176
+ const polyglotResult = await orchestrator.analyzePolyglotProject(projectRoot, {
177
+ analyzers: options.enabledAnalyzers,
178
+ minSeverity: options.minSeverity,
179
+ updateIndex: options.indexFunctions,
180
+ enableCrossLanguageAnalysis: true,
181
+ buildCrossReferences: true,
182
+ });
183
+ return convertPolyglotToAuditResult(polyglotResult, projectRoot);
184
+ }
185
+ //# sourceMappingURL=auditRouter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auditRouter.js","sourceRoot":"","sources":["../src/auditRouter.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auditRunner.d.ts","sourceRoot":"","sources":["../src/auditRunner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,cAAc,EAWf,MAAM,YAAY,CAAC;AA6DpB;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB;oCAiBnB,MAAM,KAAG,OAAO,CAAC,kBAAkB,CAAC;uBAQjD,kBAAkB,KAAG,OAAO,CAAC,WAAW,CAAC;6BAogC1B,WAAW,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;EAS7F;AAkRD,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,GAAG,YAAY,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,gBAAgB,EAAE,MAAM,EAAE,EAC1B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EAC/C,UAAU,CAAC,EAAE,MAAM,GAClB,iBAAiB,EAAE,CA0CrB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAGjF;AAED,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"auditRunner.d.ts","sourceRoot":"","sources":["../src/auditRunner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,cAAc,EAWf,MAAM,YAAY,CAAC;AAmEpB;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB;oCAQnB,MAAM,KAAG,OAAO,CAAC,kBAAkB,CAAC;uBAQjD,kBAAkB,KAAG,OAAO,CAAC,WAAW,CAAC;6BAijC1B,WAAW,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;EAS7F;AAkRD,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,GAAG,YAAY,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,gBAAgB,EAAE,MAAM,EAAE,EAC1B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EAC/C,UAAU,CAAC,EAAE,MAAM,GAClB,iBAAiB,EAAE,CA0CrB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAGjF;AAED,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
@@ -11,9 +11,10 @@ import { randomUUID } from 'node:crypto';
11
11
  import { AuditAbortedError, AuditHandoffError, } from './types.js';
12
12
  import { discoverFiles, discoverFilesDetailed } from './utils/fileDiscovery.js';
13
13
  import { FileAccounting } from './services/fileAccounting.js';
14
- import { loadConfig } from './config/configLoader.js';
14
+ import { loadConfig, findConfigFileUp } from './config/configLoader.js';
15
15
  import { mergePathProfiles } from './config/defaults.js';
16
16
  import { checkThresholdRationales } from './config/thresholdRationales.js';
17
+ import { ALL_ANALYZERS } from './analyzers/ruleRegistry.js';
17
18
  import { applyPresets } from './presets/presets.js';
18
19
  import { generateReport } from './reporting/reportGenerator.js';
19
20
  import { extractFunctionsFromFile } from './functionScanner.js';
@@ -29,7 +30,7 @@ import { CodeIndexDB } from './codeIndexDB.js';
29
30
  import { writeAuditToLedger, detectRunInput } from './ledger.js';
30
31
  // Pipeline imports (Spec 25 — pipeline replaces hand-rolled analyzer loop)
31
32
  import { runPipeline, writeIndexFactsToDb, makeVisitorStatus, getFilesProcessed, isVisitorStatus, buildCoverageReport } from './pipeline.js';
32
- import { createSolidVisitor, createDryVisitor, createDataAccessVisitor, createDocumentationVisitor, createFunctionIndexVisitor, createStylesCssVisitor, createReactVisitor, createStylesReducer, createConventionsReducer, createCrossDomainReducer, createInvariantsReducer, createSchemaSqlVisitor, createSchemaCodeVisitor, createSchemaPrismaVisitor, createSchemaJsonVisitor, createSchemaReducer, } from './pipelineAdapters.js';
33
+ import { createSolidVisitor, createDryVisitor, createDataAccessVisitor, createDocumentationVisitor, createFunctionIndexVisitor, createStylesCssVisitor, createStylesSourceVisitor, createReactVisitor, createStylesReducer, createConventionsReducer, createCrossDomainReducer, createInvariantsReducer, createSchemaSqlVisitor, createSchemaCodeVisitor, createSchemaPrismaVisitor, createSchemaJsonVisitor, createSchemaReducer, createCrossLanguageEntityVisitor, createSchemaValidatorReducer, createAPIContractReducer, createDependencyGraphReducer, } from './pipelineAdapters.js';
33
34
  // Package version — read once at module load
34
35
  const __auditRunnerDirname = path.dirname(fileURLToPath(import.meta.url));
35
36
  const _pkg = JSON.parse(readFileSync(path.join(__auditRunnerDirname, '..', 'package.json'), 'utf-8'));
@@ -44,18 +45,7 @@ const SCHEMA_SUB_VISITORS = ['schema-sql', 'schema-code', 'schema-prisma', 'sche
44
45
  * Create an audit runner with the given options
45
46
  */
46
47
  export function createAuditRunner(options = {}) {
47
- const analyzerRegistry = {
48
- solid: { name: 'solid' },
49
- dry: { name: 'dry' },
50
- 'data-access': { name: 'data-access' },
51
- react: { name: 'react' },
52
- documentation: { name: 'documentation' },
53
- invariants: { name: 'invariants' },
54
- schema: { name: 'schema' },
55
- styles: { name: 'styles' },
56
- conventions: { name: 'conventions' },
57
- 'cross-domain': { name: 'cross-domain' },
58
- };
48
+ const analyzerRegistry = Object.fromEntries(ALL_ANALYZERS.map((name) => [name, { name }]));
59
49
  /**
60
50
  * Load configuration from file
61
51
  */
@@ -67,19 +57,16 @@ export function createAuditRunner(options = {}) {
67
57
  * Run the audit
68
58
  */
69
59
  async function run(runOptions) {
70
- // Auto-load .codeauditor.json from project root so the programmatic API
71
- // respects the same config the CLI surface reads. RunOptions (caller)
72
- // override file config, and createAuditRunner options override both.
60
+ // Auto-load .codeauditor.json so the programmatic API respects the same
61
+ // config the CLI surface reads. Walk UP from the audit path so a scoped
62
+ // audit (`--path src`) still finds the project-root config rather than
63
+ // silently using defaults. RunOptions (caller) override file config, and
64
+ // createAuditRunner options override both.
73
65
  const rootForConfig = runOptions?.projectRoot || options.projectRoot || process.cwd();
74
- let fileConfig = {};
75
- try {
76
- const configPath = path.join(rootForConfig, '.codeauditor.json');
77
- await fs.access(configPath);
78
- fileConfig = await loadConfig({ configPath });
79
- }
80
- catch {
81
- // No config file — proceed with defaults
82
- }
66
+ const configPath = await findConfigFileUp(rootForConfig);
67
+ const fileConfig = configPath
68
+ ? await loadConfig({ configPath })
69
+ : {};
83
70
  const mergedOptions = { ...fileConfig, ...options, ...runOptions };
84
71
  // Spec 36 R5 — a threshold change needs a written rationale. Check the
85
72
  // user-facing analyzerConfigs layer (project config + inline options)
@@ -316,12 +303,17 @@ export function createAuditRunner(options = {}) {
316
303
  // `.css`/`.scss`), so `consumedFiles` is the "any layer read it" evidence
317
304
  // for files dropped at stage 2 — threaded into the pipeline below.
318
305
  let styleConsumedFiles = [];
306
+ // In-scope files that contributed style data during the sync. Undefined when
307
+ // the sync did not run or failed — the styles reducer must not short-circuit
308
+ // on that (it falls back to full analysis), so we only assign on success.
309
+ let styleContributingFiles;
319
310
  if (enabledAnalyzers.includes('styles')) {
320
311
  try {
321
312
  const styleDb = CodeIndexDB.getInstance(undefined, root);
322
313
  await styleDb.initialize();
323
314
  const styleSyncResult = await syncStyleIndex(styleDb.rawDb, files, root, { scoped: isScoped });
324
315
  styleConsumedFiles = styleSyncResult.consumedFiles;
316
+ styleContributingFiles = styleSyncResult.contributingFiles;
325
317
  logMcpInfo('style-index', 'style index sync complete', {
326
318
  changed: styleSyncResult.changed,
327
319
  skipped: styleSyncResult.skipped,
@@ -394,6 +386,7 @@ export function createAuditRunner(options = {}) {
394
386
  exec: (sql) => auditIndex.rawDb.exec(sql),
395
387
  getMeta: (key) => auditIndex.getMeta(key),
396
388
  getUntestedTopDecile: (td) => auditIndex.getUntestedTopDecile(td),
389
+ rawDb: auditIndex.rawDb,
397
390
  };
398
391
  }
399
392
  // ── 1. Build pipeline adapters ───────────────────────────────────────
@@ -409,6 +402,10 @@ export function createAuditRunner(options = {}) {
409
402
  // styles-css visitor — AST-extracts .css files into style_* tables (Spec 26 Phase 2)
410
403
  if (enabledAnalyzers.includes('styles'))
411
404
  pipelineVisitors.push(createStylesCssVisitor());
405
+ // styles-source visitor — AST-extracts TS/JS CSS-in-JS into style_* tables,
406
+ // reusing the stage-1 parse (eliminates the style-index re-parse).
407
+ if (enabledAnalyzers.includes('styles'))
408
+ pipelineVisitors.push(createStylesSourceVisitor());
412
409
  if (enabledAnalyzers.includes('solid'))
413
410
  pipelineVisitors.push(createSolidVisitor());
414
411
  if (enabledAnalyzers.includes('dry')) {
@@ -438,6 +435,22 @@ export function createAuditRunner(options = {}) {
438
435
  pipelineVisitors.push(createSchemaJsonVisitor());
439
436
  pipelineReducers.push(createSchemaReducer());
440
437
  }
438
+ // Cross-language analyzers (SchemaValidator, APIContractAnalyzer,
439
+ // DependencyGraphBuilder) — three Stage-4 reducers fed by one shared
440
+ // entity-extraction visitor. The visitor is cheap (per-file) and runs
441
+ // whenever ANY cross-language analyzer is enabled; the corpus-wide reducers
442
+ // short-circuit on scoped/diff runs.
443
+ const crossLanguageEnabled = ['schema-validator', 'api-contract', 'dependency-graph']
444
+ .some((a) => enabledAnalyzers.includes(a));
445
+ if (crossLanguageEnabled) {
446
+ pipelineVisitors.push(createCrossLanguageEntityVisitor());
447
+ }
448
+ if (enabledAnalyzers.includes('schema-validator'))
449
+ pipelineDerivedReducers.push(createSchemaValidatorReducer());
450
+ if (enabledAnalyzers.includes('api-contract'))
451
+ pipelineDerivedReducers.push(createAPIContractReducer());
452
+ if (enabledAnalyzers.includes('dependency-graph'))
453
+ pipelineDerivedReducers.push(createDependencyGraphReducer());
441
454
  // ── 2. Safeguard warnings ────────────────────────────────────────────
442
455
  if (auditIndex) {
443
456
  if (enabledAnalyzers.includes('cross-domain')) {
@@ -530,6 +543,7 @@ export function createAuditRunner(options = {}) {
530
543
  isScoped,
531
544
  fileAccounting,
532
545
  consumedFilePaths: styleConsumedFiles,
546
+ styleContributingFiles,
533
547
  onStage2Complete: async (ctx) => {
534
548
  // Post-stage-2 setup: rebuild function_calls from the functions table
535
549
  // (populated by the function-index visitor), then mine conventions.
@@ -600,7 +614,7 @@ export function createAuditRunner(options = {}) {
600
614
  // zero-violation rules promote from `unassessed` to `clean`/`notApplicable`.
601
615
  pipelineCoverage = buildCoverageReport(analyzerResults, pipelineConfig, pipelineResult.metadata?.inputPresence, new Map((pipelineResult.metadata?.ruleApplicability ?? []).map((a) => [
602
616
  a.ruleId,
603
- { applicable: a.applicable, reason: a.reason },
617
+ { applicable: a.applicable, reason: a.reason, kind: a.kind },
604
618
  ])));
605
619
  // Spec 29: extract table catalog from pipeline metadata for audit report
606
620
  pipelineTableCatalog = pipelineResult.metadata?.tableCatalog;
@@ -662,6 +676,22 @@ export function createAuditRunner(options = {}) {
662
676
  }
663
677
  }
664
678
  }
679
+ // ── Normalize the analyzer field on every violation to its result key ──
680
+ // Violations are emitted by analyzers that don't always stamp `analyzer`
681
+ // correctly: the react analyzer omits it entirely, and the schema sub-visitor
682
+ // helpers (createSchemaViolation) hardcode `analyzer: 'schema'` for every
683
+ // sub-visitor, collapsing schema-code/schema-sql/schema-prisma/schema-json
684
+ // into the parent 'schema'. Both surface in the baseline metadata as
685
+ // `unknown: 338` / an over-broad `schema` bucket. The result key IS the
686
+ // analyzer that emitted the finding, so stamp it here as the single source
687
+ // of truth — a no-op for every analyzer that already labels itself, and it
688
+ // also fixes any future analyzer that forgets. Runs before baseline
689
+ // classification (Spec 18) so fingerprints and analyzerCounts are correct.
690
+ for (const [resultName, result] of Object.entries(orderedAnalyzerResults)) {
691
+ for (const v of result.violations) {
692
+ v.analyzer = resultName;
693
+ }
694
+ }
665
695
  // ── Spec 13 R5 Phase 1: Persist seeded DRY pairs ──────────────────
666
696
  // Store pairs from DRY analysis in dry_pair_history for divergence tracking.
667
697
  // Pair identity is fingerprint-based (file + nodeType + line),
@@ -690,36 +720,6 @@ export function createAuditRunner(options = {}) {
690
720
  catch {
691
721
  // dry_pair_history persistence is advisory — non-fatal
692
722
  }
693
- // ── Baseline classification (Spec 18 R1) ────────────────────────────
694
- const projectRoot = path.resolve(mergedOptions.projectRoot || process.cwd());
695
- const baseline = loadBaseline(projectRoot);
696
- let baselineMetadata;
697
- if (baseline) {
698
- const allViolations = Object.values(orderedAnalyzerResults).flatMap((r) => r.violations);
699
- // For scoped runs, limit "fixed" to in-scope files
700
- const scopedFiles = isScoped ? files : undefined;
701
- const classified = matchFindings(allViolations, baseline, scopedFiles);
702
- // Tag violations with their baseline status
703
- for (const v of classified.new) {
704
- v.new = true;
705
- }
706
- for (const v of classified.known) {
707
- v.new = false;
708
- }
709
- baselineMetadata = {
710
- present: true,
711
- hash: hashBaseline(baseline),
712
- newCount: classified.new.length,
713
- fixedCount: classified.fixed.length,
714
- knownCount: classified.known.length,
715
- previousKnownCount: baseline.metadata.totalFindings,
716
- };
717
- logMcpInfo('baseline', 'baseline classification complete', {
718
- new: classified.new.length,
719
- fixed: classified.fixed.length,
720
- known: classified.known.length,
721
- });
722
- }
723
723
  // ── Spec 36 R7 — suppression decay ──────────────────────────────────
724
724
  // Inline `code-audit-disable-*` directives suppress a finding only while
725
725
  // it fires, carry a required reason, and are themselves an error when
@@ -900,6 +900,42 @@ export function createAuditRunner(options = {}) {
900
900
  for (const ar of Object.values(orderedAnalyzerResults)) {
901
901
  validateHookContract(ar.violations);
902
902
  }
903
+ // ── Baseline classification (Spec 18 R1) ────────────────────────────
904
+ // Runs AFTER validateHookContract (and after divergence tracking) so it
905
+ // classifies exactly the violation set that reaches the report. Previously
906
+ // this ran before the hook-contract guard spliced out malformed violations
907
+ // (empty file / line 0 / missing line), so newCount+knownCount over-counted
908
+ // by the number of spliced findings and no longer reconciled with
909
+ // summary.totalViolations.
910
+ const projectRoot = path.resolve(mergedOptions.projectRoot || process.cwd());
911
+ const baseline = loadBaseline(projectRoot);
912
+ let baselineMetadata;
913
+ if (baseline) {
914
+ const allViolations = Object.values(orderedAnalyzerResults).flatMap((r) => r.violations);
915
+ // For scoped runs, limit "fixed" to in-scope files
916
+ const scopedFiles = isScoped ? files : undefined;
917
+ const classified = matchFindings(allViolations, baseline, scopedFiles);
918
+ // Tag violations with their baseline status
919
+ for (const v of classified.new) {
920
+ v.new = true;
921
+ }
922
+ for (const v of classified.known) {
923
+ v.new = false;
924
+ }
925
+ baselineMetadata = {
926
+ present: true,
927
+ hash: hashBaseline(baseline),
928
+ newCount: classified.new.length,
929
+ fixedCount: classified.fixed.length,
930
+ knownCount: classified.known.length,
931
+ previousKnownCount: baseline.metadata.totalFindings,
932
+ };
933
+ logMcpInfo('baseline', 'baseline classification complete', {
934
+ new: classified.new.length,
935
+ fixed: classified.fixed.length,
936
+ known: classified.known.length,
937
+ });
938
+ }
903
939
  // Generate summary
904
940
  const summary = generateSummary(orderedAnalyzerResults, files.length);
905
941
  // Create result