code-auditor-mcp 3.4.11 → 3.4.13

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 (254) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +119 -1
  3. package/dist/analyzers/__verify_self_probe.d.ts +2 -0
  4. package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
  5. package/dist/analyzers/__verify_self_probe.js +5 -0
  6. package/dist/analyzers/__verify_self_probe.js.map +1 -0
  7. package/dist/analyzers/analyzerUtils.d.ts +34 -1
  8. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  9. package/dist/analyzers/analyzerUtils.js +61 -22
  10. package/dist/analyzers/analyzerUtils.js.map +1 -1
  11. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  13. package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
  14. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  15. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
  16. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  17. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
  18. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  19. package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
  20. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  21. package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
  22. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  23. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
  24. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  25. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
  26. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  27. package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
  28. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  29. package/dist/analyzers/documentationAnalyzer.js +259 -198
  30. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  31. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  32. package/dist/analyzers/invariantsAnalyzer.js +59 -46
  33. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  34. package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
  35. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
  36. package/dist/analyzers/orm/adapterRegistry.js +10 -0
  37. package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
  38. package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
  39. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
  40. package/dist/analyzers/orm/drizzleAdapter.js +42 -44
  41. package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
  42. package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
  43. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
  44. package/dist/analyzers/orm/prismaAdapter.js +30 -0
  45. package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
  46. package/dist/analyzers/provenance.d.ts +72 -2
  47. package/dist/analyzers/provenance.d.ts.map +1 -1
  48. package/dist/analyzers/provenance.js +462 -338
  49. package/dist/analyzers/provenance.js.map +1 -1
  50. package/dist/analyzers/reactAnalyzer.d.ts +11 -0
  51. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  52. package/dist/analyzers/reactAnalyzer.js +188 -125
  53. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  54. package/dist/analyzers/ruleRegistry.d.ts +118 -1
  55. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  56. package/dist/analyzers/ruleRegistry.js +1717 -104
  57. package/dist/analyzers/ruleRegistry.js.map +1 -1
  58. package/dist/analyzers/ruleTiming.d.ts +54 -0
  59. package/dist/analyzers/ruleTiming.d.ts.map +1 -0
  60. package/dist/analyzers/ruleTiming.js +71 -0
  61. package/dist/analyzers/ruleTiming.js.map +1 -0
  62. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
  63. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  64. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
  65. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  66. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
  67. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  68. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
  69. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  70. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +24 -186
  71. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  72. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1081 -1061
  73. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  74. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
  75. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  76. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
  77. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  78. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
  79. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  80. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
  81. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  82. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -212
  83. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  84. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1518
  85. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  86. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
  87. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  88. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -472
  89. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  90. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
  91. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
  92. package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
  93. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
  94. package/dist/analyzers/universal/schema/config.d.ts +31 -0
  95. package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
  96. package/dist/analyzers/universal/schema/config.js +57 -0
  97. package/dist/analyzers/universal/schema/config.js.map +1 -0
  98. package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
  99. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
  100. package/dist/analyzers/universal/schema/discovery.js +616 -0
  101. package/dist/analyzers/universal/schema/discovery.js.map +1 -0
  102. package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
  103. package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
  104. package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
  105. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
  106. package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
  107. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
  108. package/dist/analyzers/universal/schema/migrations.js +238 -0
  109. package/dist/analyzers/universal/schema/migrations.js.map +1 -0
  110. package/dist/analyzers/universal/schema/types.d.ts +131 -0
  111. package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
  112. package/dist/analyzers/universal/schema/types.js +10 -0
  113. package/dist/analyzers/universal/schema/types.js.map +1 -0
  114. package/dist/analyzers/universal/schema/violations.d.ts +44 -0
  115. package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
  116. package/dist/analyzers/universal/schema/violations.js +54 -0
  117. package/dist/analyzers/universal/schema/violations.js.map +1 -0
  118. package/dist/auditRunner.d.ts.map +1 -1
  119. package/dist/auditRunner.js +101 -39
  120. package/dist/auditRunner.js.map +1 -1
  121. package/dist/cli.js +348 -40
  122. package/dist/cli.js.map +1 -1
  123. package/dist/config/configLoader.d.ts.map +1 -1
  124. package/dist/config/configLoader.js +9 -7
  125. package/dist/config/configLoader.js.map +1 -1
  126. package/dist/config/defaults.d.ts +12 -3
  127. package/dist/config/defaults.d.ts.map +1 -1
  128. package/dist/config/defaults.js +15 -5
  129. package/dist/config/defaults.js.map +1 -1
  130. package/dist/config/effectiveConfig.d.ts +75 -0
  131. package/dist/config/effectiveConfig.d.ts.map +1 -0
  132. package/dist/config/effectiveConfig.js +213 -0
  133. package/dist/config/effectiveConfig.js.map +1 -0
  134. package/dist/config/pathProfiles.d.ts +7 -4
  135. package/dist/config/pathProfiles.d.ts.map +1 -1
  136. package/dist/config/pathProfiles.js +8 -10
  137. package/dist/config/pathProfiles.js.map +1 -1
  138. package/dist/config/thresholdRationales.d.ts +45 -0
  139. package/dist/config/thresholdRationales.d.ts.map +1 -0
  140. package/dist/config/thresholdRationales.js +67 -0
  141. package/dist/config/thresholdRationales.js.map +1 -0
  142. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  143. package/dist/conventions/conventionMiner.js +6 -2
  144. package/dist/conventions/conventionMiner.js.map +1 -1
  145. package/dist/enforcement/diffGate.d.ts +55 -0
  146. package/dist/enforcement/diffGate.d.ts.map +1 -0
  147. package/dist/enforcement/diffGate.js +136 -0
  148. package/dist/enforcement/diffGate.js.map +1 -0
  149. package/dist/enforcement/gate.d.ts +36 -0
  150. package/dist/enforcement/gate.d.ts.map +1 -0
  151. package/dist/enforcement/gate.js +72 -0
  152. package/dist/enforcement/gate.js.map +1 -0
  153. package/dist/enforcement/suppressions.d.ts +41 -0
  154. package/dist/enforcement/suppressions.d.ts.map +1 -0
  155. package/dist/enforcement/suppressions.js +170 -0
  156. package/dist/enforcement/suppressions.js.map +1 -0
  157. package/dist/fingerprint.d.ts.map +1 -1
  158. package/dist/fingerprint.js +4 -1
  159. package/dist/fingerprint.js.map +1 -1
  160. package/dist/grammars/tree-sitter-scss.wasm +0 -0
  161. package/dist/invariants/ruleEngine.d.ts +3 -4
  162. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  163. package/dist/invariants/ruleEngine.js +2 -2
  164. package/dist/invariants/ruleEngine.js.map +1 -1
  165. package/dist/languages/LanguageOrchestrator.d.ts +14 -0
  166. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  167. package/dist/languages/LanguageOrchestrator.js +93 -55
  168. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  169. package/dist/languages/LanguageRegistry.d.ts +8 -0
  170. package/dist/languages/LanguageRegistry.d.ts.map +1 -1
  171. package/dist/languages/LanguageRegistry.js +8 -0
  172. package/dist/languages/LanguageRegistry.js.map +1 -1
  173. package/dist/languages/RuntimeManager.d.ts +82 -60
  174. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  175. package/dist/languages/RuntimeManager.js +404 -349
  176. package/dist/languages/RuntimeManager.js.map +1 -1
  177. package/dist/languages/UniversalAnalyzer.d.ts +51 -6
  178. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  179. package/dist/languages/UniversalAnalyzer.js +127 -88
  180. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  181. package/dist/languages/adapterBridge.d.ts +36 -0
  182. package/dist/languages/adapterBridge.d.ts.map +1 -1
  183. package/dist/languages/adapterBridge.js +89 -35
  184. package/dist/languages/adapterBridge.js.map +1 -1
  185. package/dist/languages/go/GoAdapter.d.ts +66 -23
  186. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  187. package/dist/languages/go/GoAdapter.js +553 -474
  188. package/dist/languages/go/GoAdapter.js.map +1 -1
  189. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +38 -9
  190. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
  191. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +111 -74
  192. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  193. package/dist/languages/tree-sitter/converter.d.ts +18 -0
  194. package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
  195. package/dist/languages/tree-sitter/converter.js +18 -0
  196. package/dist/languages/tree-sitter/converter.js.map +1 -1
  197. package/dist/languages/tree-sitter/parser.d.ts +39 -2
  198. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  199. package/dist/languages/tree-sitter/parser.js +95 -12
  200. package/dist/languages/tree-sitter/parser.js.map +1 -1
  201. package/dist/languages/types.d.ts +67 -2
  202. package/dist/languages/types.d.ts.map +1 -1
  203. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -62
  204. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  205. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1461 -791
  206. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  207. package/dist/mcp-tools/whitelistTools.d.ts +1 -1
  208. package/dist/mcp-tools-shared.d.ts.map +1 -1
  209. package/dist/mcp-tools-shared.js +1 -0
  210. package/dist/mcp-tools-shared.js.map +1 -1
  211. package/dist/mcpAuditJobs.d.ts.map +1 -1
  212. package/dist/mcpAuditJobs.js +1 -0
  213. package/dist/mcpAuditJobs.js.map +1 -1
  214. package/dist/pipeline.d.ts +30 -1
  215. package/dist/pipeline.d.ts.map +1 -1
  216. package/dist/pipeline.js +527 -203
  217. package/dist/pipeline.js.map +1 -1
  218. package/dist/pipelineAdapters.d.ts +5 -12
  219. package/dist/pipelineAdapters.d.ts.map +1 -1
  220. package/dist/pipelineAdapters.js +175 -100
  221. package/dist/pipelineAdapters.js.map +1 -1
  222. package/dist/presets/presets.d.ts +54 -0
  223. package/dist/presets/presets.d.ts.map +1 -0
  224. package/dist/presets/presets.js +181 -0
  225. package/dist/presets/presets.js.map +1 -0
  226. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  227. package/dist/reporting/sarifReportGenerator.js +0 -2
  228. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  229. package/dist/ruleAliases.d.ts +67 -0
  230. package/dist/ruleAliases.d.ts.map +1 -0
  231. package/dist/ruleAliases.js +79 -0
  232. package/dist/ruleAliases.js.map +1 -0
  233. package/dist/styles/cssAstExtractor.d.ts +20 -5
  234. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  235. package/dist/styles/cssAstExtractor.js +247 -9
  236. package/dist/styles/cssAstExtractor.js.map +1 -1
  237. package/dist/styles/styleIndexer.js +13 -4
  238. package/dist/styles/styleIndexer.js.map +1 -1
  239. package/dist/styles/tailwindConfigLoader.d.ts +8 -6
  240. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  241. package/dist/styles/tailwindConfigLoader.js +61 -15
  242. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  243. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  244. package/dist/styles/tailwindUtilityExpander.js +13 -1
  245. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  246. package/dist/types.d.ts +201 -1
  247. package/dist/types.d.ts.map +1 -1
  248. package/dist/types.js +8 -0
  249. package/dist/types.js.map +1 -1
  250. package/package.json +7 -4
  251. package/plugin/.claude-plugin/plugin.json +1 -1
  252. package/plugin/hooks/hooks.json +1 -1
  253. package/plugin/scripts/hook-audit.sh +25 -7
  254. package/plugin/skills/code-auditor/SKILL.md +5 -5
