code-auditor-mcp 3.4.13 → 3.4.15

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 (105) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +96 -0
  3. package/dist/analyzers/applicability.d.ts +41 -0
  4. package/dist/analyzers/applicability.d.ts.map +1 -0
  5. package/dist/analyzers/applicability.js +96 -0
  6. package/dist/analyzers/applicability.js.map +1 -0
  7. package/dist/analyzers/ruleRegistry.js +11 -11
  8. package/dist/analyzers/ruleRegistry.js.map +1 -1
  9. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +8 -8
  10. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  11. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +2 -2
  12. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  13. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +2 -2
  14. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  15. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  16. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +9 -1
  17. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  18. package/dist/analyzers/universal/schema/migrations.d.ts +11 -0
  19. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -1
  20. package/dist/analyzers/universal/schema/migrations.js +127 -4
  21. package/dist/analyzers/universal/schema/migrations.js.map +1 -1
  22. package/dist/auditRunner.d.ts.map +1 -1
  23. package/dist/auditRunner.js +62 -23
  24. package/dist/auditRunner.js.map +1 -1
  25. package/dist/cli.js +393 -1
  26. package/dist/cli.js.map +1 -1
  27. package/dist/codeIndexDB.d.ts +40 -1
  28. package/dist/codeIndexDB.d.ts.map +1 -1
  29. package/dist/codeIndexDB.js +222 -50
  30. package/dist/codeIndexDB.js.map +1 -1
  31. package/dist/config/defaults.js +2 -2
  32. package/dist/config/defaults.js.map +1 -1
  33. package/dist/config/pathProfiles.d.ts +7 -0
  34. package/dist/config/pathProfiles.d.ts.map +1 -1
  35. package/dist/config/pathProfiles.js +7 -1
  36. package/dist/config/pathProfiles.js.map +1 -1
  37. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  38. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1 -1
  39. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  40. package/dist/ledger.d.ts +163 -2
  41. package/dist/ledger.d.ts.map +1 -1
  42. package/dist/ledger.js +528 -7
  43. package/dist/ledger.js.map +1 -1
  44. package/dist/mcp.js +3 -3
  45. package/dist/mcp.js.map +1 -1
  46. package/dist/mcpAuditJobs.d.ts +24 -2
  47. package/dist/mcpAuditJobs.d.ts.map +1 -1
  48. package/dist/mcpAuditJobs.js +364 -58
  49. package/dist/mcpAuditJobs.js.map +1 -1
  50. package/dist/pipeline.d.ts +9 -2
  51. package/dist/pipeline.d.ts.map +1 -1
  52. package/dist/pipeline.js +165 -18
  53. package/dist/pipeline.js.map +1 -1
  54. package/dist/pipelineAdapters.d.ts.map +1 -1
  55. package/dist/pipelineAdapters.js +45 -10
  56. package/dist/pipelineAdapters.js.map +1 -1
  57. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  58. package/dist/reporting/sarifReportGenerator.js +6 -1
  59. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  60. package/dist/ruleAliases.d.ts.map +1 -1
  61. package/dist/ruleAliases.js +23 -0
  62. package/dist/ruleAliases.js.map +1 -1
  63. package/dist/scripts/benchmarkWorkers.js +1 -1
  64. package/dist/scripts/benchmarkWorkers.js.map +1 -1
  65. package/dist/services/auditJobService.d.ts +22 -5
  66. package/dist/services/auditJobService.d.ts.map +1 -1
  67. package/dist/services/auditJobService.js +67 -26
  68. package/dist/services/auditJobService.js.map +1 -1
  69. package/dist/services/fileAccounting.d.ts +83 -0
  70. package/dist/services/fileAccounting.d.ts.map +1 -0
  71. package/dist/services/fileAccounting.js +0 -0
  72. package/dist/services/fileAccounting.js.map +1 -0
  73. package/dist/styles/styleExtractor.d.ts +2 -2
  74. package/dist/styles/styleExtractor.d.ts.map +1 -1
  75. package/dist/styles/styleExtractor.js +83 -11
  76. package/dist/styles/styleExtractor.js.map +1 -1
  77. package/dist/styles/styleIndexer.d.ts +14 -0
  78. package/dist/styles/styleIndexer.d.ts.map +1 -1
  79. package/dist/styles/styleIndexer.js +117 -16
  80. package/dist/styles/styleIndexer.js.map +1 -1
  81. package/dist/styles/tailwindProbe.d.ts +31 -0
  82. package/dist/styles/tailwindProbe.d.ts.map +1 -1
  83. package/dist/styles/tailwindProbe.js +145 -4
  84. package/dist/styles/tailwindProbe.js.map +1 -1
  85. package/dist/styles/tailwindUtilityExpander.d.ts +3 -0
  86. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  87. package/dist/styles/tailwindUtilityExpander.js +35 -1
  88. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  89. package/dist/styles/types.d.ts +11 -0
  90. package/dist/styles/types.d.ts.map +1 -1
  91. package/dist/types.d.ts +130 -0
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/types.js.map +1 -1
  94. package/dist/utils/fileDiscovery.d.ts +71 -0
  95. package/dist/utils/fileDiscovery.d.ts.map +1 -1
  96. package/dist/utils/fileDiscovery.js +201 -25
  97. package/dist/utils/fileDiscovery.js.map +1 -1
  98. package/dist/workers/auditJobRunner.d.ts +2 -0
  99. package/dist/workers/auditJobRunner.d.ts.map +1 -0
  100. package/dist/workers/auditJobRunner.js +37 -0
  101. package/dist/workers/auditJobRunner.js.map +1 -0
  102. package/dist/workers/auditWorker.js +26 -0
  103. package/dist/workers/auditWorker.js.map +1 -1
  104. package/package.json +2 -2
  105. package/plugin/.claude-plugin/plugin.json +1 -1
