code-auditor-mcp 3.6.0 → 3.8.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 (153) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +73 -0
  3. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  4. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +12 -1
  5. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  6. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  7. package/dist/analyzers/cross-language/SchemaValidator.js +122 -43
  8. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  9. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +2 -2
  10. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +6 -7
  11. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  12. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  13. package/dist/analyzers/ruleRegistry.js +152 -20
  14. package/dist/analyzers/ruleRegistry.js.map +1 -1
  15. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +38 -1
  16. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  17. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +437 -48
  18. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  19. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +6 -0
  20. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  21. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +117 -35
  22. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  23. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  24. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +86 -15
  25. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  26. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +32 -7
  27. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  28. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +104 -23
  29. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  30. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts +55 -0
  31. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -0
  32. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js +318 -0
  33. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js.map +1 -0
  34. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -8
  35. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  36. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +54 -45
  37. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  38. package/dist/analyzers/universal/functionConcerns.d.ts +57 -0
  39. package/dist/analyzers/universal/functionConcerns.d.ts.map +1 -0
  40. package/dist/analyzers/universal/functionConcerns.js +239 -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 +13 -7
  44. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  45. package/dist/analyzers/universal/schema/jsonSchema.js +124 -11
  46. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
  47. package/dist/auditRouter.d.ts.map +1 -1
  48. package/dist/auditRouter.js +4 -6
  49. package/dist/auditRouter.js.map +1 -1
  50. package/dist/auditRunner.d.ts.map +1 -1
  51. package/dist/auditRunner.js +74 -48
  52. package/dist/auditRunner.js.map +1 -1
  53. package/dist/cli.js +321 -98
  54. package/dist/cli.js.map +1 -1
  55. package/dist/config/configLoader.d.ts.map +1 -1
  56. package/dist/config/configLoader.js +23 -0
  57. package/dist/config/configLoader.js.map +1 -1
  58. package/dist/config/defaults.d.ts +6 -0
  59. package/dist/config/defaults.d.ts.map +1 -1
  60. package/dist/config/defaults.js +45 -10
  61. package/dist/config/defaults.js.map +1 -1
  62. package/dist/config/effectiveConfig.d.ts.map +1 -1
  63. package/dist/config/effectiveConfig.js +2 -0
  64. package/dist/config/effectiveConfig.js.map +1 -1
  65. package/dist/conventions/conventionMiner.d.ts +18 -3
  66. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  67. package/dist/conventions/conventionMiner.js +130 -12
  68. package/dist/conventions/conventionMiner.js.map +1 -1
  69. package/dist/daemon/core.d.ts +128 -0
  70. package/dist/daemon/core.d.ts.map +1 -0
  71. package/dist/daemon/core.js +700 -0
  72. package/dist/daemon/core.js.map +1 -0
  73. package/dist/daemon/lspServer.d.ts +51 -0
  74. package/dist/daemon/lspServer.d.ts.map +1 -0
  75. package/dist/daemon/lspServer.js +168 -0
  76. package/dist/daemon/lspServer.js.map +1 -0
  77. package/dist/daemon/main.d.ts +34 -0
  78. package/dist/daemon/main.d.ts.map +1 -0
  79. package/dist/daemon/main.js +147 -0
  80. package/dist/daemon/main.js.map +1 -0
  81. package/dist/daemon/resolve.d.ts +41 -0
  82. package/dist/daemon/resolve.d.ts.map +1 -0
  83. package/dist/daemon/resolve.js +90 -0
  84. package/dist/daemon/resolve.js.map +1 -0
  85. package/dist/daemon/socketClient.d.ts +21 -0
  86. package/dist/daemon/socketClient.d.ts.map +1 -0
  87. package/dist/daemon/socketClient.js +60 -0
  88. package/dist/daemon/socketClient.js.map +1 -0
  89. package/dist/daemon/socketServer.d.ts +33 -0
  90. package/dist/daemon/socketServer.d.ts.map +1 -0
  91. package/dist/daemon/socketServer.js +106 -0
  92. package/dist/daemon/socketServer.js.map +1 -0
  93. package/dist/daemon/types.d.ts +78 -0
  94. package/dist/daemon/types.d.ts.map +1 -0
  95. package/dist/daemon/types.js +10 -0
  96. package/dist/daemon/types.js.map +1 -0
  97. package/dist/dataPaths.d.ts +21 -0
  98. package/dist/dataPaths.d.ts.map +1 -1
  99. package/dist/dataPaths.js +27 -0
  100. package/dist/dataPaths.js.map +1 -1
  101. package/dist/graph/importGraph.d.ts +11 -0
  102. package/dist/graph/importGraph.d.ts.map +1 -1
  103. package/dist/graph/importGraph.js +11 -3
  104. package/dist/graph/importGraph.js.map +1 -1
  105. package/dist/hooks/core.d.ts.map +1 -1
  106. package/dist/hooks/core.js +21 -4
  107. package/dist/hooks/core.js.map +1 -1
  108. package/dist/installer.d.ts.map +1 -1
  109. package/dist/installer.js +2 -5
  110. package/dist/installer.js.map +1 -1
  111. package/dist/languages/RuntimeManager.js +10 -0
  112. package/dist/languages/RuntimeManager.js.map +1 -1
  113. package/dist/languages/go/analyzer +0 -0
  114. package/dist/languages/go/analyzer-src/analyzer.go +166 -35
  115. package/dist/languages/go/analyzer-src/solid.go +80 -172
  116. package/dist/languages/go/analyzer-src/types.go +18 -18
  117. package/dist/ledger.d.ts +1 -1
  118. package/dist/ledger.d.ts.map +1 -1
  119. package/dist/ledger.js +3 -3
  120. package/dist/ledger.js.map +1 -1
  121. package/dist/mcp.js +1 -1
  122. package/dist/mcp.js.map +1 -1
  123. package/dist/mcpAuditJobs.js +1 -1
  124. package/dist/mcpAuditJobs.js.map +1 -1
  125. package/dist/nextFileIncremental.d.ts +9 -3
  126. package/dist/nextFileIncremental.d.ts.map +1 -1
  127. package/dist/nextFileIncremental.js +32 -6
  128. package/dist/nextFileIncremental.js.map +1 -1
  129. package/dist/pipeline.d.ts +26 -0
  130. package/dist/pipeline.d.ts.map +1 -1
  131. package/dist/pipeline.js +86 -10
  132. package/dist/pipeline.js.map +1 -1
  133. package/dist/pipelineAdapters.d.ts +2 -1
  134. package/dist/pipelineAdapters.d.ts.map +1 -1
  135. package/dist/pipelineAdapters.js +228 -5
  136. package/dist/pipelineAdapters.js.map +1 -1
  137. package/dist/reporting/csvReportGenerator.js +1 -1
  138. package/dist/ruleAliases.d.ts.map +1 -1
  139. package/dist/ruleAliases.js +41 -2
  140. package/dist/ruleAliases.js.map +1 -1
  141. package/dist/styles/tailwindProbe.d.ts +8 -0
  142. package/dist/styles/tailwindProbe.d.ts.map +1 -1
  143. package/dist/styles/tailwindProbe.js +11 -1
  144. package/dist/styles/tailwindProbe.js.map +1 -1
  145. package/dist/types/crossLanguage.d.ts +1 -0
  146. package/dist/types/crossLanguage.d.ts.map +1 -1
  147. package/dist/types.d.ts +30 -1
  148. package/dist/types.d.ts.map +1 -1
  149. package/dist/types.js.map +1 -1
  150. package/package.json +7 -3
  151. package/plugin/.claude-plugin/plugin.json +1 -1
  152. package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +4 -2
  153. package/plugin/skills/code-auditor/SKILL.md +19 -11