package/dist/cli.js CHANGED
@@ -20,6 +20,8 @@ import { queryParser } from './search/QueryParser.js';
20
20
  import { CodeIndexDB } from './codeIndexDB.js';
21
21
  import { getFilesProcessed, getFactsConsumed, isVisitorStatus, isReducerStatus } from './pipeline.js';
22
22
  import { createBaselineFromFindings, saveBaseline, loadBaseline, diffBaselines } from './baseline.js';
23
+ import { computeDiffGatingDecision } from './enforcement/gate.js';
24
+ import { computeDiffGate } from './enforcement/diffGate.js';
23
25
  // Get package.json for version info
24
26
  const __filename = fileURLToPath(import.meta.url);
25
27
  const __dirname = dirname(__filename);
@@ -42,6 +44,7 @@ program
42
44
  .option('--full', 'Show full violation inventory (overrides default delta view when baseline exists)')
43
45
  .option('--include-baseline', 'Evaluate baseline-known violations in --fail-on checks')
44
46
  .option('--fail-on-regression', 'Exit code 2 when total advisory debt exceeds the baseline snapshot')
47
+ .option('--preset <id>', 'Apply a shareable preset (repeatable)', (v, prev) => prev.concat([v]), [])
45
48
  .action(async (options) => {
46
49
  console.log(chalk.blue('šŸ” Code Quality Audit Tool'));
47
50
  console.log(chalk.gray('══════════════════════════════════════════════════'));
@@ -54,10 +57,23 @@ program
54
57
  console.error(chalk.red(`Invalid --fail-on severity: "${failOnSeverity}". Must be one of: ${validSeverities.join(', ')}`));
55
58
  process.exit(1);
56
59
  }
60
+ // Validate --preset ids up front (Spec 38 R4). Unknown ids are a hard
61
+ // error, not a silent skip — same contract as print-config.
62
+ const presetIds = Array.isArray(options.preset) ? options.preset : (options.preset ? [options.preset] : []);
63
+ if (presetIds.length > 0) {
64
+ const { getPreset, PRESET_IDS } = await import('./presets/presets.js');
65
+ for (const id of presetIds) {
66
+ if (!getPreset(id)) {
67
+ console.error(chalk.red(`Error: unknown preset "${id}". Available: ${PRESET_IDS.join(', ')}`));
68
+ process.exit(1);
69
+ }
70
+ }
71
+ }
57
72
  const runner = createAuditRunner({
58
73
  projectRoot: options.path,
59
74
  configName: options.config,
60
- outputDirectory: options.output
75
+ outputDirectory: options.output,
76
+ presets: presetIds
61
77
  });
62
78
  const result = await runner.run();
63
79
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
@@ -134,6 +150,38 @@ program
134
150
  console.log(`Warnings: ${result.summary.warnings}`);
135
151
  console.log(`Suggestions: ${result.summary.suggestions}`);
136
152
  }
