code-auditor-mcp 3.4.18 → 3.6.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 (193) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +99 -0
  3. package/README.md +5 -5
  4. package/dist/analyzers/applicability.d.ts +19 -5
  5. package/dist/analyzers/applicability.d.ts.map +1 -1
  6. package/dist/analyzers/applicability.js +36 -24
  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 +35 -12
  36. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  37. package/dist/analyzers/ruleRegistry.js +66 -108
  38. package/dist/analyzers/ruleRegistry.js.map +1 -1
  39. package/dist/analyzers/ruleTiming.d.ts +2 -2
  40. package/dist/analyzers/ruleTiming.js +2 -2
  41. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  42. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +60 -24
  43. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  44. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  45. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +5 -2
  46. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  47. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  48. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +82 -71
  49. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  50. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +2 -1
  51. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  52. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +23 -12
  53. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  54. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +11 -1
  55. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  56. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +60 -22
  57. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  58. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  59. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +5 -2
  60. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  61. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -3
  62. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  63. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +258 -87
  64. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  65. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
  66. package/dist/analyzers/universal/schema/codeAnalysis.js +7 -1
  67. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  68. package/dist/analyzers/universal/schema/discovery.d.ts +29 -1
  69. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -1
  70. package/dist/analyzers/universal/schema/discovery.js +76 -21
  71. package/dist/analyzers/universal/schema/discovery.js.map +1 -1
  72. package/dist/analyzers/universal/schema/jsonSchema.js +7 -11
  73. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
  74. package/dist/auditRouter.d.ts +41 -0
  75. package/dist/auditRouter.d.ts.map +1 -0
  76. package/dist/auditRouter.js +185 -0
  77. package/dist/auditRouter.js.map +1 -0
  78. package/dist/auditRunner.d.ts.map +1 -1
  79. package/dist/auditRunner.js +93 -84
  80. package/dist/auditRunner.js.map +1 -1
  81. package/dist/cli.js +306 -95
  82. package/dist/cli.js.map +1 -1
  83. package/dist/codeIndexDB.d.ts.map +1 -1
  84. package/dist/codeIndexDB.js +41 -7
  85. package/dist/codeIndexDB.js.map +1 -1
  86. package/dist/componentScanner.d.ts.map +1 -1
  87. package/dist/componentScanner.js +2 -1
  88. package/dist/componentScanner.js.map +1 -1
  89. package/dist/config/configLoader.d.ts +9 -0
  90. package/dist/config/configLoader.d.ts.map +1 -1
  91. package/dist/config/configLoader.js +45 -2
  92. package/dist/config/configLoader.js.map +1 -1
  93. package/dist/config/defaults.d.ts +8 -1
  94. package/dist/config/defaults.d.ts.map +1 -1
  95. package/dist/config/defaults.js +19 -3
  96. package/dist/config/defaults.js.map +1 -1
  97. package/dist/dataPaths.js +18 -1
  98. package/dist/dataPaths.js.map +1 -1
  99. package/dist/enforcement/gate.d.ts +14 -19
  100. package/dist/enforcement/gate.d.ts.map +1 -1
  101. package/dist/enforcement/gate.js +27 -45
  102. package/dist/enforcement/gate.js.map +1 -1
  103. package/dist/functionScanner.d.ts.map +1 -1
  104. package/dist/functionScanner.js +1 -17
  105. package/dist/functionScanner.js.map +1 -1
  106. package/dist/invariants/ruleEngine.d.ts +8 -0
  107. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  108. package/dist/invariants/ruleEngine.js +35 -16
  109. package/dist/invariants/ruleEngine.js.map +1 -1
  110. package/dist/languages/LanguageOrchestrator.d.ts +9 -10
  111. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  112. package/dist/languages/LanguageOrchestrator.js +135 -101
  113. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  114. package/dist/languages/RuntimeManager.d.ts +28 -1
  115. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  116. package/dist/languages/RuntimeManager.js +158 -47
  117. package/dist/languages/RuntimeManager.js.map +1 -1
  118. package/dist/languages/go/GoAdapter.d.ts +2 -0
  119. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  120. package/dist/languages/go/GoAdapter.js +15 -22
  121. package/dist/languages/go/GoAdapter.js.map +1 -1
  122. package/dist/languages/go/analyzer +0 -0
  123. package/dist/languages/go/analyzer-src/analyzer.go +510 -0
  124. package/dist/languages/go/analyzer-src/go.mod +3 -0
  125. package/dist/languages/go/analyzer-src/indexer.go +268 -0
  126. package/dist/languages/go/analyzer-src/parser.go +461 -0
  127. package/dist/languages/go/analyzer-src/solid.go +395 -0
  128. package/dist/languages/go/analyzer-src/testconventions.go +25 -0
  129. package/dist/languages/go/analyzer-src/types.go +148 -0
  130. package/dist/languages/go/go.mod +7 -0
  131. package/dist/languages/go/main.go +203 -0
  132. package/dist/languages/testConventions.d.ts +35 -0
  133. package/dist/languages/testConventions.d.ts.map +1 -0
  134. package/dist/languages/testConventions.js +51 -0
  135. package/dist/languages/testConventions.js.map +1 -0
  136. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
  137. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  138. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +25 -18
  139. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  140. package/dist/mcp-tools/whitelistTools.d.ts +1 -1
  141. package/dist/mcp-tools-shared.d.ts +0 -3
  142. package/dist/mcp-tools-shared.d.ts.map +1 -1
  143. package/dist/mcp-tools-shared.js +8 -117
  144. package/dist/mcp-tools-shared.js.map +1 -1
  145. package/dist/mcp.js +3 -1
  146. package/dist/mcp.js.map +1 -1
  147. package/dist/mcpAuditJobs.d.ts.map +1 -1
  148. package/dist/mcpAuditJobs.js +13 -0
  149. package/dist/mcpAuditJobs.js.map +1 -1
  150. package/dist/nextFile.d.ts +21 -0
  151. package/dist/nextFile.d.ts.map +1 -0
  152. package/dist/nextFile.js +51 -0
  153. package/dist/nextFile.js.map +1 -0
  154. package/dist/nextFileIncremental.d.ts +141 -0
  155. package/dist/nextFileIncremental.d.ts.map +1 -0
  156. package/dist/nextFileIncremental.js +314 -0
  157. package/dist/nextFileIncremental.js.map +1 -0
  158. package/dist/pipeline.d.ts.map +1 -1
  159. package/dist/pipeline.js +43 -3
  160. package/dist/pipeline.js.map +1 -1
  161. package/dist/pipelineAdapters.d.ts +17 -0
  162. package/dist/pipelineAdapters.d.ts.map +1 -1
  163. package/dist/pipelineAdapters.js +791 -40
  164. package/dist/pipelineAdapters.js.map +1 -1
  165. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  166. package/dist/reporting/jsonReportGenerator.js +11 -30
  167. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  168. package/dist/styles/styleExtractor.js +2 -2
  169. package/dist/styles/styleExtractor.js.map +1 -1
  170. package/dist/styles/styleIndexer.d.ts +7 -0
  171. package/dist/styles/styleIndexer.d.ts.map +1 -1
  172. package/dist/styles/styleIndexer.js +51 -67
  173. package/dist/styles/styleIndexer.js.map +1 -1
  174. package/dist/styles/types.d.ts +3 -2
  175. package/dist/styles/types.d.ts.map +1 -1
  176. package/dist/types/crossLanguage.d.ts +1 -0
  177. package/dist/types/crossLanguage.d.ts.map +1 -1
  178. package/dist/types.d.ts +43 -34
  179. package/dist/types.d.ts.map +1 -1
  180. package/dist/types.js.map +1 -1
  181. package/dist/utils/fileDiscovery.d.ts +9 -0
  182. package/dist/utils/fileDiscovery.d.ts.map +1 -1
  183. package/dist/utils/fileDiscovery.js +18 -0
  184. package/dist/utils/fileDiscovery.js.map +1 -1
  185. package/package.json +16 -6
  186. package/plugin/.claude-plugin/plugin.json +1 -1
  187. package/plugin/README.md +11 -9
  188. package/plugin/hooks/hooks.json +4 -0
  189. package/plugin/scripts/hook-audit.sh +20 -30
  190. package/plugin/scripts/hook-common.sh +63 -0
  191. package/plugin/scripts/hook-self-audit.sh +99 -0
  192. package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +55 -9
  193. package/plugin/skills/code-auditor/SKILL.md +22 -23
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import './native-bootstrap.js';
7
7
  import { Command } from 'commander';
