code-auditor-mcp 3.5.0 → 3.7.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 (110) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +62 -0
  3. package/dist/analyzers/applicability.d.ts +6 -5
  4. package/dist/analyzers/applicability.d.ts.map +1 -1
  5. package/dist/analyzers/applicability.js +1 -26
  6. package/dist/analyzers/applicability.js.map +1 -1
  7. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  8. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +12 -1
  9. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  10. package/dist/analyzers/ruleRegistry.d.ts +14 -12
  11. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  12. package/dist/analyzers/ruleRegistry.js +138 -98
  13. package/dist/analyzers/ruleRegistry.js.map +1 -1
  14. package/dist/analyzers/ruleTiming.d.ts +2 -2
  15. package/dist/analyzers/ruleTiming.js +2 -2
  16. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +29 -0
  17. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  18. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +376 -6
  19. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  20. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +6 -0
  21. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  22. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +41 -7
  23. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  24. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  25. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +13 -2
  26. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  27. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +20 -5
  28. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  29. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +83 -14
  30. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  31. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  32. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +5 -2
  33. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  34. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts +55 -0
  35. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -0
  36. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js +318 -0
  37. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js.map +1 -0
  38. package/dist/analyzers/universal/functionConcerns.d.ts +52 -0
  39. package/dist/analyzers/universal/functionConcerns.d.ts.map +1 -0
  40. package/dist/analyzers/universal/functionConcerns.js +225 -0
  41. package/dist/analyzers/universal/functionConcerns.js.map +1 -0
  42. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
  43. package/dist/analyzers/universal/schema/codeAnalysis.js +9 -3
  44. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  45. package/dist/analyzers/universal/schema/discovery.d.ts +29 -1
  46. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -1
  47. package/dist/analyzers/universal/schema/discovery.js +76 -21
  48. package/dist/analyzers/universal/schema/discovery.js.map +1 -1
  49. package/dist/auditRunner.d.ts.map +1 -1
  50. package/dist/auditRunner.js +73 -74
  51. package/dist/auditRunner.js.map +1 -1
  52. package/dist/cli.js +109 -105
  53. package/dist/cli.js.map +1 -1
  54. package/dist/codeIndexDB.js +6 -6
  55. package/dist/codeIndexDB.js.map +1 -1
  56. package/dist/config/configLoader.d.ts.map +1 -1
  57. package/dist/config/configLoader.js +15 -0
  58. package/dist/config/configLoader.js.map +1 -1
  59. package/dist/config/defaults.d.ts +11 -1
  60. package/dist/config/defaults.d.ts.map +1 -1
  61. package/dist/config/defaults.js +25 -3
  62. package/dist/config/defaults.js.map +1 -1
  63. package/dist/config/effectiveConfig.d.ts.map +1 -1
  64. package/dist/config/effectiveConfig.js +2 -0
  65. package/dist/config/effectiveConfig.js.map +1 -1
  66. package/dist/dataPaths.js +18 -1
  67. package/dist/dataPaths.js.map +1 -1
  68. package/dist/enforcement/gate.d.ts +14 -19
  69. package/dist/enforcement/gate.d.ts.map +1 -1
  70. package/dist/enforcement/gate.js +27 -45
  71. package/dist/enforcement/gate.js.map +1 -1
  72. package/dist/graph/importGraph.d.ts +11 -0
  73. package/dist/graph/importGraph.d.ts.map +1 -1
  74. package/dist/graph/importGraph.js +11 -3
  75. package/dist/graph/importGraph.js.map +1 -1
  76. package/dist/installer.d.ts.map +1 -1
  77. package/dist/installer.js +2 -5
  78. package/dist/installer.js.map +1 -1
  79. package/dist/mcp.js +1 -1
  80. package/dist/mcp.js.map +1 -1
  81. package/dist/pipeline.d.ts.map +1 -1
  82. package/dist/pipeline.js +21 -3
  83. package/dist/pipeline.js.map +1 -1
  84. package/dist/pipelineAdapters.d.ts +1 -0
  85. package/dist/pipelineAdapters.d.ts.map +1 -1
  86. package/dist/pipelineAdapters.js +227 -4
  87. package/dist/pipelineAdapters.js.map +1 -1
  88. package/dist/styles/styleExtractor.js +2 -2
  89. package/dist/styles/styleExtractor.js.map +1 -1
  90. package/dist/styles/styleIndexer.js +7 -6
  91. package/dist/styles/styleIndexer.js.map +1 -1
  92. package/dist/styles/tailwindProbe.d.ts +8 -0
  93. package/dist/styles/tailwindProbe.d.ts.map +1 -1
  94. package/dist/styles/tailwindProbe.js +11 -1
  95. package/dist/styles/tailwindProbe.js.map +1 -1
  96. package/dist/styles/types.d.ts +3 -2
  97. package/dist/styles/types.d.ts.map +1 -1
  98. package/dist/types/crossLanguage.d.ts +1 -0
  99. package/dist/types/crossLanguage.d.ts.map +1 -1
  100. package/dist/types.d.ts +23 -30
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js.map +1 -1
  103. package/package.json +13 -4
  104. package/plugin/.claude-plugin/plugin.json +1 -1
  105. package/plugin/README.md +5 -3
  106. package/plugin/scripts/hook-audit.sh +20 -30
  107. package/plugin/scripts/hook-common.sh +63 -0
  108. package/plugin/scripts/hook-self-audit.sh +23 -31
  109. package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +4 -2
  110. package/plugin/skills/code-auditor/SKILL.md +20 -12
