code-auditor-mcp 3.4.12 → 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 (245) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/dist/analyzers/__verify_self_probe.d.ts +2 -0
  3. package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
  4. package/dist/analyzers/__verify_self_probe.js +5 -0
  5. package/dist/analyzers/__verify_self_probe.js.map +1 -0
  6. package/dist/analyzers/analyzerUtils.d.ts +34 -1
  7. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  8. package/dist/analyzers/analyzerUtils.js +61 -22
  9. package/dist/analyzers/analyzerUtils.js.map +1 -1
  10. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
  11. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
  13. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  14. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
  15. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  16. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
  17. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  18. package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
  19. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  20. package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
  21. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  22. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
  23. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  24. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
  25. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  26. package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
  27. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  28. package/dist/analyzers/documentationAnalyzer.js +259 -198
  29. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  30. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  31. package/dist/analyzers/invariantsAnalyzer.js +57 -50
  32. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  33. package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
  34. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
  35. package/dist/analyzers/orm/adapterRegistry.js +10 -0
  36. package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
  37. package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
  38. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
  39. package/dist/analyzers/orm/drizzleAdapter.js +42 -44
  40. package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
  41. package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
  42. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
  43. package/dist/analyzers/orm/prismaAdapter.js +30 -0
  44. package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
  45. package/dist/analyzers/provenance.d.ts +62 -2
  46. package/dist/analyzers/provenance.d.ts.map +1 -1
  47. package/dist/analyzers/provenance.js +443 -349
  48. package/dist/analyzers/provenance.js.map +1 -1
  49. package/dist/analyzers/reactAnalyzer.d.ts +11 -0
  50. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  51. package/dist/analyzers/reactAnalyzer.js +188 -125
  52. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  53. package/dist/analyzers/ruleRegistry.d.ts +110 -1
  54. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  55. package/dist/analyzers/ruleRegistry.js +1714 -124
  56. package/dist/analyzers/ruleRegistry.js.map +1 -1
  57. package/dist/analyzers/ruleTiming.d.ts +54 -0
  58. package/dist/analyzers/ruleTiming.d.ts.map +1 -0
  59. package/dist/analyzers/ruleTiming.js +71 -0
  60. package/dist/analyzers/ruleTiming.js.map +1 -0
  61. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
  62. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  63. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
  64. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  65. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
  66. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  67. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
  68. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  69. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +17 -202
  70. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  71. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1073 -1121
  72. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  73. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
  74. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  75. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
  76. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  77. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
  78. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  79. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
  80. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  81. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -214
  82. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  83. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1521
  84. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  85. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
  86. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  87. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -480
  88. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  89. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
  90. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
  91. package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
  92. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
  93. package/dist/analyzers/universal/schema/config.d.ts +31 -0
  94. package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
  95. package/dist/analyzers/universal/schema/config.js +57 -0
  96. package/dist/analyzers/universal/schema/config.js.map +1 -0
  97. package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
  98. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
  99. package/dist/analyzers/universal/schema/discovery.js +616 -0
  100. package/dist/analyzers/universal/schema/discovery.js.map +1 -0
  101. package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
  102. package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
  103. package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
  104. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
  105. package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
  106. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
  107. package/dist/analyzers/universal/schema/migrations.js +238 -0
  108. package/dist/analyzers/universal/schema/migrations.js.map +1 -0
  109. package/dist/analyzers/universal/schema/types.d.ts +131 -0
  110. package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
  111. package/dist/analyzers/universal/schema/types.js +10 -0
  112. package/dist/analyzers/universal/schema/types.js.map +1 -0
  113. package/dist/analyzers/universal/schema/violations.d.ts +44 -0
  114. package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
  115. package/dist/analyzers/universal/schema/violations.js +54 -0
  116. package/dist/analyzers/universal/schema/violations.js.map +1 -0
  117. package/dist/auditRunner.d.ts.map +1 -1
  118. package/dist/auditRunner.js +89 -13
  119. package/dist/auditRunner.js.map +1 -1
  120. package/dist/cli.js +315 -40
  121. package/dist/cli.js.map +1 -1
  122. package/dist/config/configLoader.d.ts.map +1 -1
  123. package/dist/config/configLoader.js +9 -7
  124. package/dist/config/configLoader.js.map +1 -1
  125. package/dist/config/defaults.d.ts +12 -3
  126. package/dist/config/defaults.d.ts.map +1 -1
  127. package/dist/config/defaults.js +15 -5
  128. package/dist/config/defaults.js.map +1 -1
  129. package/dist/config/effectiveConfig.d.ts +75 -0
  130. package/dist/config/effectiveConfig.d.ts.map +1 -0
  131. package/dist/config/effectiveConfig.js +213 -0
  132. package/dist/config/effectiveConfig.js.map +1 -0
  133. package/dist/config/pathProfiles.d.ts +7 -4
  134. package/dist/config/pathProfiles.d.ts.map +1 -1
  135. package/dist/config/pathProfiles.js +8 -10
  136. package/dist/config/pathProfiles.js.map +1 -1
  137. package/dist/config/thresholdRationales.d.ts +45 -0
  138. package/dist/config/thresholdRationales.d.ts.map +1 -0
  139. package/dist/config/thresholdRationales.js +67 -0
  140. package/dist/config/thresholdRationales.js.map +1 -0
  141. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  142. package/dist/conventions/conventionMiner.js +6 -2
  143. package/dist/conventions/conventionMiner.js.map +1 -1
  144. package/dist/enforcement/diffGate.d.ts +55 -0
  145. package/dist/enforcement/diffGate.d.ts.map +1 -0
  146. package/dist/enforcement/diffGate.js +136 -0
  147. package/dist/enforcement/diffGate.js.map +1 -0
  148. package/dist/enforcement/gate.d.ts +36 -0
  149. package/dist/enforcement/gate.d.ts.map +1 -0
  150. package/dist/enforcement/gate.js +72 -0
  151. package/dist/enforcement/gate.js.map +1 -0
  152. package/dist/enforcement/suppressions.d.ts +41 -0
  153. package/dist/enforcement/suppressions.d.ts.map +1 -0
  154. package/dist/enforcement/suppressions.js +170 -0
  155. package/dist/enforcement/suppressions.js.map +1 -0
  156. package/dist/fingerprint.d.ts.map +1 -1
  157. package/dist/fingerprint.js +4 -1
  158. package/dist/fingerprint.js.map +1 -1
  159. package/dist/invariants/ruleEngine.d.ts +3 -4
  160. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  161. package/dist/invariants/ruleEngine.js +2 -2
  162. package/dist/invariants/ruleEngine.js.map +1 -1
  163. package/dist/languages/LanguageOrchestrator.d.ts +14 -0
  164. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  165. package/dist/languages/LanguageOrchestrator.js +93 -55
  166. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  167. package/dist/languages/LanguageRegistry.d.ts +8 -0
  168. package/dist/languages/LanguageRegistry.d.ts.map +1 -1
  169. package/dist/languages/LanguageRegistry.js +8 -0
  170. package/dist/languages/LanguageRegistry.js.map +1 -1
  171. package/dist/languages/RuntimeManager.d.ts +82 -60
  172. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  173. package/dist/languages/RuntimeManager.js +404 -349
  174. package/dist/languages/RuntimeManager.js.map +1 -1
  175. package/dist/languages/UniversalAnalyzer.d.ts +51 -6
  176. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  177. package/dist/languages/UniversalAnalyzer.js +127 -88
  178. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  179. package/dist/languages/adapterBridge.d.ts +36 -0
  180. package/dist/languages/adapterBridge.d.ts.map +1 -1
  181. package/dist/languages/adapterBridge.js +89 -35
  182. package/dist/languages/adapterBridge.js.map +1 -1
  183. package/dist/languages/go/GoAdapter.d.ts +66 -23
  184. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  185. package/dist/languages/go/GoAdapter.js +553 -474
  186. package/dist/languages/go/GoAdapter.js.map +1 -1
  187. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +33 -4
  188. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
  189. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +101 -66
  190. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  191. package/dist/languages/tree-sitter/converter.d.ts +18 -0
  192. package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
  193. package/dist/languages/tree-sitter/converter.js +18 -0
  194. package/dist/languages/tree-sitter/converter.js.map +1 -1
  195. package/dist/languages/tree-sitter/parser.d.ts +39 -2
  196. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  197. package/dist/languages/tree-sitter/parser.js +93 -12
  198. package/dist/languages/tree-sitter/parser.js.map +1 -1
  199. package/dist/languages/types.d.ts +67 -2
  200. package/dist/languages/types.d.ts.map +1 -1
  201. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -72
  202. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  203. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1419 -923
  204. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  205. package/dist/mcp-tools-shared.d.ts.map +1 -1
  206. package/dist/mcp-tools-shared.js.map +1 -1
  207. package/dist/pipeline.d.ts +13 -5
  208. package/dist/pipeline.d.ts.map +1 -1
  209. package/dist/pipeline.js +381 -200
  210. package/dist/pipeline.js.map +1 -1
  211. package/dist/pipelineAdapters.d.ts +3 -8
  212. package/dist/pipelineAdapters.d.ts.map +1 -1
  213. package/dist/pipelineAdapters.js +149 -94
  214. package/dist/pipelineAdapters.js.map +1 -1
  215. package/dist/presets/presets.d.ts +54 -0
  216. package/dist/presets/presets.d.ts.map +1 -0
  217. package/dist/presets/presets.js +181 -0
  218. package/dist/presets/presets.js.map +1 -0
  219. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  220. package/dist/reporting/sarifReportGenerator.js +0 -2
  221. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  222. package/dist/ruleAliases.d.ts +67 -0
  223. package/dist/ruleAliases.d.ts.map +1 -0
  224. package/dist/ruleAliases.js +79 -0
  225. package/dist/ruleAliases.js.map +1 -0
  226. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  227. package/dist/styles/cssAstExtractor.js +78 -18
  228. package/dist/styles/cssAstExtractor.js.map +1 -1
  229. package/dist/styles/styleIndexer.js +10 -1
  230. package/dist/styles/styleIndexer.js.map +1 -1
  231. package/dist/styles/tailwindConfigLoader.d.ts +8 -6
  232. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  233. package/dist/styles/tailwindConfigLoader.js +61 -15
  234. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  235. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  236. package/dist/styles/tailwindUtilityExpander.js +13 -1
  237. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  238. package/dist/types.d.ts +181 -1
  239. package/dist/types.d.ts.map +1 -1
  240. package/dist/types.js +8 -0
  241. package/dist/types.js.map +1 -1
  242. package/package.json +5 -3
  243. package/plugin/hooks/hooks.json +1 -1
  244. package/plugin/scripts/hook-audit.sh +25 -7
  245. 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) {
@@ -208,6 +256,7 @@ program
208
256
  const outputDir = options.output || process.cwd();
209
257
  const ext = options.format === 'sarif' ? 'sarif' : options.format;
210
258
  const reportPath = join(outputDir, `audit-report.${ext}`);
259
+ await fs.mkdir(dirname(reportPath), { recursive: true });
211
260
  await fs.writeFile(reportPath, report, 'utf-8');
212
261
  console.log(chalk.green(`\nReport written to ${reportPath}`));
213
262
  }