package/dist/cli.js CHANGED
@@ -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);
@@ -296,7 +310,7 @@ program
296
310
  }
297
311
  }
298
312
  if (unassessed.length > 0) {
299
- console.log(chalk.gray(` ── Unassessed (zero findings, applicability unknown) ──`));
313
+ console.log(chalk.gray(` ── Unassessed (zero readings, applicability unknown) ──`));
300
314
  for (const c of unassessed) {
301
315
  console.log(` ${c.ruleId} (0)`);
302
316
  }
@@ -308,7 +322,7 @@ program
308
322
  const builtinCapped = Object.values(result.analyzerResults)
309
323
  .reduce((count, ar) => count + ar.violations.filter(v => v.profile === 'scripts-and-tests').length, 0);
310
324
  if (builtinCapped > 0) {
311
- 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).`));
312
326
  console.log(chalk.gray(` Set ${chalk.cyan('"builtin": false')} in .codeauditor.json to disable.`));
313
327
  }
314
328
  // Generate formatted report if --format is specified
@@ -333,7 +347,7 @@ program
333
347
  const currentDebt = violations.filter((v) => v.new || v.new === false).length;
334
348
  const snapshotDebt = baseline.previousKnownCount ?? 0;
335
349
  if (currentDebt > snapshotDebt) {
336
- 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.`));
337
351
  process.exit(2);