package/dist/cli.js CHANGED
@@ -22,8 +22,8 @@ import { CodeIndexDB } from './codeIndexDB.js';
22
22
  import { getFilesProcessed, getFactsConsumed, isVisitorStatus, isReducerStatus } from './pipeline.js';
23
23
  import { createBaselineFromFindings, saveBaseline, loadBaseline, diffBaselines } from './baseline.js';
24
24
  import { ALL_ANALYZERS } from './analyzers/ruleRegistry.js';
25
- import { computeDiffGatingDecision } from './enforcement/gate.js';
26
- import { computeDiffGate } from './enforcement/diffGate.js';
25
+ import { computeGatingDecision } from './enforcement/gate.js';
26
+ import { DEFAULT_BLOCKING_SEVERITIES } from './config/defaults.js';
27
27
  import { rankFilesByPriority, orderFindingsWithinFile } from './nextFile.js';
28
28
  import { runNextFile } from './nextFileIncremental.js';
29
29
  // Get package.json for version info
@@ -139,6 +139,20 @@ program
139
139
  });
140
140
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
141
141
  const baseline = result.metadata?.baseline;
142
+ // ── Coverage panel leads the report (Spec 47 R2) ─────────────
143
+ // A diagnostic report opens with what was measured before it lists any
144
+ // readings, so a zero-reading report can't be mistaken for a clean tree.
145
+ const coverage = result.metadata?.coverage;
146
+ if (coverage && coverage.length > 0) {
147
+ const covFired = coverage.filter(c => c.state === 'fired').length;
148
+ const covClean = coverage.filter(c => c.state === 'clean').length;
149
+ const covUnassessed = coverage.filter(c => c.state === 'unassessed').length;
150
+ const covNotApplicable = coverage.filter(c => c.state === 'notApplicable').length;
151
+ const covCannotFire = coverage.filter(c => c.state === 'cannot-fire').length;
152
+ console.log(chalk.gray(`── Coverage panel ── ${covFired} fired · ${covClean} clean · ` +
153
+ `${covUnassessed} unassessed · ${covNotApplicable} not-applicable · ` +
154
+ `${covCannotFire} cannot-fire`));
155
+ }
142
156
  // ── Delta output (Spec 18 R2) ─────────────────────────────────