8
8
  import chalk from 'chalk';
9
9
  import { createAuditRunner } from './auditRunner.js';
10
+ import { runAuditDispatch } from './auditRouter.js';
10
11
  import { readFileSync } from 'fs';
11
12
  import { promises as fs } from 'fs';
12
13
  import { createInterface } from 'readline';
@@ -20,8 +21,11 @@ import { queryParser } from './search/QueryParser.js';
20
21
  import { CodeIndexDB } from './codeIndexDB.js';
21
22
  import { getFilesProcessed, getFactsConsumed, isVisitorStatus, isReducerStatus } from './pipeline.js';
22
23
  import { createBaselineFromFindings, saveBaseline, loadBaseline, diffBaselines } from './baseline.js';
23
- import { computeDiffGatingDecision } from './enforcement/gate.js';
24
- import { computeDiffGate } from './enforcement/diffGate.js';
24
+ import { ALL_ANALYZERS } from './analyzers/ruleRegistry.js';
25
+ import { computeGatingDecision } from './enforcement/gate.js';
26
+ import { DEFAULT_BLOCKING_SEVERITIES } from './config/defaults.js';
27
+ import { rankFilesByPriority, orderFindingsWithinFile } from './nextFile.js';
28
+ import { runNextFile } from './nextFileIncremental.js';
25
29
  // Get package.json for version info