153
+ // Spec 32 — unparsed files are never silent. A run where any file failed
154
+ // to parse (WASM abort, read error, …) must surface the count + reasons.
155
+ const unparsedFiles = result.metadata?.unparsedFiles ?? [];
156
+ if (unparsedFiles.length > 0) {
157
+ console.error(chalk.red(`\nāš ļø ${unparsedFiles.length} file${unparsedFiles.length !== 1 ? 's' : ''} failed to parse:`));
158
+ for (const u of unparsedFiles.slice(0, 20)) {
159
+ console.error(` ${u.filePath} — ${u.reason}`);
160
+ }
161
+ if (unparsedFiles.length > 20) {
162
+ console.error(` … and ${unparsedFiles.length - 20} more`);
163
+ }
164
+ }
165
+ // Spec 36 R7 — suppressions are reported: how many exist, where, and how
166
+ // many are unnecessary (or reasonless). Both are errors, surfaced here.
167
+ const suppressions = result.metadata?.suppressions;
168
+ if (suppressions) {
169
+ console.log(chalk.gray(`\n── Suppressions ──────────────────────────────`));
170
+ console.log(` ${suppressions.total} directive${suppressions.total !== 1 ? 's' : ''}, ` +
171
+ `${suppressions.suppressed} finding${suppressions.suppressed !== 1 ? 's' : ''} suppressed`);
172
+ if (suppressions.unnecessary.length > 0) {
173
+ console.error(chalk.red(` āŒ ${suppressions.unnecessary.length} unnecessary (finding no longer fires):`));
174
+ for (const d of suppressions.unnecessary) {
175
+ console.error(` ${d.rule} @ ${d.file}:${d.line}`);
176
+ }
177
+ }
178
+ if (suppressions.reasonless.length > 0) {
179
+ console.error(chalk.red(` āŒ ${suppressions.reasonless.length} missing a required reason:`));
180
+ for (const d of suppressions.reasonless) {
181
+ console.error(` ${d.rule} @ ${d.file}:${d.line}`);
182
+ }
183
+ }
184
+ }
137
185
  // Per-analyzer activity (read from result data, not serialized summary)