143
157
  if (baseline && !options.full) {
144
158
  const newViolations = violations.filter((v) => v.new === true);
@@ -155,7 +169,7 @@ program
155
169
  : '(unchanged)';
156
170
  console.log(`\n📊 Delta: +${newViolations.length} new · −${fixedCount} fixed · ${knownCount} known ${trendIcon} ${trendLabel}`);
157
171
  if (newViolations.length > 0) {
158
- console.log(chalk.gray(`\n── New Findings (${newViolations.length}) ──────────────────────────`));
172
+ console.log(chalk.gray(`\n── New readings (${newViolations.length}) ──────────────────────────`));
159
173
  for (const v of newViolations) {
160
174
  const icon = v.severity === 'critical' ? '🔴' :
161
175
  v.severity === 'warning' ? '🟡' : '🔵';
@@ -163,9 +177,9 @@ program
163
177
  }
164
178
  }
165
179
  else {
166
- console.log(chalk.green('\n✓ No new findings since last baseline.'));
180
+ console.log(chalk.green('\n✓ No new readings since last baseline.'));
167
181
  if (knownCount > 0) {
168
- console.log(chalk.gray(` ${knownCount} known finding(s) are still open — recorded, not fixed.`));
182
+ console.log(chalk.gray(` ${knownCount} known reading(s) are still open — recorded, not resolved.`));
169
183
  }
170
184
  }
171
185
  // Debt by analyzer
@@ -195,26 +209,26 @@ program
195
209
  .sort((a, b) => b[1] - a[1])
196
210
  .slice(0, 5);
197
211
  for (const [file, count] of topFiles) {
198
- console.log(`${file} — ${count} finding${count !== 1 ? 's' : ''}`);
212
+ console.log(`${file} — ${count} reading${count !== 1 ? 's' : ''}`);
199
213
  }
200
- console.log(chalk.gray(`\n💡 Run ${chalk.cyan('code-audit --full')} to see all ${currentDebt.toLocaleString()} findings.`));
214
+ console.log(chalk.gray(`\n💡 Run ${chalk.cyan('code-audit --full')} to see all ${currentDebt.toLocaleString()} readings.`));
201
215
  }
202
216
  else if (!baseline) {
203
217
  // No baseline: current behavior + hint
204
- console.log(`\nFound ${result.summary.totalViolations} violations`);
218
+ console.log(`\nFound ${result.summary.totalViolations} readings`);
205
219
  console.log(`Critical: ${result.summary.criticalIssues}`);
206
220
  console.log(`Warnings: ${result.summary.warnings}`);
207
221
  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.`));
222
+ console.log(chalk.gray(`\nEvery reading is a measurement, not a verdict — severity is triage, the order to act.`));
209
223
  console.log(chalk.gray(`\n💡 Run ${chalk.cyan('code-audit baseline')} to adopt the ratchet and track changes over time.`));
210
224
  }
211
225
  else {
212
226
  // --full with baseline: full itemized inventory (current behavior)
213
- console.log(`\nFound ${result.summary.totalViolations} violations`);
227
+ console.log(`\nFound ${result.summary.totalViolations} readings`);
214
228
  console.log(`Critical: ${result.summary.criticalIssues}`);
215
229
  console.log(`Warnings: ${result.summary.warnings}`);
216
230
  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.`));
231
+ console.log(chalk.gray(`\nEvery reading is a measurement, not a verdict — severity is triage, the order to act.`));
218
232
  }
219
233
  // Spec 44 R4 — file accounting: analyzed/dropped totals + optional breakdown.
220
234
  printFileAccounting(result, !!options.explainSkipped);
@@ -241,26 +255,6 @@ program
241
255
  const parts = skippedExtensions.map(s => `${s.ext} (${s.count})`);
242
256
  console.log(` ${parts.join(', ')}`);
243
257
  }
244
- // Spec 36 R7 — suppressions are reported: how many exist, where, and how
245
- // many are unnecessary (or reasonless). Both are errors, surfaced here.
246
- const suppressions = result.metadata?.suppressions;
247
- if (suppressions) {
248
- console.log(chalk.gray(`\n── Suppressions ──────────────────────────────`));
249
- console.log(` ${suppressions.total} directive${suppressions.total !== 1 ? 's' : ''}, ` +
250
- `${suppressions.suppressed} finding${suppressions.suppressed !== 1 ? 's' : ''} suppressed`);
251
- if (suppressions.unnecessary.length > 0) {
252
- console.error(chalk.red(` ❌ ${suppressions.unnecessary.length} unnecessary (finding no longer fires):`));
253
- for (const d of suppressions.unnecessary) {
254
- console.error(` ${d.rule} @ ${d.file}:${d.line}`);
255
- }
256
- }
257
- if (suppressions.reasonless.length > 0) {
258
- console.error(chalk.red(` ❌ ${suppressions.reasonless.length} missing a required reason:`));
259
- for (const d of suppressions.reasonless) {
260
- console.error(` ${d.rule} @ ${d.file}:${d.line}`);
261
- }
262
- }
263
- }
264
258
  // Per-analyzer activity (read from result data, not serialized summary)