338
352
  }
339
353
  }
@@ -392,7 +406,6 @@ program
392
406
  .option('-p, --path <projectPath>', 'Project root path', process.cwd())
393
407
  .action(async (paths, options) => {
394
408
  try {
395
- await initParsers();
396
409
  const fileSet = new Set();
397
410
  // Collect paths from stdin if requested
398
411
  if (options.stdin) {
@@ -413,18 +426,43 @@ program
413
426
  }
414
427
  // Resolve scope
415
428
  let scope = 'changed';
429
+ let resolvedPaths = [];
416
430
  if (fileSet.size > 0) {
417
431
  // Convert to absolute paths
418
- const resolved = [...fileSet].map((f) => isAbsolute(f) ? f : resolve(process.cwd(), f));
419
- scope = resolved;
432
+ resolvedPaths = [...fileSet].map((f) => isAbsolute(f) ? f : resolve(process.cwd(), f));
433
+ scope = resolvedPaths;
434
+ }
435
+ // R2 — resolve to a live daemon first (fast path); fall back in-process.
436
+ // The daemon serves per-file diagnostics for exactly the changed set. It is
437
+ // only trusted when ready and none of the queried files are stale (R4): a
438
+ // just-edited file whose re-audit hasn't landed falls through in-process so
439
+ // the gate never misses a finding. SARIF needs full run metadata, so it
440
+ // always runs in-process.
441
+ const projectRoot = resolve(options.path || process.cwd());
442
+ let result;
443
+ if (resolvedPaths.length > 0 && options.format !== 'sarif') {
444
+ const { resolveDaemon, readDaemonDiagnostics } = await import('./daemon/resolve.js');
445
+ const daemon = await resolveDaemon(projectRoot);
446
+ if (daemon.mode === 'not-ready') {
447
+ reportDaemonIndexing(daemon.state, !!options.json);
448
+ return;
449
+ }
450
+ if (daemon.mode === 'ready') {
451
+ const diag = await readDaemonDiagnostics(daemon.socketPath, resolvedPaths);
452
+ if (diag && diag.status === 'ready' && diag.staleFiles.length === 0) {
453
+ result = await buildChangedResultFromDiagnostics(diag.diagnostics, projectRoot);
454
+ }
455
+ }
456
+ }
457
+ if (!result) {
458
+ await initParsers();
459
+ const runner = createAuditRunner({
460
+ projectRoot: options.path,
461
+ scope,
462
+ analyzerConcurrency: 4
463
+ });
464
+ result = await runner.run();
420
465
  }
421
- // Create runner
422
- const runner = createAuditRunner({
423
- projectRoot: options.path,
424
- scope,
425
- analyzerConcurrency: 4
426
- });
427
- const result = await runner.run();
428
466
  // Collect all violations
429
467
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
430
468
  // Blocking gate decision (Spec 45 R1/R2/R4), computed once so the
@@ -459,7 +497,7 @@ program
459
497
  const col = v.column ?? v.start?.column ?? 1;
460
498
  return {
461
499
  analyzer: v.analyzer || '',
462
- rule: v.rule || v.type || '',
500
+ rule: v.rule,
463
501
  severity: v.severity,
464
502
  message: v.message,
465
503
  file: filePath,
@@ -497,7 +535,7 @@ program
497
535
  }
498
536
  }
499
537
  else {
500
- console.log(chalk.green('\n✓ No findings.'));
538
+ console.log(chalk.green('\n✓ No readings.'));
501
539
  }
502
540
  }
503
541
  // Spec 38 R2/R3 — per-rule timing + gate wall-clock, opt-in by env var.
@@ -572,7 +610,7 @@ function summarizeFindings(violations) {
572
610
  bySeverity.set(severity, (bySeverity.get(severity) ?? 0) + 1);
573
611
  const analyzer = v.analyzer ?? 'unknown';
574
612
  byAnalyzer.set(analyzer, (byAnalyzer.get(analyzer) ?? 0) + 1);
575
- const rule = v.rule || v.type || 'unknown';
613
+ const rule = v.rule;
576
614
  byRule.set(rule, (byRule.get(rule) ?? 0) + 1);
577
615
  }
578
616
  return { bySeverity, byAnalyzer, byRule };
@@ -590,6 +628,32 @@ function printCountSummary(violations) {
590
628
  console.log(`by analyzer: ${groupedCounts(byAnalyzer)}`);
591
629
  console.log(`by rule: ${groupedCounts(byRule)}`);
592
630
  }
631
+ /**
632
+ * Reconstruct a `changed`-compatible result from daemon-served diagnostics so the
633
+ * gate/output logic below stays identical to the in-process path. Gate severities
634
+ * are read from the project config (one JSON file, no parsing) so a custom
635
+ * `gateSeverities` never diverges from what the in-process run would have used.
636
+ */
637
+ async function buildChangedResultFromDiagnostics(violations, projectRoot) {
638
+ const analyzerResults = {};
639
+ for (const v of violations) {
640
+ const key = v.analyzer || 'unknown';
641
+ (analyzerResults[key] = analyzerResults[key] || { violations: [] }).violations.push(v);
642
+ }
643
+ let configUsed;
644
+ try {
645
+ const { findConfigFileUp, loadConfig } = await import('./config/configLoader.js');
646
+ const configPath = await findConfigFileUp(projectRoot);
647
+ const config = await loadConfig({ configPath: configPath ?? undefined });
648
+ if (Array.isArray(config.gateSeverities) && config.gateSeverities.length > 0) {
649
+ configUsed = { gateSeverities: config.gateSeverities };
650
+ }
651
+ }
652
+ catch {
653
+ // No config — the gate falls back to DEFAULT_BLOCKING_SEVERITIES, matching in-process.
654
+ }
655
+ return { analyzerResults, metadata: { configUsed, diagnostics: [] } };
656
+ }
593
657
  // Self-audit gate (Spec 33 Item 15 + Spec 44 remediation). Runs the full
594
658
  // analyzer pipeline over the tool's own production source and asserts zero
595
659
  // *blocking* (critical/warning) findings in the self-audit scope — the same
@@ -621,7 +685,7 @@ function isSelfAuditInScope(file) {
621
685
  }
622
686
  program
623
687
  .command('self-audit [paths...]')
624
- .description('Audit the tool\'s own analyzers/ + languages/ source and fail on blocking findings')
688
+ .description('Audit the tool\'s own analyzers/ + languages/ source and fail on blocking readings')
625
689
  .option('--json', 'Output violations as machine-readable JSON to stdout')
626
690
  .option('--stdin', 'Read file paths from stdin (one per line)')
627
691
  .option('-p, --path <projectPath>', 'Project root path', process.cwd())
@@ -685,7 +749,7 @@ program
685
749
  }