@@ -232,6 +281,16 @@ program
232
281
  process.exit(2);
233
282
  }
234
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
+ }
235
294
  // --fail-on: evaluate new + invariant findings only (unless --include-baseline)
236
295
  if (failOnSeverity) {
237
296
  const evaluableViolations = (baseline && !options.includeBaseline)
@@ -260,8 +319,6 @@ program
260
319
  .option('--json', 'Output violations as machine-readable JSON to stdout')
261
320
  .option('-f, --format <format>', 'Report format: json, or sarif (overrides --json)')
262
321
  .option('--quiet', 'Suppress output when zero violations')
263
- .option('--fail-on <severity>', 'Exit code 2 when violations at or above this severity exist', 'critical')
264
- .option('--include-baseline', 'Evaluate baseline-known violations in --fail-on checks')
265
322
  .option('--fail-on-zero-files', 'Exit code 2 when any enabled analyzer matches zero source files', true)
266
323
  .option('--stdin', 'Read file paths from stdin (one per line)')
267
324
  .option('-p, --path <projectPath>', 'Project root path', process.cwd())
@@ -293,13 +350,6 @@ program
293
350
  const resolved = [...fileSet].map((f) => isAbsolute(f) ? f : resolve(process.cwd(), f));
294
351
  scope = resolved;
295
352
  }