package/dist/cli.js CHANGED
@@ -27,6 +27,40 @@ const __filename = fileURLToPath(import.meta.url);
27
27
  const __dirname = dirname(__filename);
28
28
  const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
29
29
  const program = new Command();
30
+ // Spec 44 R4 — file accounting summary line + `--explain-skipped` breakdown.
31
+ // The full per-reason file lists live in the JSON report (`metadata.fileAccounting`);
32
+ // the CLI preview is bounded to the first ~20 files per reason.
33
+ function printFileAccounting(result, explainSkipped) {
34
+ const fa = result?.metadata?.fileAccounting;
35
+ if (!fa)
36
+ return;
37
+ const reasons = fa.reasons ?? {};
38
+ const reasonNames = Object.keys(reasons);
39
+ const reasonCount = reasonNames.length;
40
+ const partialSeg = fa.partiallyAnalyzed > 0
41
+ ? ` · ${fa.partiallyAnalyzed.toLocaleString()} partially analyzed`
42
+ : '';
43
+ console.log(chalk.gray(`${fa.analyzed.toLocaleString()} analyzed` +
44
+ `${partialSeg}` +
45
+ ` · ${fa.dropped.toLocaleString()} dropped` +
46
+ ` (${reasonCount} reason${reasonCount !== 1 ? 's' : ''})`));
47
+ if (!explainSkipped || reasonCount === 0)
48
+ return;
49
+ console.log(chalk.gray('\n── Skipped files (by reason) ────────────────'));
50
+ for (const name of reasonNames.sort((a, b) => reasons[b].count - reasons[a].count)) {
51
+ const { count, files } = reasons[name];
52
+ const partialCount = files.filter((f) => f.partial).length;
53
+ const label = partialCount > 0
54
+ ? `${name}: ${count.toLocaleString()} (${partialCount.toLocaleString()} partially analyzed)`
55
+ : `${name}: ${count.toLocaleString()}`;
56
+ console.log(` ${label}`);
57
+ for (const f of files.slice(0, 20)) {
58
+ console.log(` ${f.filePath}${f.partial ? ' (partially analyzed)' : ''}`);
59
+ }
60
+ if (files.length > 20)
61
+ console.log(` … and ${files.length - 20} more`);
62
+ }
63
+ }
30
64
  // Configure the main program