686
750
  return {
687
751
  analyzer: v.analyzer || '',
688
- rule: v.rule || v.type || '',
752
+ rule: v.rule,
689
753
  severity: v.severity,
690
754
  message: v.message,
691
755
  file: filePath,
@@ -702,7 +766,7 @@ program
702
766
  }
703
767
  else if (blocking.length > 0) {
704
768
  // Spec 45 A2 — counts plus findings (per analyzer/rule/severity).
705
- console.log(chalk.red('\nSelf-audit blocking findings:'));
769
+ console.log(chalk.red('\nSelf-audit blocking readings:'));
706
770
  printCountSummary(blocking);
707
771
  for (const v of blocking) {
708
772
  const icon = v.severity === 'critical' ? '🔴' :
@@ -711,7 +775,7 @@ program
711
775
  }
712
776
  }
713
777
  else {
714
- console.log(chalk.green('\n✓ Self-audit clean — zero blocking findings in scope.'));
778
+ console.log(chalk.green('\n✓ Self-audit clean — zero blocking readings in scope.'));
715
779
  }
716
780
  if (blocking.length > 0)
717
781
  process.exit(2);
@@ -732,88 +796,110 @@ program
732
796
  // skipping the file.
733
797
  program
734
798
  .command('next-file')
735
- .description('Audit and return the highest-priority file with findings (file-by-file refactor loop)')
799
+ .description('Audit and return the highest-priority file with readings (file-by-file refactor loop)')
736
800
  .option('-p, --path <projectPath>', 'Project root path', process.cwd())