265
259
  // — surfaces zero-scan failures that would otherwise be invisible.
266
260
  if (!options.json) {
@@ -316,7 +310,7 @@ program
316
310
  }
317
311
  }
318
312
  if (unassessed.length > 0) {
319
- console.log(chalk.gray(` ── Unassessed (zero findings, applicability unknown) ──`));
313
+ console.log(chalk.gray(` ── Unassessed (zero readings, applicability unknown) ──`));
320
314
  for (const c of unassessed) {
321
315
  console.log(` ${c.ruleId} (0)`);
322
316
  }
@@ -328,7 +322,7 @@ program
328
322
  const builtinCapped = Object.values(result.analyzerResults)
329
323
  .reduce((count, ar) => count + ar.violations.filter(v => v.profile === 'scripts-and-tests').length, 0);
330
324
  if (builtinCapped > 0) {
331
- console.log(chalk.blue(`\nℹ️ ${builtinCapped.toLocaleString()} findings capped by built-in profile "scripts-and-tests" (scripts/tests/fixtures → suggestion).`));
325
+ console.log(chalk.blue(`\nℹ️ ${builtinCapped.toLocaleString()} readings capped by built-in profile "scripts-and-tests" (scripts/tests/fixtures → suggestion).`));
332
326
  console.log(chalk.gray(` Set ${chalk.cyan('"builtin": false')} in .codeauditor.json to disable.`));
333
327
  }
334
328
  // Generate formatted report if --format is specified
@@ -353,7 +347,7 @@ program
353
347
  const currentDebt = violations.filter((v) => v.new || v.new === false).length;
354
348
  const snapshotDebt = baseline.previousKnownCount ?? 0;
355
349
  if (currentDebt > snapshotDebt) {
356
- console.error(chalk.red(`Debt regression: ${currentDebt - snapshotDebt} findings added without re-baselining.`));
350
+ console.error(chalk.red(`Debt regression: ${currentDebt - snapshotDebt} readings added without re-baselining.`));
357
351
  process.exit(2);
358
352
  }
359
353
  }
@@ -447,6 +441,16 @@ program
447
441
  const result = await runner.run();
448
442
  // Collect all violations
449
443
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
444
+ // Blocking gate decision (Spec 45 R1/R2/R4), computed once so the
445
+ // agent-facing before/after count (Spec 45 A2) and the exit code agree on
446
+ // the same number. Every registered rule participates (R1); severity
447
+ // decides (R2); enforcement is not diff-scoped (R4). No rule is removed
448
+ // from the gate for speed (Spec 45 A1).
449
+ const configuredGateSeverities = result.metadata.configUsed?.gateSeverities;
450
+ const blockingSeverities = new Set(Array.isArray(configuredGateSeverities) && configuredGateSeverities.length > 0
451
+ ? configuredGateSeverities
452
+ : DEFAULT_BLOCKING_SEVERITIES);
453
+ const { blocking, resolutionGaps } = computeGatingDecision(violations, blockingSeverities);
450
454
  // JSON output
451
455
  if (options.format === 'sarif') {
452
456
  const { generateSARIFReport } = await import('./reporting/sarifReportGenerator.js');
@@ -486,15 +490,17 @@ program
486
490
  process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
487
491
  }