31
65
  program
32
66
  .name('code-auditor')
@@ -45,9 +79,20 @@ program
45
79
  .option('--include-baseline', 'Evaluate baseline-known violations in --fail-on checks')
46
80
  .option('--fail-on-regression', 'Exit code 2 when total advisory debt exceeds the baseline snapshot')
47
81
  .option('--preset <id>', 'Apply a shareable preset (repeatable)', (v, prev) => prev.concat([v]), [])
82
+ .option('--detach', 'Run the audit in a detached background process and print the job ID')
83
+ .option('--partition-strategy <strategy>', 'Partition strategy for detached runs: none, auto, or top-level')
84
+ .option('--max-partitions <n>', 'Maximum number of partition shards (detached runs)')
85
+ .option('--shard-timeout-ms <ms>', 'Per-shard timeout in milliseconds (detached runs)')
86
+ .option('--explain-skipped', 'Print a per-reason breakdown of files dropped from analysis')
48
87
  .action(async (options) => {
49
88
  console.log(chalk.blue('🔍 Code Quality Audit Tool'));
50
89
  console.log(chalk.gray('══════════════════════════════════════════════════'));
90
+ // Spec 41 — `--detach` hands off to a forked worker; the parent never parses
91
+ // or hashes, so it must route out before `initParsers()`.
92
+ if (options.detach) {
93
+ await runDetachedAudit(options);
94
+ return;
95
+ }
51
96
  try {
52
97
  await initParsers();
53
98
  // Validate --fail-on severity
@@ -150,6 +195,8 @@ program
150
195
  console.log(`Warnings: ${result.summary.warnings}`);
151
196
  console.log(`Suggestions: ${result.summary.suggestions}`);
152
197
  }
198
+ // Spec 44 R4 — file accounting: analyzed/dropped totals + optional breakdown.
199
+ printFileAccounting(result, !!options.explainSkipped);
153
200
  // Spec 32 — unparsed files are never silent. A run where any file failed
154
201
  // to parse (WASM abort, read error, …) must surface the count + reasons.
155
202
  const unparsedFiles = result.metadata?.unparsedFiles ?? [];
@@ -162,6 +209,17 @@ program
162
209
  console.error(` … and ${unparsedFiles.length - 20} more`);
163
210
  }
164
211
  }
212
+ // Spec 43 R5 follow-up — extensions present on disk that discovery skipped.
213
+ // Informational: surfaces "what isn't being analyzed here" so an unlisted
214
+ // extension (e.g. `.mdx`) never vanishes silently at discovery. Not an
215
+ // error — a skipped extension is not necessarily a defect (`.md`/`.svg`
216
+ // are legitimately not source), unlike `unparsedFiles` above.
217
+ const skippedExtensions = result.metadata?.skippedExtensions ?? [];
218
+ if (skippedExtensions.length > 0) {
219
+ console.log(chalk.gray(`\n── Not analyzed (skipped extensions) ────────`));
220
+ const parts = skippedExtensions.map(s => `${s.ext} (${s.count})`);
221
+ console.log(` ${parts.join(', ')}`);
222
+ }
165
223
  // Spec 36 R7 — suppressions are reported: how many exist, where, and how
166
224
  // many are unnecessary (or reasonless). Both are errors, surfaced here.
167
225
  const suppressions = result.metadata?.suppressions;
@@ -321,6 +379,7 @@ program
321
379
  .option('--quiet', 'Suppress output when zero violations')
322
380
  .option('--fail-on-zero-files', 'Exit code 2 when any enabled analyzer matches zero source files', true)
323
381
  .option('--stdin', 'Read file paths from stdin (one per line)')
382
+ .option('--explain-skipped', 'Print a per-reason breakdown of files dropped from analysis')
324
383
  .option('-p, --path <projectPath>', 'Project root path', process.cwd())