26
30
  const __filename = fileURLToPath(import.meta.url);
27
31
  const __dirname = dirname(__filename);
@@ -127,13 +131,12 @@ program
127
131
  }
128
132
  }
129
133
  }
130
- const runner = createAuditRunner({
134
+ const result = await runAuditDispatch({
131
135
  projectRoot: options.path,
132
136
  configName: options.config,
133
137
  outputDirectory: options.output,
134
138
  presets: presetIds
135
139
  });
136
- const result = await runner.run();
137
140
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
138
141
  const baseline = result.metadata?.baseline;
139
142
  // ── Delta output (Spec 18 R2) ─────────────────────────────────
@@ -202,6 +205,7 @@ program
202
205
  console.log(`Critical: ${result.summary.criticalIssues}`);
203
206
  console.log(`Warnings: ${result.summary.warnings}`);
204
207
  console.log(`Suggestions: ${result.summary.suggestions}`);
208
+ console.log(chalk.gray(`\nEvery finding is a defect to resolve — severity ranks urgency, never whether a finding is real.`));
205
209
  console.log(chalk.gray(`\n💡 Run ${chalk.cyan('code-audit baseline')} to adopt the ratchet and track changes over time.`));
206
210
  }
207
211
  else {
@@ -210,6 +214,7 @@ program
210
214
  console.log(`Critical: ${result.summary.criticalIssues}`);
211
215
  console.log(`Warnings: ${result.summary.warnings}`);
212
216
  console.log(`Suggestions: ${result.summary.suggestions}`);
217
+ console.log(chalk.gray(`\nEvery finding is a defect to resolve — severity ranks urgency, never whether a finding is real.`));
213
218
  }
214
219
  // Spec 44 R4 — file accounting: analyzed/dropped totals + optional breakdown.
215
220
  printFileAccounting(result, !!options.explainSkipped);
@@ -236,26 +241,6 @@ program
236
241
  const parts = skippedExtensions.map(s => `${s.ext} (${s.count})`);
237
242
  console.log(` ${parts.join(', ')}`);
238
243
  }
239
- // Spec 36 R7 — suppressions are reported: how many exist, where, and how
240
- // many are unnecessary (or reasonless). Both are errors, surfaced here.
241
- const suppressions = result.metadata?.suppressions;
242
- if (suppressions) {
243
- console.log(chalk.gray(`\n── Suppressions ──────────────────────────────`));
244
- console.log(` ${suppressions.total} directive${suppressions.total !== 1 ? 's' : ''}, ` +
245
- `${suppressions.suppressed} finding${suppressions.suppressed !== 1 ? 's' : ''} suppressed`);
246
- if (suppressions.unnecessary.length > 0) {
247
- console.error(chalk.red(` ❌ ${suppressions.unnecessary.length} unnecessary (finding no longer fires):`));
248
- for (const d of suppressions.unnecessary) {
249
- console.error(` ${d.rule} @ ${d.file}:${d.line}`);
250
- }
251
- }
252
- if (suppressions.reasonless.length > 0) {
253
- console.error(chalk.red(` ❌ ${suppressions.reasonless.length} missing a required reason:`));
254
- for (const d of suppressions.reasonless) {
255
- console.error(` ${d.rule} @ ${d.file}:${d.line}`);
256
- }
257
- }
258
- }
259
244
  // Per-analyzer activity (read from result data, not serialized summary)
260
245
  // — surfaces zero-scan failures that would otherwise be invisible.