138
186
  // — surfaces zero-scan failures that would otherwise be invisible.
139
187
  if (!options.json) {
@@ -154,6 +202,39 @@ program
154
202
  console.log(chalk.gray(`\n── Pipeline Stages ──────────────────────────`));
155
203
  console.log(analyzerFiles.join('\n'));
156
204
  }
205
+ // ── Coverage (Spec 27) ──────────────────────────────────────────
206
+ const coverage = result.metadata?.coverage;
207
+ if (coverage && coverage.length > 0) {
208
+ const fired = coverage.filter(c => c.state === 'fired');
209
+ const clean = coverage.filter(c => c.state === 'clean');
210
+ const unassessed = coverage.filter(c => c.state === 'unassessed');
211
+ const notApplicable = coverage.filter(c => c.state === 'notApplicable');
212
+ const firedCount = fired.reduce((s, c) => s + c.count, 0);
213
+ console.log(chalk.gray(`\n── Coverage ─────────────────────────────────`));
214
+ console.log(` ${fired.length} fired (${firedCount.toLocaleString()} violations), ` +
215
+ `${clean.length} clean, ` +
216
+ `${unassessed.length} unassessed, ` +
217
+ `${notApplicable.length} notApplicable ` +
218
+ `(${coverage.length} rules registered)`);
219
+ if (notApplicable.length > 0) {
220
+ console.log(chalk.gray(` ── Not Applicable ──`));
221
+ for (const c of notApplicable) {
222
+ console.log(` ${c.ruleId}: ${c.reason ?? 'unknown'}`);
223
+ }
224
+ }
225
+ if (clean.length > 0) {
226
+ console.log(chalk.gray(` ── Clean ──`));
227
+ for (const c of clean) {
228
+ console.log(` ${c.ruleId} (0)`);
229
+ }
230
+ }
231
+ if (unassessed.length > 0) {
232
+ console.log(chalk.gray(` ── Unassessed (zero findings, applicability unknown) ──`));
233
+ for (const c of unassessed) {
234
+ console.log(` ${c.ruleId} (0)`);
235
+ }
236
+ }
237
+ }
157
238
  }
158
239
  // Spec-20 R4: built-in profile visibility — silent behavior changes
159
240
  // are never acceptable. Notify when scripts-and-tests capped findings.
@@ -175,6 +256,7 @@ program
175
256
  const outputDir = options.output || process.cwd();
176
257
  const ext = options.format === 'sarif' ? 'sarif' : options.format;
177
258
  const reportPath = join(outputDir, `audit-report.${ext}`);
259
+ await fs.mkdir(dirname(reportPath), { recursive: true });
178
260
  await fs.writeFile(reportPath, report, 'utf-8');
179
261
  console.log(chalk.green(`\nReport written to ${reportPath}`));
180
262
  }
@@ -199,6 +281,16 @@ program
199
281
  process.exit(2);
200
282
  }
201
283
  }
284
+ // Spec 32 — a run where any file failed to parse is incomplete and must
285
+ // not exit 0. This mirrors the zero-files gate: a silent dark-analyzer
286
+ // failure at file granularity is a hard failure.
287
+ {
288
+ const unparsedFiles = result.metadata?.unparsedFiles ?? [];
289
+ if (unparsedFiles.length > 0) {
290
+ console.error(`Parse-failure: ${unparsedFiles.length} file(s) could not be parsed; analysis is incomplete.`);
291
+ process.exit(2);
292
+ }
293
+ }
202
294
  // --fail-on: evaluate new + invariant findings only (unless --include-baseline)