737
801
  .option('-c, --config <config>', 'Configuration name')
738
802
  .option('--json', 'Output a single JSON object (or {done:true}) to stdout')
739
803
  .action(async (options) => {
740
804
  try {
805
+ const projectDir = resolve(options.path || process.cwd());
806
+ // R2 — resolve to a live daemon first; fall back in-process when absent.
807
+ // The daemon is an optimisation: a ready daemon serves the current ranking
808
+ // without re-auditing, and an indexing daemon reports its status (R3).
809
+ const { resolveDaemon, readDaemonFindings } = await import('./daemon/resolve.js');
810
+ const daemon = await resolveDaemon(projectDir);
811
+ if (daemon.mode === 'not-ready') {
812
+ reportDaemonIndexing(daemon.state, !!options.json);
813
+ return;
814
+ }
815
+ if (daemon.mode === 'ready') {
816
+ const findings = await readDaemonFindings(daemon.socketPath);
817
+ if (findings && findings.status === 'ready' && findings.staleFiles.length === 0) {
818
+ const { summarizeViolations } = await import('./nextFileIncremental.js');
819
+ printNextFile(findings.violations, summarizeViolations(findings.violations), projectDir, !!options.json);
820
+ return;
821
+ }
822
+ // Daemon fell behind (R4) or vanished — fall through to in-process.
823
+ }
741
824
  await initParsers();
742
825
  const { violations, summary } = await runNextFile({
743
826
  projectRoot: options.path,
744
827
  configName: options.config,
745
828
  });
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
- }
829
+ printNextFile(violations, summary, projectDir, !!options.json);
807
830
  }
808
831
  catch (error) {
809
832
  console.error(chalk.red('Error:'), error);
810
833
  process.exit(1);
811
834
  }
812
835
  });
836
+ /** Rank the violation set and emit the `next-file` result (shared by both paths). */
837
+ function printNextFile(violations, summary, projectDir, json) {
838
+ // Rank files worst-first from the (incrementally-merged) violation set.
839
+ const ranked = rankFilesByPriority(violations);
840
+ if (ranked.length === 0) {
841
+ if (json) {
842
+ process.stdout.write(JSON.stringify({ done: true, summary }, null, 2) + '\n');
843
+ }
844
+ else {
845
+ console.log(chalk.green('\n✓ No readings — nothing left to refactor.'));
846
+ }
847
+ return;
848
+ }
849
+ const top = ranked[0];
850
+ // Every issue on the file, ordered critical → warning → suggestion.
851
+ const ordered = orderFindingsWithinFile(top.violations);
852
+ const relativize = (filePath) => {
853
+ if (!filePath)
854
+ return '';
855
+ if (filePath.startsWith('/') || filePath.startsWith('\\\\')) {
856
+ const rel = relative(projectDir, filePath);
857
+ if (!rel.startsWith('..') && !isAbsolute(rel))
858
+ return rel;
859
+ }
860
+ return filePath;
861
+ };
862
+ if (json) {
863
+ const output = {
864
+ done: false,
865
+ file: relativize(top.file),
866
+ remainingFiles: ranked.length - 1,
867
+ remainingFindings: violations.length - top.count,
868
+ summary,
869
+ findings: ordered.map((v) => ({
870
+ analyzer: v.analyzer || '',
871
+ rule: v.rule,
872
+ severity: v.severity,
873
+ message: v.message,
874
+ file: relativize(v.file || ''),
875
+ line: v.line ?? v.start?.line,
876
+ column: v.column ?? v.start?.column ?? 1,
877
+ endLine: v.end?.line,
878
+ endColumn: v.end?.column,
879
+ enclosingSymbol: v.symbol || v.enclosingFunction || '',
880
+ suggestion: v.suggestion || '',
881
+ details: v.details || '',
882
+ ...(v.new !== undefined && { new: v.new }),
883
+ })),
884
+ };
885
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
886
+ }
887
+ else {
888
+ console.log(chalk.blue('🔍 Next File to Refactor'));
889
+ console.log(chalk.gray('══════════════════════════════════════════════════'));
890
+ console.log(`\n${chalk.bold(relativize(top.file))} — ${top.count} reading(s), highest severity ${top.maxSeverity}`);
891
+ console.log(chalk.gray(`${ranked.length - 1} more file(s) with readings · ${violations.length - top.count} remaining reading(s)`));
892
+ console.log(chalk.gray('\n── Readings ────────────────────────────────────────'));
893
+ for (const v of ordered) {
894
+ const icon = v.severity === 'critical' ? '🔴' : v.severity === 'warning' ? '🟡' : '🔵';
895
+ console.log(`${icon} ${chalk.bold(relativize(v.file || ''))}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.rule} — ${v.message}`);
896
+ }
897
+ }
898
+ }
813
899
  // Baseline command (Spec 18 R1)