261
246
  if (!options.json) {
@@ -283,13 +268,21 @@ program
283
268
  const clean = coverage.filter(c => c.state === 'clean');
284
269
  const unassessed = coverage.filter(c => c.state === 'unassessed');
285
270
  const notApplicable = coverage.filter(c => c.state === 'notApplicable');
271
+ const cannotFire = coverage.filter(c => c.state === 'cannot-fire');
286
272
  const firedCount = fired.reduce((s, c) => s + c.count, 0);
287
273
  console.log(chalk.gray(`\n── Coverage ─────────────────────────────────`));
288
274
  console.log(` ${fired.length} fired (${firedCount.toLocaleString()} violations), ` +
289
275
  `${clean.length} clean, ` +
290
276
  `${unassessed.length} unassessed, ` +
291
- `${notApplicable.length} notApplicable ` +
277
+ `${notApplicable.length} notApplicable, ` +
278
+ `${cannotFire.length} cannot-fire ` +
292
279
  `(${coverage.length} rules registered)`);
280
+ if (cannotFire.length > 0) {
281
+ console.log(chalk.yellow(` ── Cannot Fire (broken in the tool) ──`));
282
+ for (const c of cannotFire) {
283
+ console.log(` ${c.ruleId}: ${c.reason ?? 'unknown'}`);
284
+ }
285
+ }
293
286
  if (notApplicable.length > 0) {
294
287
  console.log(chalk.gray(` ── Not Applicable ──`));
295
288
  for (const c of notApplicable) {
@@ -434,6 +427,16 @@ program
434
427
  const result = await runner.run();
435
428
  // Collect all violations
436
429
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
430
+ // Blocking gate decision (Spec 45 R1/R2/R4), computed once so the
431
+ // agent-facing before/after count (Spec 45 A2) and the exit code agree on
432
+ // the same number. Every registered rule participates (R1); severity
433
+ // decides (R2); enforcement is not diff-scoped (R4). No rule is removed
434
+ // from the gate for speed (Spec 45 A1).
435
+ const configuredGateSeverities = result.metadata.configUsed?.gateSeverities;
436
+ const blockingSeverities = new Set(Array.isArray(configuredGateSeverities) && configuredGateSeverities.length > 0
437
+ ? configuredGateSeverities
438
+ : DEFAULT_BLOCKING_SEVERITIES);
439
+ const { blocking, resolutionGaps } = computeGatingDecision(violations, blockingSeverities);
437
440
  // JSON output
438
441
  if (options.format === 'sarif') {
439
442
  const { generateSARIFReport } = await import('./reporting/sarifReportGenerator.js');
@@ -473,15 +476,17 @@ program
473
476
  process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
474
477
  }
475
478
  else if (!options.quiet || violations.length > 0) {
476
- // Console output — Spec 36 R3: agent-facing output emits findings,
477
- // never an aggregate total. The hook and the `changed` command are the
478
- // agent's surface; a finding total is exactly the representation the
479
- // consumer routes around. Findings only, each with file:line.
479
+ // Console output — Spec 45 A2: agent-facing output emits counts (per
480
+ // analyzer/rule/severity, plus the before/after gate figure) alongside
481
+ // findings. The hook and the `changed` command are the agent's surface.
480
482
  console.log(chalk.blue('🔍 Diff-Scoped Code Audit'));
481
483
  console.log(chalk.gray('══════════════════════════════════════════════════'));
482
484
  printFileAccounting(result, !!options.explainSkipped);
483
485
  if (violations.length > 0) {
484
- console.log(chalk.gray('\n── Violations ────────────────────────────────────'));
486
+ console.log('');
487
+ printCountSummary(violations);
488
+ console.log(chalk.bold(`gate before/after: ${violations.length} → ${blocking.length} blocking`));
489
+ console.log(chalk.gray('── Violations ────────────────────────────────────'));
485
490
  for (const v of violations) {
486
491
  const icon = v.severity === 'critical' ? '🔴' :
487
492
  v.severity === 'warning' ? '🟡' : '🔵';
@@ -510,34 +515,23 @@ program
510
515
  }
511
516
  }
512
517
  else {
513
- lines.push(' (no gating rules recorded)');
518
+ lines.push(' (no rules recorded)');
514
519
  }
515
520
  process.stderr.write(lines.join('\n') + '\n');
516
521
  }
517
- // Binary gate (Spec 36 R2/R4/R6): exit 2 when a gating finding is present.
518
- // The `changed` command is the edit-boundary gate, so it compares against
519
- // the file's prior state (git HEAD) via the touched-line diff gate (R2),
520
- // NOT against a stored baseline. Gate-excluded files and non-gating rules
521
- // never block. Severity does NOT factor in (R4). A gating rule that cannot
522
- // name a next action for an occurrence emits non-blocking and the gap is
523
- // recorded (R6), surfaced below to stderr.
522
+ // Blocking gate (Spec 45 R1/R2/R4): exit 2 when a finding at a blocking
523
+ // severity is present. Every registered rule participates (R1); severity
524
+ // decides (R2); enforcement is not diff-scoped (R4) — a pre-existing
525
+ // finding in the audited file blocks exactly like a new one. Gate-excluded
526
+ // files never block. A rule that cannot name a next action still blocks;
527
+ // the missing action is recorded below (R1). The decision is computed
528
+ // above so the before/after count and the exit code share one number.
524
529
  {
525
- const analyzedFiles = result.metadata?.analyzedFiles;
526
- // The `changed` command is always scoped, so the runner records the
527
- // analyzed file list. If it is somehow absent we still derive the gate
528
- // from the files that actually produced findings rather than falling
529
- // back to the baseline gate — a silent soft-fail is the one outcome R1
530
- // forbids.
531
- const gateFiles = analyzedFiles && analyzedFiles.length > 0
532
- ? analyzedFiles
533
- : [...new Set(violations.map((v) => v.file).filter(Boolean))];
534
- const diffGate = computeDiffGate(options.path, gateFiles);
535
- const { blocking, resolutionGaps } = computeDiffGatingDecision(violations, diffGate);
536
530
  if (resolutionGaps.length > 0) {
537
- // Spec 36 R6 — a gating rule that could not name an action is a defect
538
- // in the rule. Record it loudly so it cannot route around as a count.
531
+ // Spec 45 R1 — a rule that could not name an action is a defect in the
532
+ // rule. Record it loudly so it cannot route around as a count.
539
533
  const lines = [
540
- '⚠️ resolution gap — gating rule produced no next action for these occurrences (Spec 36 R6):',
534
+ '⚠️ resolution gap — rule produced no next action for these occurrences (Spec 45 R1):',
541
535
  ...resolutionGaps.map((g) => ` - ${g.rule} @ ${g.file}${g.line ? `:${g.line}` : ''}`),
542
536
  ];
543
537
  process.stderr.write(lines.join('\n') + '\n');
@@ -545,28 +539,6 @@ program
545
539
  if (blocking.length > 0) {
546
540
  process.exit(2);
547
541
  }
548
- // Spec 36 R7 — an unnecessary or reasonless suppression is itself an
549
- // error. It is reported loudly and fails the write, exactly like an
550
- // unused `@ts-expect-error`. A suppression that outlives its finding is
551
- // a baseline entry with better branding unless it errors here.
552
- const suppressions = result.metadata?.suppressions;
553
- if (suppressions && (suppressions.unnecessary.length > 0 || suppressions.reasonless.length > 0)) {
554
- const lines = [];
555
- if (suppressions.reasonless.length > 0) {
556
- lines.push('❌ suppression missing a required reason (Spec 36 R7):');
557
- for (const d of suppressions.reasonless) {
558
- lines.push(` - ${d.rule} @ ${d.file}:${d.line}`);
559
- }
560
- }
561
- if (suppressions.unnecessary.length > 0) {
562
- lines.push('❌ unnecessary suppression — the finding no longer fires (Spec 36 R7):');
563
- for (const d of suppressions.unnecessary) {
564
- lines.push(` - ${d.rule} @ ${d.file}:${d.line}`);
565
- }
566
- }
567
- process.stderr.write(lines.join('\n') + '\n');
568
- process.exit(2);
569
- }
570
542
  }
571
543
  // Exit code based on --no-fail-on-zero-files (default-on in v3.4.8)
572
544
  // Any enabled analyzer matching zero source files is a failure — a >50%
@@ -586,6 +558,258 @@ program
586
558
  process.exit(1);
587
559
  }
588
560
  });
561
+ // Spec 45 A2 — counts are emitted where useful (per analyzer, per rule, per
562
+ // severity), on agent-facing surfaces as well as human ones. This reverts Spec
563
+ // 36 R3's "no bare counts": an agent now sees the shape of what fired instead
564
+ // of a single aggregate it could route around. The `before/after` gate figure
565
+ // is Spec 45 R1's "report count before/after".
566
+ function summarizeFindings(violations) {
567
+ const bySeverity = new Map();
568
+ const byAnalyzer = new Map();
569
+ const byRule = new Map();
570
+ for (const v of violations) {
571
+ const severity = v.severity ?? 'unknown';
572
+ bySeverity.set(severity, (bySeverity.get(severity) ?? 0) + 1);
573
+ const analyzer = v.analyzer ?? 'unknown';
574
+ byAnalyzer.set(analyzer, (byAnalyzer.get(analyzer) ?? 0) + 1);
575
+ const rule = v.rule || v.type || 'unknown';
576
+ byRule.set(rule, (byRule.get(rule) ?? 0) + 1);
577
+ }
578
+ return { bySeverity, byAnalyzer, byRule };
579
+ }
580
+ function groupedCounts(map, sep = ', ') {
581
+ return [...map.entries()]
582
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
583
+ .map(([k, n]) => `${k} ${n}`)
584
+ .join(sep);
585
+ }
586
+ function printCountSummary(violations) {
587
+ const { bySeverity, byAnalyzer, byRule } = summarizeFindings(violations);
588
+ console.log(chalk.gray('── Findings ────────────────────────────────────'));
589
+ console.log(`${violations.length} total · ${groupedCounts(bySeverity, ' · ')}`);
590
+ console.log(`by analyzer: ${groupedCounts(byAnalyzer)}`);
591
+ console.log(`by rule: ${groupedCounts(byRule)}`);
592
+ }
593
+ // Self-audit gate (Spec 33 Item 15 + Spec 44 remediation). Runs the full
594
+ // analyzer pipeline over the tool's own production source and asserts zero
595
+ // *blocking* (critical/warning) findings in the self-audit scope — the same
596
+ // scope + severity contract as scripts/verify-self.mjs, but callable from the
597
+ // shipped CLI so the edit-time plugin hook can enforce it. `verify:self` (the
598
+ // release gate) stays authoritative; this command is its per-edit sibling.
599
+ //
600
+ // Scope semantics: a file is a self-audit target iff its path is under
601
+ // `analyzers/` or `languages/` (relative to the project's `src/`) and is not a
602
+ // test/spec/fixture or the declarative `ruleRegistry.ts` data table. Mirroring
603
+ // verify-self.mjs keeps the two gates from drifting.
604
+ function selfAuditScopePath(file) {
605
+ const idx = file.lastIndexOf('/src/');
606
+ if (idx === -1)
607
+ return null;
608
+ return file.slice(idx + '/src/'.length);
609
+ }
610
+ function isSelfAuditInScope(file) {
611
+ const rel = selfAuditScopePath(file);
612
+ if (!rel)
613
+ return false;
614
+ if (!(rel.startsWith('analyzers/') || rel.startsWith('languages/')))
615
+ return false;
616
+ if (/(__tests__|\.test\.|\.spec\.|fixtures)/.test(rel))
617
+ return false;
618
+ if (rel === 'analyzers/ruleRegistry.ts')
619
+ return false;
620
+ return true;
621
+ }
622
+ program
623
+ .command('self-audit [paths...]')
624
+ .description('Audit the tool\'s own analyzers/ + languages/ source and fail on blocking findings')
625
+ .option('--json', 'Output violations as machine-readable JSON to stdout')
626
+ .option('--stdin', 'Read file paths from stdin (one per line)')
627
+ .option('-p, --path <projectPath>', 'Project root path', process.cwd())
628
+ .option('--fail-on <severity>', 'Blocking severity floor: critical, warning, or suggestion', 'warning')
629
+ .action(async (paths, options) => {
630
+ try {
631
+ await initParsers();
632
+ const validSeverities = ['critical', 'warning', 'suggestion'];
633
+ const failOnSeverity = options.failOn;
634
+ if (!validSeverities.includes(failOnSeverity)) {
635
+ console.error(chalk.red(`Invalid --fail-on severity: "${failOnSeverity}". Must be one of: ${validSeverities.join(', ')}`));
636
+ process.exit(1);
637
+ }
638
+ const fileSet = new Set();
639
+ if (options.stdin) {
640
+ const rl = createInterface({
641
+ input: process.stdin,
642
+ output: undefined,
643
+ terminal: false
644
+ });
645
+ for await (const line of rl) {
646
+ const trimmed = line.trim();
647
+ if (trimmed)
648
+ fileSet.add(trimmed);
649
+ }
650
+ }
651
+ for (const p of paths)
652
+ fileSet.add(p);
653
+ // No explicit paths → audit the whole `src` tree (mirrors verify:self).
654
+ if (fileSet.size === 0) {
655
+ fileSet.add(join(options.path, 'src'));
656
+ }
657
+ // Resolve to absolute paths; `createAuditRunner` treats an array scope as
658
+ // explicit files/globs (directories included) and, for scoped runs, still
659
+ // loads the full function index so cross-file DRY duplicates are caught.
660
+ const resolved = [...fileSet].map((f) => isAbsolute(f) ? f : resolve(process.cwd(), f));
661
+ const scope = resolved;
662
+ const runner = createAuditRunner({
663
+ projectRoot: options.path,
664
+ scope,
665
+ analyzerConcurrency: 4
666
+ });
667
+ const result = await runner.run();
668
+ const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
669
+ const severityOrder = ['critical', 'warning', 'suggestion'];
670
+ const failIndex = severityOrder.indexOf(failOnSeverity);
671
+ const blocking = violations.filter((v) => {
672
+ if (!isSelfAuditInScope(v.file ?? ''))
673
+ return false;
674
+ const vIndex = severityOrder.indexOf(v.severity);
675
+ return vIndex >= 0 && vIndex <= failIndex;
676
+ });
677
+ if (options.json) {
678
+ const projectDir = resolve(options.path || process.cwd());
679
+ const jsonOutput = blocking.map((v) => {
680
+ let filePath = v.file || '';
681
+ if (filePath.startsWith('/') || filePath.startsWith('\\\\')) {
682
+ const rel = relative(projectDir, filePath);
683
+ if (!rel.startsWith('..') && !isAbsolute(rel))
684
+ filePath = rel;
685
+ }
686
+ return {
687
+ analyzer: v.analyzer || '',
688
+ rule: v.rule || v.type || '',
689
+ severity: v.severity,
690
+ message: v.message,
691
+ file: filePath,
692
+ line: v.line ?? v.start?.line,
693
+ column: v.column ?? v.start?.column ?? 1,
694
+ endLine: v.end?.line,
695
+ endColumn: v.end?.column,
696
+ enclosingSymbol: v.symbol || v.enclosingFunction || '',
697
+ suggestion: v.suggestion || '',
698
+ details: v.details || ''
699
+ };
700
+ });
701
+ process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
702
+ }
703
+ else if (blocking.length > 0) {
704
+ // Spec 45 A2 — counts plus findings (per analyzer/rule/severity).
705
+ console.log(chalk.red('\nSelf-audit blocking findings:'));
706
+ printCountSummary(blocking);
707
+ for (const v of blocking) {
708
+ const icon = v.severity === 'critical' ? '🔴' :
709
+ v.severity === 'warning' ? '🟡' : '🔵';
710
+ console.log(`${icon} ${v.file}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.message}`);
711
+ }
712
+ }
713
+ else {
714
+ console.log(chalk.green('\n✓ Self-audit clean — zero blocking findings in scope.'));
715
+ }
716
+ if (blocking.length > 0)
717
+ process.exit(2);
718
+ }
719
+ catch (error) {
720
+ console.error(chalk.red('Error:'), error);
721
+ process.exit(1);
722
+ }
723
+ });
724
+ // Next-file command: the refactoring loop's single entry point. The first call
725
+ // seeds a full audit into a snapshot (file → content hash + findings); each
726
+ // subsequent call diffs, re-audits only what changed, merges per-analyzer, and
727
+ // returns the head file with every finding on it. A consuming LLM calls it,
728
+ // fixes the returned file, and calls it again — if the file is still in bad
729
+ // shape it comes back, otherwise the next-worst file surfaces. The queue is
730
+ // derived from the snapshot each invocation, so there is no cursor to corrupt
731
+ // and no decline action: removing a finding means editing the rules, not
732
+ // skipping the file.
733
+ program
734
+ .command('next-file')
735
+ .description('Audit and return the highest-priority file with findings (file-by-file refactor loop)')
736
+ .option('-p, --path <projectPath>', 'Project root path', process.cwd())
737
+ .option('-c, --config <config>', 'Configuration name')
738
+ .option('--json', 'Output a single JSON object (or {done:true}) to stdout')
739
+ .action(async (options) => {
740
+ try {
741
+ await initParsers();
742
+ const { violations, summary } = await runNextFile({
743
+ projectRoot: options.path,
744
+ configName: options.config,
745
+ });
746
+ // Rank files worst-first from the (incrementally-merged) violation set.
747
+ const ranked = rankFilesByPriority(violations);
748
+ if (ranked.length === 0) {
749
+ if (options.json) {
750
+ process.stdout.write(JSON.stringify({ done: true, summary }, null, 2) + '\n');
751
+ }
752
+ else {
753
+ console.log(chalk.green('\n✓ No findings — nothing left to refactor.'));
754
+ }
755
+ return;
756
+ }
757
+ const top = ranked[0];
758
+ // Every issue on the file, ordered critical → warning → suggestion.
759
+ const ordered = orderFindingsWithinFile(top.violations);
760
+ const projectDir = resolve(options.path || process.cwd());
761
+ const relativize = (filePath) => {
762
+ if (!filePath)
763
+ return '';
764
+ if (filePath.startsWith('/') || filePath.startsWith('\\\\')) {
765
+ const rel = relative(projectDir, filePath);
766
+ if (!rel.startsWith('..') && !isAbsolute(rel))
767
+ return rel;
768
+ }
769
+ return filePath;
770
+ };
771
+ if (options.json) {
772
+ const output = {
773
+ done: false,
774
+ file: relativize(top.file),
775
+ remainingFiles: ranked.length - 1,
776
+ remainingFindings: violations.length - top.count,
777
+ summary,
778
+ findings: ordered.map((v) => ({
779
+ analyzer: v.analyzer || '',
780
+ rule: v.rule || v.type || '',
781
+ severity: v.severity,
782
+ message: v.message,
783
+ file: relativize(v.file || ''),
784
+ line: v.line ?? v.start?.line,
785
+ column: v.column ?? v.start?.column ?? 1,
786
+ endLine: v.end?.line,
787
+ endColumn: v.end?.column,
788
+ enclosingSymbol: v.symbol || v.enclosingFunction || '',
789
+ suggestion: v.suggestion || '',
790
+ details: v.details || '',
791
+ ...(v.new !== undefined && { new: v.new }),
792
+ })),
793
+ };
794
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
795
+ }
796
+ else {
797
+ console.log(chalk.blue('🔍 Next File to Refactor'));
798
+ console.log(chalk.gray('══════════════════════════════════════════════════'));
799
+ console.log(`\n${chalk.bold(relativize(top.file))} — ${top.count} finding(s), highest severity ${top.maxSeverity}`);
800
+ console.log(chalk.gray(`${ranked.length - 1} more file(s) with findings · ${violations.length - top.count} remaining finding(s)`));
801
+ console.log(chalk.gray('\n── Findings ────────────────────────────────────────'));
802
+ for (const v of ordered) {
803
+ const icon = v.severity === 'critical' ? '🔴' : v.severity === 'warning' ? '🟡' : '🔵';
804
+ console.log(`${icon} ${chalk.bold(relativize(v.file || ''))}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.rule} — ${v.message}`);
805
+ }
806
+ }
807
+ }
808
+ catch (error) {
809
+ console.error(chalk.red('Error:'), error);
810
+ process.exit(1);
811
+ }
812
+ });
589
813
  // Baseline command (Spec 18 R1)
590
814
  program
591
815
  .command('baseline')
@@ -1871,20 +2095,9 @@ ledgerCmd
1871
2095
  // `audit --detach` forks workers/auditJobRunner; `status`/`result`/`jobs` read
1872
2096
  // the persisted ledger. One job model (the ledger run), one write path — these
1873
2097
  // are thin wrappers over ledger.ts + mcpAuditJobs.ts, not a second store.
1874
- /** Full analyzer set — matches createAuditRunner's registry, so `--detach` produces
1875
- * the same per-rule coverage as a synchronous `audit` (73 rows on recall). */
1876
- const DETACHED_DEFAULT_ANALYZERS = [
1877
- 'solid',
1878
- 'dry',
1879
- 'data-access',
1880
- 'react',
1881
- 'documentation',
1882
- 'invariants',
1883
- 'schema',
1884
- 'styles',
1885
- 'conventions',
1886
- 'cross-domain',
1887
- ];
2098
+ /** Full analyzer set — derives from createAuditRunner's registry so `--detach`
2099
+ * produces the same per-rule coverage as a synchronous `audit`. */
2100
+ const DETACHED_DEFAULT_ANALYZERS = [...ALL_ANALYZERS];
1888
2101
  /** Lease TTL used by read-path reclaim, mirroring mcpAuditJobs.jobLeaseTtlMs. */
1889
2102
  const CLI_JOB_LEASE_TTL_MS = Number(process.env.CODE_AUDITOR_JOB_LEASE_TTL_MS) || 30_000;
1890
2103
  function spec41StatusBadge(status) {
@@ -2054,7 +2267,7 @@ program
2054
2267
  .option('--analyzer <analyzer>', 'Filter by analyzer')
2055
2268
  .option('--file <file>', 'Filter by file path (substring match)')
2056
2269
  .option('--severity <severity>', 'Filter by severity (critical|warning|suggestion)')
2057
- .option('--state [state]', 'Query coverage by state (fired|clean|notApplicable|unassessed); omit value for all')
2270
+ .option('--state [state]', 'Query coverage by state (fired|clean|notApplicable|cannot-fire|unassessed); omit value for all')
2058
2271
  .option('--count', 'Group findings by analyzer/rule with counts')
2059
2272
  .option('--limit <n>', 'Max findings to return (0 = unbounded)', '50')
2060
2273
  .option('--offset <n>', 'Findings offset', '0')
@@ -2779,20 +2992,18 @@ program
2779
2992
  line: v.line ?? v.start?.line,
2780
2993
  message: v.message,
2781
2994
  resolution: v.resolution ?? null,
2782
- suppressed: v.suppressed === true,
2783
- ...(v.suppressionReason && { suppressionReason: v.suppressionReason }),
2784
2995
  };
2785
2996
  });
2786
2997
  process.stdout.write(JSON.stringify(out, null, 2) + '\n');
2787
2998
  return;
2788
2999
  }
2789
- // Spec 36 R3 — findings, never a total. Each line is a file:line finding;
2790
- // there is no count anywhere in this output to route around.
3000
+ // Spec 45 A2counts plus findings (per analyzer/rule/severity).
3001
+ printCountSummary(sorted);
3002
+ console.log('');
2791
3003
  for (const v of sorted) {
2792
3004
  const icon = v.severity === 'critical' ? '🔴' :
2793
3005
  v.severity === 'warning' ? '🟡' : '🔵';
2794
- const suppressed = v.suppressed ? chalk.dim(' [suppressed]') : '';
2795
- console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.rule || v.severity}] ${v.message}${suppressed}`);
3006
+ console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.rule || v.severity}] ${v.message}`);
2796
3007
  }
2797
3008
  }
2798
3009
  catch (error) {