488
492
  else if (!options.quiet || violations.length > 0) {
489
- // Console output — Spec 36 R3: agent-facing output emits findings,
490
- // never an aggregate total. The hook and the `changed` command are the
491
- // agent's surface; a finding total is exactly the representation the
492
- // consumer routes around. Findings only, each with file:line.
493
+ // Console output — Spec 45 A2: agent-facing output emits counts (per
494
+ // analyzer/rule/severity, plus the before/after gate figure) alongside
495
+ // findings. The hook and the `changed` command are the agent's surface.
493
496
  console.log(chalk.blue('🔍 Diff-Scoped Code Audit'));
494
497
  console.log(chalk.gray('══════════════════════════════════════════════════'));
495
498
  printFileAccounting(result, !!options.explainSkipped);
496
499
  if (violations.length > 0) {
497
- console.log(chalk.gray('\n── Violations ────────────────────────────────────'));
500
+ console.log('');
501
+ printCountSummary(violations);
502
+ console.log(chalk.bold(`gate before/after: ${violations.length} → ${blocking.length} blocking`));
503
+ console.log(chalk.gray('── Violations ────────────────────────────────────'));
498
504
  for (const v of violations) {
499
505
  const icon = v.severity === 'critical' ? '🔴' :
500
506
  v.severity === 'warning' ? '🟡' : '🔵';
@@ -505,7 +511,7 @@ program
505
511
  }
506
512
  }
507
513
  else {
508
- console.log(chalk.green('\n✓ No findings.'));
514
+ console.log(chalk.green('\n✓ No readings.'));
509
515
  }
510
516
  }
511
517
  // Spec 38 R2/R3 — per-rule timing + gate wall-clock, opt-in by env var.
@@ -523,34 +529,23 @@ program
523
529
  }
524
530
  }
525
531
  else {
526
- lines.push(' (no gating rules recorded)');
532
+ lines.push(' (no rules recorded)');
527
533
  }
528
534
  process.stderr.write(lines.join('\n') + '\n');
529
535
  }
530
- // Binary gate (Spec 36 R2/R4/R6): exit 2 when a gating finding is present.
531
- // The `changed` command is the edit-boundary gate, so it compares against
532
- // the file's prior state (git HEAD) via the touched-line diff gate (R2),
533
- // NOT against a stored baseline. Gate-excluded files and non-gating rules
534
- // never block. Severity does NOT factor in (R4). A gating rule that cannot
535
- // name a next action for an occurrence emits non-blocking and the gap is
536
- // recorded (R6), surfaced below to stderr.
536
+ // Blocking gate (Spec 45 R1/R2/R4): exit 2 when a finding at a blocking
537
+ // severity is present. Every registered rule participates (R1); severity
538
+ // decides (R2); enforcement is not diff-scoped (R4) — a pre-existing
539
+ // finding in the audited file blocks exactly like a new one. Gate-excluded
540
+ // files never block. A rule that cannot name a next action still blocks;
541
+ // the missing action is recorded below (R1). The decision is computed
542
+ // above so the before/after count and the exit code share one number.
537
543
  {
538
- const analyzedFiles = result.metadata?.analyzedFiles;
539
- // The `changed` command is always scoped, so the runner records the
540
- // analyzed file list. If it is somehow absent we still derive the gate
541
- // from the files that actually produced findings rather than falling
542
- // back to the baseline gate — a silent soft-fail is the one outcome R1
543
- // forbids.
544
- const gateFiles = analyzedFiles && analyzedFiles.length > 0
545
- ? analyzedFiles
546
- : [...new Set(violations.map((v) => v.file).filter(Boolean))];
547
- const diffGate = computeDiffGate(options.path, gateFiles);
548
- const { blocking, resolutionGaps } = computeDiffGatingDecision(violations, diffGate);
549
544
  if (resolutionGaps.length > 0) {
550
- // Spec 36 R6 — a gating rule that could not name an action is a defect
551
- // in the rule. Record it loudly so it cannot route around as a count.
545
+ // Spec 45 R1 — a rule that could not name an action is a defect in the
546
+ // rule. Record it loudly so it cannot route around as a count.
552
547
  const lines = [
553
- '⚠️ resolution gap — gating rule produced no next action for these occurrences (Spec 36 R6):',
548
+ '⚠️ resolution gap — rule produced no next action for these occurrences (Spec 45 R1):',
554
549
  ...resolutionGaps.map((g) => ` - ${g.rule} @ ${g.file}${g.line ? `:${g.line}` : ''}`),
555
550
  ];
556
551
  process.stderr.write(lines.join('\n') + '\n');
@@ -558,28 +553,6 @@ program
558
553
  if (blocking.length > 0) {
559
554
  process.exit(2);
560
555
  }
561
- // Spec 36 R7 — an unnecessary or reasonless suppression is itself an
562
- // error. It is reported loudly and fails the write, exactly like an
563
- // unused `@ts-expect-error`. A suppression that outlives its finding is
564
- // a baseline entry with better branding unless it errors here.
565
- const suppressions = result.metadata?.suppressions;
566
- if (suppressions && (suppressions.unnecessary.length > 0 || suppressions.reasonless.length > 0)) {
567
- const lines = [];
568
- if (suppressions.reasonless.length > 0) {
569
- lines.push('❌ suppression missing a required reason (Spec 36 R7):');
570
- for (const d of suppressions.reasonless) {
571
- lines.push(` - ${d.rule} @ ${d.file}:${d.line}`);
572
- }
573
- }
574
- if (suppressions.unnecessary.length > 0) {
575
- lines.push('❌ unnecessary suppression — the finding no longer fires (Spec 36 R7):');
576
- for (const d of suppressions.unnecessary) {
577
- lines.push(` - ${d.rule} @ ${d.file}:${d.line}`);
578
- }
579
- }
580
- process.stderr.write(lines.join('\n') + '\n');
581
- process.exit(2);
582
- }
583
556
  }
584
557
  // Exit code based on --no-fail-on-zero-files (default-on in v3.4.8)
585
558
  // Any enabled analyzer matching zero source files is a failure — a >50%
@@ -599,6 +572,38 @@ program
599
572
  process.exit(1);
600
573
  }