296
- // Validate --fail-on severity
297
- const validSeverities = ['critical', 'warning', 'suggestion'];
298
- const failOnSeverity = options.failOn;
299
- if (!validSeverities.includes(failOnSeverity)) {
300
- console.error(chalk.red(`Invalid --fail-on severity: "${failOnSeverity}". Must be one of: ${validSeverities.join(', ')}`));
301
- process.exit(1);
302
- }
303
353
  // Create runner
304
354
  const runner = createAuditRunner({
305
355
  projectRoot: options.path,
@@ -309,11 +359,6 @@ program
309
359
  const result = await runner.run();
310
360
  // Collect all violations
311
361
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
312
- // Baseline classification (Spec 18 R4 — hook path)
313
- const baseline = result.metadata?.baseline;
314
- const knownViolations = violations.filter((v) => v.new === false);
315
- const newViolations = violations.filter((v) => v.new === true || v.new === undefined);
316
- const knownCount = knownViolations.length;
317
362
  // JSON output
318
363
  if (options.format === 'sarif') {
319
364
  const { generateSARIFReport } = await import('./reporting/sarifReportGenerator.js');
@@ -353,18 +398,13 @@ program
353
398
  process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
354
399
  }
355
400
  else if (!options.quiet || violations.length > 0) {
356
- // 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.
357
405
  console.log(chalk.blue('šŸ” Diff-Scoped Code Audit'));
358
406
  console.log(chalk.gray('══════════════════════════════════════════════════'));
359
407
  console.log(chalk.gray(`Files analyzed: ${result.metadata.filesAnalyzed}`));
360
- console.log(`\nFound ${violations.length} violations`);
361
- console.log(`Critical: ${result.summary.criticalIssues}`);
362
- console.log(`Warnings: ${result.summary.warnings}`);
363
- console.log(`Suggestions: ${result.summary.suggestions}`);
364
- // Known-finding informational line (Spec 18 R4)
365
- if (baseline && knownCount > 0) {
366
- console.log(chalk.gray(`\nā„¹ļø ${knownCount} pre-existing finding${knownCount !== 1 ? 's' : ''} in files you touched (not blocking).`));
367
- }
368
408
  if (violations.length > 0) {
369
409
  console.log(chalk.gray('\n── Violations ────────────────────────────────────'));
370
410
  for (const v of violations) {
@@ -376,19 +416,80 @@ program
376
416
  console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.message}${statusTag}`);
377
417
  }
378
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');
379
441
  }
380
- // Exit code based on --fail-on (Spec 18 R3/R4)
381
- if (failOnSeverity) {
382
- const evaluableViolations = (baseline && !options.includeBaseline)
383
- ? violations.filter((v) => v.new || v.analyzer === 'invariants')
384
- : violations;
385
- const severityOrder = ['critical', 'warning', 'suggestion'];
386
- const failIndex = severityOrder.indexOf(failOnSeverity);
387
- const hasAtOrAbove = evaluableViolations.some((v) => {
388
- const vIndex = severityOrder.indexOf(v.severity);
389
- return vIndex >= 0 && vIndex <= failIndex;
390
- });
391
- 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');
392
493
  process.exit(2);
393
494
  }
394
495
  }
@@ -819,7 +920,7 @@ configCmd
819
920
  file: options.file,
820
921
  resolvedFilePath: filePath,
821
922
  matchedProfileNames: resolved.matchedProfileNames,
822
- severityCap: resolved.severityCap || null,
923
+ excludeFromGate: resolved.excludeFromGate || null,
823
924
  overrides: resolved.overrides,
824
925
  }, null, 2) + '\n');
825
926
  }
@@ -834,8 +935,8 @@ configCmd
834
935
  const builtinTag = profile?.builtin !== false && profiles.some(p => p.name === name) ? '' : '';
835
936
  console.log(` ${chalk.cyan(name)}${builtinTag ? chalk.gray(' (built-in)') : ''}`);
836
937
  }
837
- if (resolved.severityCap) {
838
- console.log(chalk.yellow(` severityCap: ${resolved.severityCap}`));
938
+ if (resolved.excludeFromGate) {
939
+ console.log(chalk.yellow(` excludeFromGate: true`));
839
940
  }
840
941
  if (Object.keys(resolved.overrides).length > 0) {
841
942
  console.log(chalk.gray(' merged overrides:'));
@@ -2117,6 +2218,180 @@ coverageCmd
2117
2218
  process.exit(1);
2118
2219
  }
2119
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
+ });
2120
2395
  /**
2121
2396
  * Render a simple ASCII bar chart.
2122
2397
  */
@@ -2280,7 +2555,7 @@ async function generateConfigurations(options) {
2280
2555
  console.log(chalk.blue('\nNext steps:'));
2281
2556
  console.log(chalk.gray(' 1. Edit .codeauditor.json to match your codebase conventions'));
2282
2557
  console.log(chalk.gray(' 2. Run ') + chalk.cyan('code-audit') + chalk.gray(' to enforce your rules'));
2283
- 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)'));
2284
2559
  }
2285
2560
  /**
2286
2561
  * Interactive rule builder — walks the user through adding rules one at a time.