325
384
  .action(async (paths, options) => {
326
385
  try {
@@ -404,7 +463,7 @@ program
404
463
  // consumer routes around. Findings only, each with file:line.
405
464
  console.log(chalk.blue('🔍 Diff-Scoped Code Audit'));
406
465
  console.log(chalk.gray('══════════════════════════════════════════════════'));
407
- console.log(chalk.gray(`Files analyzed: ${result.metadata.filesAnalyzed}`));
466
+ printFileAccounting(result, !!options.explainSkipped);
408
467
  if (violations.length > 0) {
409
468
  console.log(chalk.gray('\n── Violations ────────────────────────────────────'));
410
469
  for (const v of violations) {
@@ -1792,6 +1851,339 @@ ledgerCmd
1792
1851
  process.exit(1);
1793
1852
  }
1794
1853
  });
1854
+ // ── Spec 41 — Detached runs & queryable findings (CLI) ──────────────────────
1855
+ // `audit --detach` forks workers/auditJobRunner; `status`/`result`/`jobs` read
1856
+ // the persisted ledger. One job model (the ledger run), one write path — these
1857
+ // are thin wrappers over ledger.ts + mcpAuditJobs.ts, not a second store.
1858
+ /** Full analyzer set — matches createAuditRunner's registry, so `--detach` produces
1859
+ * the same per-rule coverage as a synchronous `audit` (73 rows on recall). */
1860
+ const DETACHED_DEFAULT_ANALYZERS = [
1861
+ 'solid',
1862
+ 'dry',
1863
+ 'data-access',
1864
+ 'react',
1865
+ 'documentation',
1866
+ 'invariants',
1867
+ 'schema',
1868
+ 'styles',
1869
+ 'conventions',
1870
+ 'cross-domain',
1871
+ ];
1872
+ /** Lease TTL used by read-path reclaim, mirroring mcpAuditJobs.jobLeaseTtlMs. */
1873
+ const CLI_JOB_LEASE_TTL_MS = Number(process.env.CODE_AUDITOR_JOB_LEASE_TTL_MS) || 30_000;
1874
+ function spec41StatusBadge(status) {
1875
+ switch (status) {
1876
+ case 'completed':
1877
+ return chalk.green('completed');
1878
+ case 'running':
1879
+ return chalk.cyan('running');
1880
+ case 'queued':
1881
+ return chalk.blue('queued');
1882
+ case 'failed':
1883
+ return chalk.red('failed');
1884
+ default:
1885
+ return status;
1886
+ }
1887
+ }
1888
+ function spec41SeverityColor(severity) {
1889
+ if (severity === 'critical')
1890
+ return chalk.red;
1891
+ if (severity === 'warning')
1892
+ return chalk.yellow;
1893
+ return chalk.gray;
1894
+ }
1895
+ /**
1896
+ * Render a run's staleness: the cheap marker (changed count) plus, when stale,
1897
+ * the per-file diff. The cheap stat-only pass in `computeStaleness` populates
1898
+ * `changedFiles` on divergence, so this list is the full `--stale` deliverable
1899
+ * regardless of whether the caller requested `full`. Bounded so a mass-change
1900
+ * doesn't flood the terminal; the JSON path always carries the complete list.
1901
+ */
1902
+ function spec41RenderStaleness(staleness) {
1903
+ if (!staleness) {
1904
+ console.log(chalk.gray(' stale: n/a'));
1905
+ return;
1906
+ }
1907
+ if (!staleness.stale) {
1908
+ console.log(chalk.gray(' stale: no (working tree unchanged)'));
1909
+ return;
1910
+ }
1911
+ console.log(chalk.yellow(` stale: ${staleness.changedFiles.length} file(s) changed since run`));
1912
+ const maxFiles = 20;
1913
+ for (const f of staleness.changedFiles.slice(0, maxFiles)) {
1914
+ console.log(chalk.yellow(` + ${f}`));
1915
+ }
1916
+ if (staleness.changedFiles.length > maxFiles) {
1917
+ console.log(chalk.gray(` … and ${staleness.changedFiles.length - maxFiles} more`));
1918
+ }
1919
+ }
1920
+ /**
1921
+ * Spec 41 — `audit --detach`. Persists a `queued` run, opens a per-run log as
1922
+ * the child's stderr, and forks workers/auditJobRunner detached. Returns with
1923
+ * the job id; the parent never parses or hashes, so it stays sub-second even
1924
+ * on a minutes-long corpus.
1925
+ */
1926
+ async function runDetachedAudit(options) {
1927
+ // `spawn` + no IPC channel: `fork()` establishes an IPC channel, and `unref()`
1928
+ // does NOT let the parent exit while that channel is open (Node: "unless there
1929
+ // is an established IPC channel"). The runner talks to the ledger through the
1930
+ // DB, never back to the parent, so there is no channel to keep — and without
1931
+ // one the parent exits immediately, keeping `--detach` sub-second.
1932
+ const { spawn } = await import('node:child_process');
1933
+ const { openSync } = await import('node:fs');
1934
+ const { createAuditJob } = await import('./services/auditJobService.js');
1935
+ const { patchLedgerRun } = await import('./ledger.js');
1936
+ const { resolveJobRunnerEntrypoint } = await import('./mcpAuditJobs.js');
1937
+ const { assertAuditPathExists } = await import('./mcpToolErrors.js');
1938
+ const { getPersistedStorageRoot } = await import('./dataPaths.js');
1939
+ const auditPath = resolve(options.path || process.cwd());
1940
+ const { isFile } = await assertAuditPathExists(auditPath);
1941
+ const projectRoot = isFile ? dirname(auditPath) : auditPath;
1942
+ const db = CodeIndexDB.getInstance(undefined, projectRoot);
1943
+ await db.initialize();
1944
+ const job = createAuditJob(db.rawDb, projectRoot, { surface: 'cli', command: 'audit --detach' });
1945
+ // The log is the diagnosable trace when the child dies before its first DB
1946
+ // patch (e.g. inside initParsers()). Lives next to the index db.
1947
+ const logsDir = join(getPersistedStorageRoot(projectRoot), 'logs');
1948
+ await fs.mkdir(logsDir, { recursive: true });
1949
+ const logPath = join(logsDir, `${job.jobId}.log`);
1950
+ const logFd = openSync(logPath, 'a');
1951
+ const argsJson = JSON.stringify({
1952
+ path: auditPath,
1953
+ ...(options.partitionStrategy && { partitionStrategy: options.partitionStrategy }),
1954
+ ...(options.maxPartitions && { maxPartitions: Number(options.maxPartitions) }),
1955
+ ...(options.shardTimeoutMs && { shardTimeoutMs: Number(options.shardTimeoutMs) }),
1956
+ });
1957
+ const defaultsJson = JSON.stringify({
1958
+ defaultAnalyzers: DETACHED_DEFAULT_ANALYZERS,
1959
+ defaultMinSeverity: 'warning',
1960
+ defaultGenerateCodeMap: false,
1961
+ });
1962
+ patchLedgerRun(db.rawDb, job.jobId, { stderrLog: logPath });
1963
+ const child = spawn(process.execPath, [resolveJobRunnerEntrypoint(), job.jobId, argsJson, defaultsJson], {
1964
+ detached: true,
1965
+ stdio: ['ignore', 'ignore', logFd],
1966
+ env: process.env,
1967
+ });
1968
+ child.unref();
1969
+ console.log(chalk.green(`✓ Audit detached (job ${job.jobId})`));
1970
+ console.log(chalk.gray(` Monitor: code-auditor status ${job.jobId}`));
1971
+ console.log(chalk.gray(` Results: code-auditor result ${job.jobId}`));
1972
+ console.log(chalk.gray(` Log: ${logPath}`));
1973
+ }
1974
+ program
1975
+ .command('status <jobId>')
1976
+ .description('Show a detached audit job status, progress, and staleness')
1977
+ .option('-p, --path <path>', 'Project root containing the job (default: cwd)', process.cwd())
1978
+ .option('--json', 'Output as JSON')
1979
+ .action(async (jobId, options) => {
1980
+ try {
1981
+ const { getLedgerRun, reclaimStaleRunning, computeStaleness } = await import('./ledger.js');
1982
+ const projectRoot = resolve(options.path);
1983
+ const db = CodeIndexDB.getInstance(undefined, projectRoot);
1984
+ await db.initialize();
1985
+ reclaimStaleRunning(db.rawDb, projectRoot, CLI_JOB_LEASE_TTL_MS);
1986
+ const run = getLedgerRun(db.rawDb, jobId);
1987
+ if (!run) {
1988
+ console.error(chalk.red(`Job not found: ${jobId}`));
1989
+ process.exit(1);
1990
+ }
1991
+ const staleness = run.projectRoot ? computeStaleness(db.rawDb, jobId, run.projectRoot) : null;
1992
+ if (options.json) {
1993
+ process.stdout.write(JSON.stringify({ ...run, staleness }, null, 2) + '\n');
1994
+ return;
1995
+ }
1996
+ let progress = '';
1997
+ if (run.progressJson) {
1998
+ try {
1999
+ const p = JSON.parse(run.progressJson);
2000
+ progress = [p.phase, p.message].filter(Boolean).join(' — ');
2001
+ if (p.total != null)
2002
+ progress += ` (${p.current ?? 0}/${p.total})`;
2003
+ }
2004
+ catch {
2005
+ progress = '';
2006
+ }
2007
+ }
2008
+ console.log(chalk.blue(`Job ${jobId}`));
2009
+ console.log(` status: ${spec41StatusBadge(run.status)}`);
2010
+ console.log(` path: ${run.projectRoot ?? run.target}`);
2011
+ console.log(` created: ${run.timestamp}`);
2012
+ if (run.startedAt)
2013
+ console.log(` started: ${run.startedAt}`);
2014
+ if (run.finishedAt)
2015
+ console.log(` finished: ${run.finishedAt}`);
2016
+ if (run.durationMs)
2017
+ console.log(` duration: ${run.durationMs}ms`);
2018
+ if (progress)
2019
+ console.log(` progress: ${progress}`);
2020
+ if (run.error)
2021
+ console.log(chalk.red(` error: ${run.error}`));
2022
+ if (run.stderrLog)
2023
+ console.log(chalk.gray(` log: ${run.stderrLog}`));
2024
+ if (run.contentHash)
2025
+ console.log(chalk.gray(` content: ${run.contentHash.slice(0, 12)} (${run.filesCount} files)`));
2026
+ spec41RenderStaleness(staleness);
2027
+ }
2028
+ catch (error) {
2029
+ console.error(chalk.red('Error:'), error);
2030
+ process.exit(1);
2031
+ }
2032
+ });
2033
+ program
2034
+ .command('result <jobId>')
2035
+ .description('Query a detached audit run: findings or per-rule coverage')
2036
+ .option('-p, --path <path>', 'Project root containing the job (default: cwd)', process.cwd())
2037
+ .option('--rule <rule>', 'Filter by rule id')
2038
+ .option('--analyzer <analyzer>', 'Filter by analyzer')
2039
+ .option('--file <file>', 'Filter by file path (substring match)')
2040
+ .option('--severity <severity>', 'Filter by severity (critical|warning|suggestion)')
2041
+ .option('--state [state]', 'Query coverage by state (fired|clean|notApplicable|unassessed); omit value for all')
2042
+ .option('--count', 'Group findings by analyzer/rule with counts')
2043
+ .option('--limit <n>', 'Max findings to return (0 = unbounded)', '50')
2044
+ .option('--offset <n>', 'Findings offset', '0')
2045
+ .option('--stale', 'Force full content re-hash for staleness')
2046
+ .option('--json', 'Output as JSON')
2047
+ .action(async (jobId, options) => {
2048
+ try {
2049
+ const { getLedgerRun, queryLedgerFindings, queryLedgerCoverage, computeStaleness } = await import('./ledger.js');
2050
+ const projectRoot = resolve(options.path);
2051
+ const db = CodeIndexDB.getInstance(undefined, projectRoot);
2052
+ await db.initialize();
2053
+ const run = getLedgerRun(db.rawDb, jobId);
2054
+ if (!run) {
2055
+ console.error(chalk.red(`Job not found: ${jobId}`));
2056
+ process.exit(1);
2057
+ }
2058
+ const staleness = run.projectRoot
2059
+ ? computeStaleness(db.rawDb, jobId, run.projectRoot, { full: !!options.stale })
2060
+ : null;
2061
+ // A finding read must never be silent about the tree it was computed
2062
+ // against (Spec 41 R3): carry the staleness marker in human output too.
2063
+ if (!options.json)
2064
+ spec41RenderStaleness(staleness);
2065
+ if (options.state !== undefined) {
2066
+ const stateFilter = typeof options.state === 'string' ? options.state : undefined;
2067
+ const rows = queryLedgerCoverage(db.rawDb, jobId, {
2068
+ ...(stateFilter ? { state: stateFilter } : {}),
2069
+ ...(options.rule ? { rule: options.rule } : {}),
2070
+ ...(options.analyzer ? { analyzer: options.analyzer } : {}),
2071
+ });
2072
+ if (options.json) {
2073
+ process.stdout.write(JSON.stringify({ runId: jobId, staleness, coverage: rows }, null, 2) + '\n');
2074
+ }
2075
+ else {
2076
+ console.log(chalk.blue(stateFilter ? `Coverage — ${stateFilter}` : 'Coverage — all states'));
2077
+ for (const r of rows) {
2078
+ const reason = r.reason ? chalk.gray(` (${r.reason})`) : '';
2079
+ console.log(` ${r.analyzer}/${r.ruleId}: ${r.state} × ${r.count}${reason}`);
2080
+ }
2081
+ if (rows.length === 0)
2082
+ console.log(chalk.gray(' (no rows)'));
2083
+ }
2084
+ return;
2085
+ }
2086
+ const findingFilter = {
2087
+ ...(options.rule ? { rule: options.rule } : {}),
2088
+ ...(options.analyzer ? { analyzer: options.analyzer } : {}),
2089
+ ...(options.file ? { file: options.file } : {}),
2090
+ ...(options.severity ? { severity: options.severity } : {}),
2091
+ };
2092
+ if (options.count) {
2093
+ const groups = queryLedgerFindings(db.rawDb, jobId, { ...findingFilter, count: true });
2094
+ if (options.json) {
2095
+ process.stdout.write(JSON.stringify({ runId: jobId, staleness, groups }, null, 2) + '\n');
2096
+ }
2097
+ else {
2098
+ console.log(chalk.blue('Findings by analyzer/rule'));
2099
+ for (const g of groups)
2100
+ console.log(` ${g.count}\t${g.group}`);
2101
+ if (groups.length === 0)
2102
+ console.log(chalk.gray(' (no findings)'));
2103
+ }
2104
+ return;
2105
+ }
2106
+ const limit = options.limit === '0' ? 0 : parseInt(options.limit, 10) || 50;
2107
+ const offset = parseInt(options.offset, 10) || 0;
2108
+ const findings = queryLedgerFindings(db.rawDb, jobId, { ...findingFilter, limit, offset });
2109
+ if (options.json) {
2110
+ const nextOffset = limit > 0 && findings.length === limit ? offset + limit : null;
2111
+ process.stdout.write(JSON.stringify({ runId: jobId, staleness, count: findings.length, nextOffset, findings }, null, 2) + '\n');
2112
+ }
2113
+ else {
2114
+ console.log(chalk.blue(`Findings (${findings.length} shown)`));
2115
+ for (const f of findings) {
2116
+ const loc = f.line != null ? `${f.file}:${f.line}` : f.file;
2117
+ const paint = spec41SeverityColor(f.severity);
2118
+ console.log(` ${paint(f.severity)} ${chalk.gray(loc)} [${f.analyzer}/${f.rule}] ${f.message}`);
2119
+ }
2120
+ if (findings.length === 0)
2121
+ console.log(chalk.gray(' (no findings)'));
2122
+ if (limit > 0 && findings.length === limit) {
2123
+ console.log(chalk.gray(` next offset: ${offset + limit} (use --offset ${offset + limit})`));
2124
+ }
2125
+ }
2126
+ }
2127
+ catch (error) {
2128
+ console.error(chalk.red('Error:'), error);
2129
+ process.exit(1);
2130
+ }
2131
+ });
2132
+ program
2133
+ .command('jobs')
2134
+ .description('List detached audit jobs with staleness badges')
2135
+ .option('-p, --path <path>', 'Project root (default: cwd)', process.cwd())
2136
+ .option('--prune', 'Delete all but the newest runs for this project')
2137
+ .option('--keep <n>', 'Runs to keep when --prune is set', '10')
2138
+ .option('--limit <n>', 'Runs to display (staleness computed for these)', '20')
2139
+ .option('--json', 'Output as JSON')
2140
+ .action(async (options) => {
2141
+ try {
2142
+ const { listLedgerRuns, reclaimStaleRunning, pruneLedgerRuns, computeStaleness } = await import('./ledger.js');
2143
+ const projectRoot = resolve(options.path);
2144
+ const db = CodeIndexDB.getInstance(undefined, projectRoot);
2145
+ await db.initialize();
2146
+ reclaimStaleRunning(db.rawDb, projectRoot, CLI_JOB_LEASE_TTL_MS);
2147
+ if (options.prune) {
2148
+ const keep = parseInt(options.keep, 10) || 10;
2149
+ const n = pruneLedgerRuns(db.rawDb, projectRoot, keep);
2150
+ console.log(chalk.green(`✓ Pruned ${n} run(s) — keeping ${keep} newest`));
2151
+ }
2152
+ const limit = parseInt(options.limit, 10) || 20;
2153
+ const runs = listLedgerRuns(db.rawDb, projectRoot).slice(0, limit);
2154
+ const withStaleness = runs.map((run) => {
2155
+ let staleness = null;
2156
+ if (run.projectRoot && run.contentHash) {
2157
+ staleness = computeStaleness(db.rawDb, run.runId, run.projectRoot);
2158
+ }
2159
+ return { ...run, staleness };
2160
+ });
2161
+ if (options.json) {
2162
+ process.stdout.write(JSON.stringify(withStaleness, null, 2) + '\n');
2163
+ return;
2164
+ }
2165
+ if (withStaleness.length === 0) {
2166
+ console.log(chalk.gray('No jobs yet — run `code-auditor audit --detach` first.'));
2167
+ return;
2168
+ }
2169
+ console.log(chalk.blue(`Jobs (${withStaleness.length} newest)`));
2170
+ for (const run of withStaleness) {
2171
+ const badge = run.staleness?.stale
2172
+ ? chalk.yellow('stale')
2173
+ : run.staleness
2174
+ ? chalk.green('current')
2175
+ : chalk.gray('n/a');
2176
+ console.log(` ${spec41StatusBadge(run.status)} ${chalk.cyan(run.runId)} [${badge}]`);
2177
+ console.log(` ${run.projectRoot ?? run.target} ${run.timestamp}`);
2178
+ if (run.error)
2179
+ console.log(` ${chalk.red(run.error)}`);
2180
+ }
2181
+ }
2182
+ catch (error) {
2183
+ console.error(chalk.red('Error:'), error);
2184
+ process.exit(1);
2185
+ }
2186
+ });
1795
2187
  // ── Conventions command — Spec 12 R3 ────────────────────────────────────────
1796
2188
  const conventionsCmd = program
1797
2189
  .command('conventions')