203
295
  if (failOnSeverity) {
204
296
  const evaluableViolations = (baseline && !options.includeBaseline)
@@ -227,8 +319,6 @@ program
227
319
  .option('--json', 'Output violations as machine-readable JSON to stdout')
228
320
  .option('-f, --format <format>', 'Report format: json, or sarif (overrides --json)')
229
321
  .option('--quiet', 'Suppress output when zero violations')
230
- .option('--fail-on <severity>', 'Exit code 2 when violations at or above this severity exist', 'critical')
231
- .option('--include-baseline', 'Evaluate baseline-known violations in --fail-on checks')
232
322
  .option('--fail-on-zero-files', 'Exit code 2 when any enabled analyzer matches zero source files', true)
233
323
  .option('--stdin', 'Read file paths from stdin (one per line)')
234
324
  .option('-p, --path <projectPath>', 'Project root path', process.cwd())
@@ -260,13 +350,6 @@ program
260
350
  const resolved = [...fileSet].map((f) => isAbsolute(f) ? f : resolve(process.cwd(), f));
261
351
  scope = resolved;
262
352
  }
263
- // Validate --fail-on severity
264
- const validSeverities = ['critical', 'warning', 'suggestion'];
265
- const failOnSeverity = options.failOn;
266
- if (!validSeverities.includes(failOnSeverity)) {
267
- console.error(chalk.red(`Invalid --fail-on severity: "${failOnSeverity}". Must be one of: ${validSeverities.join(', ')}`));
268
- process.exit(1);
269
- }
270
353
  // Create runner