601
574
  });
575
+ // Spec 45 A2 — counts are emitted where useful (per analyzer, per rule, per
576
+ // severity), on agent-facing surfaces as well as human ones. This reverts Spec
577
+ // 36 R3's "no bare counts": an agent now sees the shape of what fired instead
578
+ // of a single aggregate it could route around. The `before/after` gate figure
579
+ // is Spec 45 R1's "report count before/after".
580
+ function summarizeFindings(violations) {
581
+ const bySeverity = new Map();
582
+ const byAnalyzer = new Map();
583
+ const byRule = new Map();
584
+ for (const v of violations) {
585
+ const severity = v.severity ?? 'unknown';
586
+ bySeverity.set(severity, (bySeverity.get(severity) ?? 0) + 1);
587
+ const analyzer = v.analyzer ?? 'unknown';
588
+ byAnalyzer.set(analyzer, (byAnalyzer.get(analyzer) ?? 0) + 1);
589
+ const rule = v.rule || v.type || 'unknown';
590
+ byRule.set(rule, (byRule.get(rule) ?? 0) + 1);
591
+ }
592
+ return { bySeverity, byAnalyzer, byRule };
593
+ }
594
+ function groupedCounts(map, sep = ', ') {
595
+ return [...map.entries()]
596
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
597
+ .map(([k, n]) => `${k} ${n}`)
598
+ .join(sep);
599
+ }
600
+ function printCountSummary(violations) {
601
+ const { bySeverity, byAnalyzer, byRule } = summarizeFindings(violations);
602
+ console.log(chalk.gray('── Findings ────────────────────────────────────'));
603
+ console.log(`${violations.length} total · ${groupedCounts(bySeverity, ' · ')}`);
604
+ console.log(`by analyzer: ${groupedCounts(byAnalyzer)}`);
605
+ console.log(`by rule: ${groupedCounts(byRule)}`);
606
+ }
602
607
  // Self-audit gate (Spec 33 Item 15 + Spec 44 remediation). Runs the full
603
608
  // analyzer pipeline over the tool's own production source and asserts zero
604
609
  // *blocking* (critical/warning) findings in the self-audit scope — the same
@@ -630,7 +635,7 @@ function isSelfAuditInScope(file) {
630
635
  }
631
636
  program
632
637
  .command('self-audit [paths...]')
633
- .description('Audit the tool\'s own analyzers/ + languages/ source and fail on blocking findings')
638
+ .description('Audit the tool\'s own analyzers/ + languages/ source and fail on blocking readings')
634
639
  .option('--json', 'Output violations as machine-readable JSON to stdout')