814
900
  program
815
901
  .command('baseline')
816
- .description('Snapshot current advisory findings as the baseline (excludes invariants)')
902
+ .description('Snapshot current advisory readings as the baseline (excludes invariants)')
817
903
  .option('-p, --path <path>', 'Project path', process.cwd())
818
904
  .option('--json', 'Output as JSON')
819
905
  .action(async (options) => {
@@ -864,7 +950,7 @@ program
864
950
  const absorbed = diff?.absorbed ?? newBaseline.entries.length;
865
951
  const fixed = diff?.fixed ?? 0;
866
952
  const totalKnown = newBaseline.entries.length;
867
- console.log(chalk.green(`\n✓ Baseline updated: ${absorbed} finding${absorbed !== 1 ? 's' : ''} absorbed, ${fixed} fixed, ${totalKnown} total known.`));
953
+ console.log(chalk.green(`\n✓ Baseline updated: ${absorbed} reading${absorbed !== 1 ? 's' : ''} absorbed, ${fixed} fixed, ${totalKnown} total known.`));
868
954
  console.log(chalk.gray('Invariants excluded (they always enforce).'));
869
955
  if (totalKnown > 0) {
870
956
  console.log(chalk.gray(`\nRun ${chalk.cyan('code-audit')} to see your delta view.`));
@@ -2030,7 +2116,7 @@ ledgerCmd
2030
2116
  });
2031
2117
  ledgerCmd
2032
2118
  .command('trends')
2033
- .description('Compare consecutive full-audit runs and report new vs fixed findings per rule')
2119
+ .description('Compare consecutive full-audit runs and report new vs fixed readings per rule')
2034
2120
  .option('--since <runId>', 'Only consider runs after this run ID')
2035
2121
  .option('--json', 'Output as JSON')
2036
2122
  .action(async (options) => {
@@ -2948,9 +3034,9 @@ program
2948
3034
  // tracked count, no state that can drift from the code.
2949
3035
  program
2950
3036
  .command('outstanding')
2951
- .description('List outstanding findings, re-derived from the codebase (never a stored list)')
3037
+ .description('List outstanding readings, re-derived from the codebase (never a stored list)')
2952
3038
  .option('-p, --path <path>', 'Path to audit', process.cwd())
2953
- .option('--json', 'Output findings as machine-readable JSON')
3039
+ .option('--json', 'Output readings as machine-readable JSON')
2954
3040
  .action(async (options) => {
2955
3041
  try {
2956
3042
  await initParsers();
@@ -2985,7 +3071,7 @@ program
2985
3071
  filePath = rel;
2986
3072
  }
2987
3073
  return {
2988
- rule: v.rule || v.type || '',
3074
+ rule: v.rule,
2989
3075
  analyzer: v.analyzer || '',
2990
3076
  severity: v.severity,
2991
3077
  file: filePath,
@@ -3020,6 +3106,143 @@ function renderBar(value, width) {
3020
3106
  const color = value > 0.8 ? chalk.green : value > 0.5 ? chalk.yellow : chalk.red;
3021
3107
  return color('█'.repeat(filled) + '░'.repeat(empty));
3022
3108
  }
3109
+ // ── Daemon command (Spec 50) ────────────────────────────────────────────
3110
+ // The daemon is an optimisation, never a dependency (R2). These commands start,
3111
+ // stop, and inspect it. The hot-path reads — `changed` (the hook gate) and
3112
+ // `next-file` (the refactor loop) — resolve to a live daemon and fall back
3113
+ // in-process when it is absent. `audit` stays in-process on purpose: it emits
3114
+ // full reports (baseline, recommendations, per-category SARIF/HTML) that need
3115
+ // the complete analyzer results, which the daemon does not serve over its
3116
+ // findings face (it serves the flattened violation set).
3117
+ const DAEMON_NOT_READY_EXIT_CODE = 3;
3118
+ /** R3 — a query while the seed is still running: the status is the answer. */
3119
+ function reportDaemonIndexing(state, json) {
3120
+ const progress = state?.progress;
3121
+ const filesIndexed = progress?.filesIndexed ?? 0;
3122
+ const filesTotal = progress?.filesTotal ?? 0;
3123
+ const phase = progress?.phase ?? 'files';
3124
+ const retryAfterMs = state?.retryAfterMs ?? null;
3125
+ const throughputUnknown = state?.throughputUnknown ?? false;
3126
+ if (json) {
3127
+ process.stdout.write(JSON.stringify({
3128
+ status: 'indexing',
3129
+ progress: { filesIndexed, filesTotal, phase },
3130
+ retryAfterMs,
3131
+ throughputUnknown,
3132
+ }, null, 2) + '\n');
3133
+ }
3134
+ else {
3135
+ const retry = retryAfterMs != null
3136
+ ? `try again in ~${Math.max(1, Math.round(retryAfterMs / 1000))}s`
3137
+ : 'check back shortly';
3138
+ const phaseDetail = phase === 'files'
3139
+ ? `${filesIndexed} of ${filesTotal} files`
3140
+ : `${phase} phase`;
3141
+ console.log(chalk.yellow(`⏳ Daemon indexing — ${phaseDetail}; ${retry}.`));
3142
+ }
3143
+ process.exit(DAEMON_NOT_READY_EXIT_CODE);
3144
+ }
3145
+ const daemonCmd = program
3146
+ .command('daemon')
3147
+ .description('Manage the per-project code-auditor daemon (Spec 50)')
3148
+ .action(() => {
3149
+ console.log(chalk.yellow('Use a daemon subcommand:'));
3150
+ console.log(chalk.gray(' code-audit daemon start [--foreground] Start the daemon for this project'));
3151
+ console.log(chalk.gray(' code-audit daemon stop Ask the daemon to shut down'));
3152
+ console.log(chalk.gray(' code-audit daemon status Show daemon status'));
3153
+ });
3154
+ daemonCmd
3155
+ .command('start')
3156
+ .description('Start the daemon for this project (detached by default)')
3157
+ .option('-p, --path <path>', 'Project root', process.cwd())
3158
+ .option('-c, --config <config>', 'Configuration name')
3159
+ .option('--foreground', 'Run in the foreground (blocking; useful for debugging)')
3160
+ .action(async (options) => {
3161
+ try {
3162
+ const { resolveDaemonSocketPath } = await import('./dataPaths.js');
3163
+ const { isDaemonListening } = await import('./daemon/socketClient.js');
3164
+ const projectRoot = resolve(options.path);
3165
+ const socketPath = resolveDaemonSocketPath(projectRoot);
3166
+ if (await isDaemonListening(socketPath)) {
3167
+ console.log(chalk.green('✓ Daemon already running.'));
3168
+ return;
3169
+ }
3170
+ if (options.foreground) {
3171
+ const { runDaemonForeground } = await import('./daemon/main.js');
3172
+ await runDaemonForeground({ projectRoot, configName: options.config, foreground: true });
3173
+ return;
3174
+ }
3175
+ const { startDaemonDetached } = await import('./daemon/resolve.js');
3176
+ startDaemonDetached(projectRoot, options.config);
3177
+ console.log(chalk.green('✓ Daemon started (detached).'));
3178
+ console.log(chalk.gray(` Socket: ${socketPath}`));
3179
+ console.log(chalk.gray(' It indexes on startup — `code-audit daemon status` to watch.'));
3180
+ }
3181
+ catch (error) {
3182
+ console.error(chalk.red('Error:'), error);
3183
+ process.exit(1);
3184
+ }
3185
+ });
3186
+ daemonCmd
3187
+ .command('stop')
3188
+ .description('Ask the daemon to shut down')
3189
+ .option('-p, --path <path>', 'Project root', process.cwd())
3190
+ .action(async (options) => {
3191
+ try {
3192
+ const { resolveDaemonSocketPath } = await import('./dataPaths.js');
3193
+ const { isDaemonListening } = await import('./daemon/socketClient.js');
3194
+ const { requestDaemonShutdown } = await import('./daemon/resolve.js');
3195
+ const socketPath = resolveDaemonSocketPath(resolve(options.path));
3196
+ if (!(await isDaemonListening(socketPath))) {
3197
+ console.log(chalk.gray('No daemon running.'));
3198
+ return;
3199
+ }
3200
+ await requestDaemonShutdown(socketPath);
3201
+ console.log(chalk.green('✓ Shutdown requested.'));
3202
+ }
3203
+ catch (error) {
3204
+ console.error(chalk.red('Error:'), error);
3205
+ process.exit(1);
3206
+ }
3207
+ });
3208
+ daemonCmd
3209
+ .command('status')
3210
+ .description('Show daemon status')
3211
+ .option('-p, --path <path>', 'Project root', process.cwd())
3212
+ .option('--json', 'Output as JSON')
3213
+ .action(async (options) => {
3214
+ try {
3215
+ const { resolveDaemon } = await import('./daemon/resolve.js');
3216
+ const { resolveDaemonSocketPath } = await import('./dataPaths.js');
3217
+ const projectRoot = resolve(options.path);
3218
+ const resolved = await resolveDaemon(projectRoot);
3219
+ const socketPath = resolveDaemonSocketPath(projectRoot);
3220
+ if (options.json) {
3221
+ process.stdout.write(JSON.stringify({
3222
+ running: resolved.mode !== 'absent',
3223
+ mode: resolved.mode,
3224
+ socketPath,
3225
+ ...(resolved.state && { state: resolved.state }),
3226
+ }, null, 2) + '\n');
3227
+ return;
3228
+ }
3229
+ if (resolved.mode === 'absent') {
3230
+ console.log(chalk.gray('No daemon running.'));
3231
+ console.log(chalk.gray('Start one: code-audit daemon start'));
3232
+ return;
3233
+ }
3234
+ if (resolved.mode === 'not-ready') {
3235
+ reportDaemonIndexing(resolved.state, false);
3236
+ return;
3237
+ }
3238
+ console.log(chalk.green('✓ Daemon ready.'));
3239
+ console.log(chalk.gray(` Socket: ${socketPath}`));
3240
+ }
3241
+ catch (error) {
3242
+ console.error(chalk.red('Error:'), error);
3243
+ process.exit(1);
3244
+ }
3245
+ });
3023
3246
  // Parse command line arguments
3024
3247
  program.parse(process.argv);
3025
3248
  // If no command was provided, show help
@@ -3174,7 +3397,7 @@ async function generateConfigurations(options) {
3174
3397
  console.log(chalk.blue('\nNext steps:'));
3175
3398
  console.log(chalk.gray(' 1. Edit .codeauditor.json to match your codebase conventions'));
3176
3399
  console.log(chalk.gray(' 2. Run ') + chalk.cyan('code-audit') + chalk.gray(' to enforce your rules'));
3177
- console.log(chalk.gray(' 3. Use ') + chalk.cyan('code-audit changed') + chalk.gray(' in your agent hook (gating rules block on new findings)'));
3400
+ console.log(chalk.gray(' 3. Use ') + chalk.cyan('code-audit changed') + chalk.gray(' in your agent hook (gating rules block on new readings)'));
3178
3401
  }
3179
3402
  /**
3180
3403
  * Interactive rule builder — walks the user through adding rules one at a time.