271
354
  const runner = createAuditRunner({
272
355
  projectRoot: options.path,
@@ -276,11 +359,6 @@ program
276
359
  const result = await runner.run();
277
360
  // Collect all violations
278
361
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
279
- // Baseline classification (Spec 18 R4 — hook path)
280
- const baseline = result.metadata?.baseline;
281
- const knownViolations = violations.filter((v) => v.new === false);
282
- const newViolations = violations.filter((v) => v.new === true || v.new === undefined);
283
- const knownCount = knownViolations.length;
284
362
  // JSON output
285
363
  if (options.format === 'sarif') {
286
364
  const { generateSARIFReport } = await import('./reporting/sarifReportGenerator.js');
@@ -320,18 +398,13 @@ program
320
398
  process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
321
399
  }
322
400
  else if (!options.quiet || violations.length > 0) {
323
- // Console output
401
+ // Console output — Spec 36 R3: agent-facing output emits findings,
402
+ // never an aggregate total. The hook and the `changed` command are the
403
+ // agent's surface; a finding total is exactly the representation the
404
+ // consumer routes around. Findings only, each with file:line.
324
405
  console.log(chalk.blue('šŸ” Diff-Scoped Code Audit'));
325
406
  console.log(chalk.gray('══════════════════════════════════════════════════'));
326
407
  console.log(chalk.gray(`Files analyzed: ${result.metadata.filesAnalyzed}`));
327
- console.log(`\nFound ${violations.length} violations`);
328
- console.log(`Critical: ${result.summary.criticalIssues}`);
329
- console.log(`Warnings: ${result.summary.warnings}`);
330
- console.log(`Suggestions: ${result.summary.suggestions}`);
331
- // Known-finding informational line (Spec 18 R4)
332
- if (baseline && knownCount > 0) {
333
- console.log(chalk.gray(`\nā„¹ļø ${knownCount} pre-existing finding${knownCount !== 1 ? 's' : ''} in files you touched (not blocking).`));
334
- }
335
408
  if (violations.length > 0) {
336
409
  console.log(chalk.gray('\n── Violations ────────────────────────────────────'));
337
410
  for (const v of violations) {
@@ -343,19 +416,80 @@ program
343
416
  console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.message}${statusTag}`);
344
417
  }
345
418
  }
419
+ else {
420
+ console.log(chalk.green('\nāœ“ No findings.'));
421
+ }
422
+ }
423
+ // Spec 38 R2/R3 — per-rule timing + gate wall-clock, opt-in by env var.
424
+ // Emitted to stderr so it never corrupts --json stdout. Slowest rule first.
425
+ if (process.env.CODE_AUDIT_RULE_TIMING === '1') {
426
+ const ruleTiming = result.metadata?.ruleTiming;
427
+ const gateMs = result.metadata?.auditDuration;
428
+ const lines = [];
429
+ lines.push('');
430
+ lines.push(`ā± gate wall-clock: ${(gateMs ?? 0).toFixed(1)} ms (budget 300 ms)`);
431
+ lines.push('── per-rule timing (slowest first) ──');
432
+ if (ruleTiming && ruleTiming.length > 0) {
433
+ for (const t of ruleTiming) {
434
+ lines.push(` ${t.ruleId.padEnd(28)} ${t.totalMs.toFixed(2).padStart(9)} ms Ɨ${t.calls}`);
435
+ }
436
+ }
437
+ else {
438
+ lines.push(' (no gating rules recorded)');
439
+ }
440
+ process.stderr.write(lines.join('\n') + '\n');
346
441
  }
347
- // Exit code based on --fail-on (Spec 18 R3/R4)
348
- if (failOnSeverity) {
349
- const evaluableViolations = (baseline && !options.includeBaseline)
350
- ? violations.filter((v) => v.new || v.analyzer === 'invariants')
351
- : violations;
352
- const severityOrder = ['critical', 'warning', 'suggestion'];
353
- const failIndex = severityOrder.indexOf(failOnSeverity);
354
- const hasAtOrAbove = evaluableViolations.some((v) => {
355
- const vIndex = severityOrder.indexOf(v.severity);
356
- return vIndex >= 0 && vIndex <= failIndex;
357
- });
358
- if (hasAtOrAbove) {
442
+ // Binary gate (Spec 36 R2/R4/R6): exit 2 when a gating finding is present.
443
+ // The `changed` command is the edit-boundary gate, so it compares against
444
+ // the file's prior state (git HEAD) via the touched-line diff gate (R2),
445
+ // NOT against a stored baseline. Gate-excluded files and non-gating rules
446
+ // never block. Severity does NOT factor in (R4). A gating rule that cannot
447
+ // name a next action for an occurrence emits non-blocking and the gap is
448
+ // recorded (R6), surfaced below to stderr.
449
+ {
450
+ const analyzedFiles = result.metadata?.analyzedFiles;
451
+ // The `changed` command is always scoped, so the runner records the
452
+ // analyzed file list. If it is somehow absent we still derive the gate
453
+ // from the files that actually produced findings rather than falling
454
+ // back to the baseline gate — a silent soft-fail is the one outcome R1
455
+ // forbids.
456
+ const gateFiles = analyzedFiles && analyzedFiles.length > 0
457
+ ? analyzedFiles
458
+ : [...new Set(violations.map((v) => v.file).filter(Boolean))];
459
+ const diffGate = computeDiffGate(options.path, gateFiles);
460
+ const { blocking, resolutionGaps } = computeDiffGatingDecision(violations, diffGate);
461
+ if (resolutionGaps.length > 0) {
462
+ // Spec 36 R6 — a gating rule that could not name an action is a defect
463
+ // in the rule. Record it loudly so it cannot route around as a count.
464
+ const lines = [
465
+ 'āš ļø resolution gap — gating rule produced no next action for these occurrences (Spec 36 R6):',
466
+ ...resolutionGaps.map((g) => ` - ${g.rule} @ ${g.file}${g.line ? `:${g.line}` : ''}`),
467
+ ];
468
+ process.stderr.write(lines.join('\n') + '\n');
469
+ }
470
+ if (blocking.length > 0) {
471
+ process.exit(2);
472
+ }
473
+ // Spec 36 R7 — an unnecessary or reasonless suppression is itself an
474
+ // error. It is reported loudly and fails the write, exactly like an
475
+ // unused `@ts-expect-error`. A suppression that outlives its finding is
476
+ // a baseline entry with better branding unless it errors here.
477
+ const suppressions = result.metadata?.suppressions;
478
+ if (suppressions && (suppressions.unnecessary.length > 0 || suppressions.reasonless.length > 0)) {
479
+ const lines = [];
480
+ if (suppressions.reasonless.length > 0) {
481
+ lines.push('āŒ suppression missing a required reason (Spec 36 R7):');
482
+ for (const d of suppressions.reasonless) {
483
+ lines.push(` - ${d.rule} @ ${d.file}:${d.line}`);
484
+ }
485
+ }
486
+ if (suppressions.unnecessary.length > 0) {
487
+ lines.push('āŒ unnecessary suppression — the finding no longer fires (Spec 36 R7):');
488
+ for (const d of suppressions.unnecessary) {
489
+ lines.push(` - ${d.rule} @ ${d.file}:${d.line}`);
490
+ }
491
+ }
492
+ process.stderr.write(lines.join('\n') + '\n');
359
493
  process.exit(2);
360
494
  }
361
495
  }
@@ -786,7 +920,7 @@ configCmd
786
920
  file: options.file,
787
921
  resolvedFilePath: filePath,
788
922
  matchedProfileNames: resolved.matchedProfileNames,
789
- severityCap: resolved.severityCap || null,
923
+ excludeFromGate: resolved.excludeFromGate || null,
790
924
  overrides: resolved.overrides,
791
925
  }, null, 2) + '\n');
792
926
  }
@@ -801,8 +935,8 @@ configCmd
801
935
  const builtinTag = profile?.builtin !== false && profiles.some(p => p.name === name) ? '' : '';
802
936
  console.log(` ${chalk.cyan(name)}${builtinTag ? chalk.gray(' (built-in)') : ''}`);
803
937
  }
804
- if (resolved.severityCap) {
805
- console.log(chalk.yellow(` severityCap: ${resolved.severityCap}`));
938
+ if (resolved.excludeFromGate) {
939
+ console.log(chalk.yellow(` excludeFromGate: true`));
806
940
  }
807
941
  if (Object.keys(resolved.overrides).length > 0) {
808
942
  console.log(chalk.gray(' merged overrides:'));
@@ -2084,6 +2218,180 @@ coverageCmd
2084
2218
  process.exit(1);
2085
2219
  }
2086
2220
  });
2221
+ // print-config command — effective config with source attribution (Spec 38 R1)
2222
+ program
2223
+ .command('print-config <file>')
2224
+ .description('Print the effective config for a file, naming the source of every value')
2225
+ .option('-p, --path <path>', 'Project path', process.cwd())
2226
+ .option('--json', 'Output as JSON')
2227
+ .option('--preset <id>', 'Apply a shareable preset (repeatable)', (v, prev) => prev.concat([v]), [])
2228
+ .action(async (file, options) => {
2229
+ try {
2230
+ const pathModule = await import('path');
2231
+ const fs = await import('fs/promises');
2232
+ const { loadConfig } = await import('./config/configLoader.js');
2233
+ const { getDefaultConfig, DEFAULT_CODE_INDEX_CONFIG } = await import('./config/defaults.js');
2234
+ const { computeEffectiveConfig, computeTopLevelConfig, } = await import('./config/effectiveConfig.js');
2235
+ const { getPreset, PRESET_IDS } = await import('./presets/presets.js');
2236
+ const projectRoot = pathModule.resolve(options.path);
2237
+ const filePath = pathModule.resolve(file);
2238
+ const configPath = pathModule.join(projectRoot, '.codeauditor.json');
2239
+ // Pristine defaults, normalized against projectRoot the same way loadConfig
2240
+ // normalizes the real config, so path keys compare cleanly instead of
2241
+ // reading as "user-set" because one side is absolute and the other is not.
2242
+ const pristine = { ...getDefaultConfig(), codeIndex: { ...DEFAULT_CODE_INDEX_CONFIG } };
2243
+ if (pristine.outputDirectory) {
2244
+ pristine.outputDirectory = pathModule.resolve(projectRoot, pristine.outputDirectory);
2245
+ }
2246
+ pristine.includePaths = (pristine.includePaths ?? []).map((p) => pathModule.isAbsolute(p) ? p : pathModule.resolve(projectRoot, p));
2247
+ pristine.excludePaths = (pristine.excludePaths ?? []).map((p) => pathModule.isAbsolute(p) ? p : pathModule.resolve(projectRoot, p));
2248
+ // Load the project config (defaults when no config file exists).
2249
+ let config;
2250
+ try {
2251
+ await fs.access(configPath);
2252
+ config = await loadConfig({ configPath });
2253
+ }
2254
+ catch {
2255
+ config = pristine;
2256
+ }
2257
+ const analyzerConfigs = (config.analyzerConfigs ?? {});
2258
+ const pathProfiles = (config.pathProfiles ?? []);
2259
+ const enabledAnalyzers = (config.enabledAnalyzers ?? []);
2260
+ // Resolve --preset ids to ordered preset objects (Spec 38 R4). Unknown
2261
+ // ids are a hard error, not a silent skip.
2262
+ const presetIds = Array.isArray(options.preset) ? options.preset : (options.preset ? [options.preset] : []);
2263
+ for (const id of presetIds) {
2264
+ if (!getPreset(id)) {
2265
+ console.error(chalk.red(`Error: unknown preset "${id}". Available: ${PRESET_IDS.join(', ')}`));
2266
+ process.exit(1);
2267
+ }
2268
+ }
2269
+ const presets = presetIds.map((id) => getPreset(id));
2270
+ const effective = computeEffectiveConfig({
2271
+ filePath,
2272
+ projectRoot,
2273
+ analyzerConfigs,
2274
+ pathProfiles,
2275
+ enabledAnalyzers,
2276
+ presets,
2277
+ });
2278
+ const topLevel = computeTopLevelConfig(pristine, config);
2279
+ if (options.json) {
2280
+ process.stdout.write(JSON.stringify({
2281
+ file: filePath,
2282
+ projectRoot,
2283
+ configPath,
2284
+ presets: presetIds,
2285
+ matchedProfiles: effective.matchedProfiles,
2286
+ excludeFromGate: effective.excludeFromGate ?? null,
2287
+ topLevel,
2288
+ analyzers: effective.analyzers,
2289
+ }, null, 2) + '\n');
2290
+ return;
2291
+ }
2292
+ console.log(chalk.blue('šŸ”§ Effective Config'));
2293
+ console.log(chalk.gray('════════════════════════════════════════════════════'));
2294
+ console.log(`${chalk.bold('file:')} ${effective.relativePath}`);
2295
+ console.log(`${chalk.bold('project root:')} ${projectRoot}`);
2296
+ console.log(`${chalk.bold('presets:')} ${presetIds.length > 0 ? presetIds.join(', ') : chalk.gray('(none)')}`);
2297
+ console.log(`${chalk.bold('profiles:')} ${effective.matchedProfiles.length > 0 ? effective.matchedProfiles.join(', ') : chalk.gray('(none)')}`);
2298
+ if (effective.excludeFromGate) {
2299
+ console.log(`${chalk.bold('excludeFromGate:')} ${chalk.yellow('true')}`);
2300
+ }
2301
+ // Top-level keys
2302
+ console.log(chalk.bold('\n── Top-level ──'));
2303
+ for (const key of topLevel) {
2304
+ const flag = key.differsFromDefault ? chalk.yellow(' [differs from default]') : '';
2305
+ console.log(`${key.key.padEnd(24)} ${chalk.dim(JSON.stringify(key.value))} ${chalk.cyan(key.source)}${flag}`);
2306
+ }
2307
+ // Per-analyzer keys
2308
+ for (const analyzer of effective.analyzers) {
2309
+ console.log(chalk.bold(`\n── ${analyzer.namespace} ──`));
2310
+ for (const key of analyzer.keys) {
2311
+ const flag = key.differsFromDefault
2312
+ ? chalk.yellow(` [differs from default: ${JSON.stringify(key.defaultValue)}]`)
2313
+ : '';
2314
+ console.log(`${key.key.padEnd(32)} ${chalk.dim(JSON.stringify(key.value))} ${chalk.cyan(key.source)}${flag}`);
2315
+ }
2316
+ }
2317
+ }
2318
+ catch (error) {
2319
+ console.error(chalk.red('Error:'), error);
2320
+ process.exit(1);
2321
+ }
2322
+ });
2323
+ // Outstanding command (Spec 36 R8) — "what is outstanding" is re-derived from
2324
+ // the codebase on every invocation, never read from a stored list. The answer a
2325
+ // compacted agent gets is identical to an uncompacted one because the only input
2326
+ // is the code itself: run the audit, list the findings. No progress file, no
2327
+ // tracked count, no state that can drift from the code.
2328
+ program
2329
+ .command('outstanding')
2330
+ .description('List outstanding findings, re-derived from the codebase (never a stored list)')
2331
+ .option('-p, --path <path>', 'Path to audit', process.cwd())
2332
+ .option('--json', 'Output findings as machine-readable JSON')
2333
+ .action(async (options) => {
2334
+ try {
2335
+ await initParsers();
2336
+ const runner = createAuditRunner({ projectRoot: options.path });
2337
+ const result = await runner.run();
2338
+ const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
2339
+ // Deterministic order — same answer every run, independent of analyzer
2340
+ // registration order or any prior agent state. Sort by file, then line,
2341
+ // then rule. This is what makes a compacted and uncompacted agent agree.
2342
+ const sorted = [...violations].sort((a, b) => {
2343
+ const fa = a.file ?? '';
2344
+ const fb = b.file ?? '';
2345
+ if (fa !== fb)
2346
+ return fa < fb ? -1 : 1;
2347
+ const la = a.line ?? 0;
2348
+ const lb = b.line ?? 0;
2349
+ if (la !== lb)
2350
+ return la - lb;
2351
+ const ra = a.rule ?? '';
2352
+ const rb = b.rule ?? '';
2353
+ if (ra !== rb)
2354
+ return ra < rb ? -1 : 1;
2355
+ return 0;
2356
+ });
2357
+ if (options.json) {
2358
+ const projectDir = resolve(options.path || process.cwd());
2359
+ const out = sorted.map((v) => {
2360
+ let filePath = v.file || '';
2361
+ if (filePath.startsWith('/')) {
2362
+ const rel = relative(projectDir, filePath);
2363
+ if (!rel.startsWith('..') && !isAbsolute(rel))
2364
+ filePath = rel;
2365
+ }
2366
+ return {
2367
+ rule: v.rule || v.type || '',
2368
+ analyzer: v.analyzer || '',
2369
+ severity: v.severity,
2370
+ file: filePath,
2371
+ line: v.line ?? v.start?.line,
2372
+ message: v.message,
2373
+ resolution: v.resolution ?? null,
2374
+ suppressed: v.suppressed === true,
2375
+ ...(v.suppressionReason && { suppressionReason: v.suppressionReason }),
2376
+ };
2377
+ });
2378
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
2379
+ return;
2380
+ }
2381
+ // Spec 36 R3 — findings, never a total. Each line is a file:line finding;
2382
+ // there is no count anywhere in this output to route around.
2383
+ for (const v of sorted) {
2384
+ const icon = v.severity === 'critical' ? 'šŸ”“' :
2385
+ v.severity === 'warning' ? '🟔' : 'šŸ”µ';
2386
+ const suppressed = v.suppressed ? chalk.dim(' [suppressed]') : '';
2387
+ console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.rule || v.severity}] ${v.message}${suppressed}`);
2388
+ }
2389
+ }
2390
+ catch (error) {
2391
+ console.error(chalk.red('Error:'), error);
2392
+ process.exit(1);
2393
+ }
2394
+ });
2087
2395
  /**
2088
2396
  * Render a simple ASCII bar chart.
2089
2397
  */
@@ -2247,7 +2555,7 @@ async function generateConfigurations(options) {
2247
2555
  console.log(chalk.blue('\nNext steps:'));
2248
2556
  console.log(chalk.gray(' 1. Edit .codeauditor.json to match your codebase conventions'));
2249
2557
  console.log(chalk.gray(' 2. Run ') + chalk.cyan('code-audit') + chalk.gray(' to enforce your rules'));
2250
- console.log(chalk.gray(' 3. Use ') + chalk.cyan('code-audit changed --fail-on critical') + chalk.gray(' in your agent hook'));
2558
+ console.log(chalk.gray(' 3. Use ') + chalk.cyan('code-audit changed') + chalk.gray(' in your agent hook (gating rules block on new findings)'));
2251
2559
  }
2252
2560
  /**
2253
2561
  * Interactive rule builder — walks the user through adding rules one at a time.