635
640
  .option('--stdin', 'Read file paths from stdin (one per line)')
636
641
  .option('-p, --path <projectPath>', 'Project root path', process.cwd())
@@ -710,8 +715,9 @@ program
710
715
  process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
711
716
  }
712
717
  else if (blocking.length > 0) {
713
- // Spec 36 R3: emit findings only, never an aggregate total.
714
- console.log(chalk.red(`\n${blocking.length} self-audit blocking finding(s):`));
718
+ // Spec 45 A2 counts plus findings (per analyzer/rule/severity).
719
+ console.log(chalk.red('\nSelf-audit blocking readings:'));
720
+ printCountSummary(blocking);
715
721
  for (const v of blocking) {
716
722
  const icon = v.severity === 'critical' ? '🔴' :
717
723
  v.severity === 'warning' ? '🟡' : '🔵';
@@ -719,7 +725,7 @@ program
719
725
  }
720
726
  }
721
727
  else {
722
- console.log(chalk.green('\n✓ Self-audit clean — zero blocking findings in scope.'));
728
+ console.log(chalk.green('\n✓ Self-audit clean — zero blocking readings in scope.'));
723
729
  }
724
730
  if (blocking.length > 0)
725
731
  process.exit(2);
@@ -740,7 +746,7 @@ program
740
746
  // skipping the file.
741
747
  program
742
748
  .command('next-file')
743
- .description('Audit and return the highest-priority file with findings (file-by-file refactor loop)')
749
+ .description('Audit and return the highest-priority file with readings (file-by-file refactor loop)')
744
750
  .option('-p, --path <projectPath>', 'Project root path', process.cwd())
745
751
  .option('-c, --config <config>', 'Configuration name')
746
752
  .option('--json', 'Output a single JSON object (or {done:true}) to stdout')
@@ -758,7 +764,7 @@ program
758
764
  process.stdout.write(JSON.stringify({ done: true, summary }, null, 2) + '\n');
759
765
  }
760
766
  else {
761
- console.log(chalk.green('\n✓ No findings — nothing left to refactor.'));
767
+ console.log(chalk.green('\n✓ No readings — nothing left to refactor.'));
762
768
  }
763
769
  return;
764
770
  }
@@ -804,9 +810,9 @@ program
804
810
  else {
805
811
  console.log(chalk.blue('🔍 Next File to Refactor'));
806
812
  console.log(chalk.gray('══════════════════════════════════════════════════'));
807
- console.log(`\n${chalk.bold(relativize(top.file))} — ${top.count} finding(s), highest severity ${top.maxSeverity}`);
808
- console.log(chalk.gray(`${ranked.length - 1} more file(s) with findings · ${violations.length - top.count} remaining finding(s)`));
809
- console.log(chalk.gray('\n── Findings ────────────────────────────────────────'));
813
+ console.log(`\n${chalk.bold(relativize(top.file))} — ${top.count} reading(s), highest severity ${top.maxSeverity}`);
814
+ console.log(chalk.gray(`${ranked.length - 1} more file(s) with readings · ${violations.length - top.count} remaining reading(s)`));
815
+ console.log(chalk.gray('\n── Readings ────────────────────────────────────────'));
810
816
  for (const v of ordered) {
811
817
  const icon = v.severity === 'critical' ? '🔴' : v.severity === 'warning' ? '🟡' : '🔵';
812
818
  console.log(`${icon} ${chalk.bold(relativize(v.file || ''))}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.rule} — ${v.message}`);
@@ -821,7 +827,7 @@ program
821
827
  // Baseline command (Spec 18 R1)
822
828
  program
823
829
  .command('baseline')
824
- .description('Snapshot current advisory findings as the baseline (excludes invariants)')
830
+ .description('Snapshot current advisory readings as the baseline (excludes invariants)')
825
831
  .option('-p, --path <path>', 'Project path', process.cwd())
826
832
  .option('--json', 'Output as JSON')
827
833
  .action(async (options) => {
@@ -872,7 +878,7 @@ program
872
878
  const absorbed = diff?.absorbed ?? newBaseline.entries.length;
873
879
  const fixed = diff?.fixed ?? 0;
874
880
  const totalKnown = newBaseline.entries.length;
875
- console.log(chalk.green(`\n✓ Baseline updated: ${absorbed} finding${absorbed !== 1 ? 's' : ''} absorbed, ${fixed} fixed, ${totalKnown} total known.`));
881
+ console.log(chalk.green(`\n✓ Baseline updated: ${absorbed} reading${absorbed !== 1 ? 's' : ''} absorbed, ${fixed} fixed, ${totalKnown} total known.`));
876
882
  console.log(chalk.gray('Invariants excluded (they always enforce).'));
877
883
  if (totalKnown > 0) {
878
884
  console.log(chalk.gray(`\nRun ${chalk.cyan('code-audit')} to see your delta view.`));
@@ -2038,7 +2044,7 @@ ledgerCmd
2038
2044
  });
2039
2045
  ledgerCmd
2040
2046
  .command('trends')
2041
- .description('Compare consecutive full-audit runs and report new vs fixed findings per rule')
2047
+ .description('Compare consecutive full-audit runs and report new vs fixed readings per rule')
2042
2048
  .option('--since <runId>', 'Only consider runs after this run ID')
2043
2049
  .option('--json', 'Output as JSON')
2044
2050
  .action(async (options) => {
@@ -2956,9 +2962,9 @@ program
2956
2962
  // tracked count, no state that can drift from the code.
2957
2963
  program
2958
2964
  .command('outstanding')
2959
- .description('List outstanding findings, re-derived from the codebase (never a stored list)')
2965
+ .description('List outstanding readings, re-derived from the codebase (never a stored list)')
2960
2966
  .option('-p, --path <path>', 'Path to audit', process.cwd())
2961
- .option('--json', 'Output findings as machine-readable JSON')
2967
+ .option('--json', 'Output readings as machine-readable JSON')
2962
2968
  .action(async (options) => {
2963
2969
  try {
2964
2970
  await initParsers();
@@ -3000,20 +3006,18 @@ program
3000
3006
  line: v.line ?? v.start?.line,
3001
3007
  message: v.message,
3002
3008
  resolution: v.resolution ?? null,
3003
- suppressed: v.suppressed === true,
3004
- ...(v.suppressionReason && { suppressionReason: v.suppressionReason }),
3005
3009
  };
3006
3010
  });
3007
3011
  process.stdout.write(JSON.stringify(out, null, 2) + '\n');
3008
3012
  return;
3009
3013
  }
3010
- // Spec 36 R3 — findings, never a total. Each line is a file:line finding;
3011
- // there is no count anywhere in this output to route around.
3014
+ // Spec 45 A2counts plus findings (per analyzer/rule/severity).
3015
+ printCountSummary(sorted);
3016
+ console.log('');
3012
3017
  for (const v of sorted) {
3013
3018
  const icon = v.severity === 'critical' ? '🔴' :
3014
3019
  v.severity === 'warning' ? '🟡' : '🔵';
3015
- const suppressed = v.suppressed ? chalk.dim(' [suppressed]') : '';
3016
- console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.rule || v.severity}] ${v.message}${suppressed}`);
3020
+ console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.rule || v.severity}] ${v.message}`);
3017
3021
  }
3018
3022
  }
3019
3023
  catch (error) {
@@ -3184,7 +3188,7 @@ async function generateConfigurations(options) {
3184
3188
  console.log(chalk.blue('\nNext steps:'));
3185
3189
  console.log(chalk.gray(' 1. Edit .codeauditor.json to match your codebase conventions'));
3186
3190
  console.log(chalk.gray(' 2. Run ') + chalk.cyan('code-audit') + chalk.gray(' to enforce your rules'));
3187
- console.log(chalk.gray(' 3. Use ') + chalk.cyan('code-audit changed') + chalk.gray(' in your agent hook (gating rules block on new findings)'));
3191
+ console.log(chalk.gray(' 3. Use ') + chalk.cyan('code-audit changed') + chalk.gray(' in your agent hook (gating rules block on new readings)'));
3188
3192
  }
3189
3193
  /**
3190
3194
  * Interactive rule builder — walks the user through adding rules one at a time.