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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +96 -0
- package/dist/analyzers/applicability.d.ts +41 -0
- package/dist/analyzers/applicability.d.ts.map +1 -0
- package/dist/analyzers/applicability.js +96 -0
- package/dist/analyzers/applicability.js.map +1 -0
- package/dist/analyzers/ruleRegistry.js +11 -11
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +8 -8
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +2 -2
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +2 -2
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +9 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/migrations.d.ts +11 -0
- package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/migrations.js +127 -4
- package/dist/analyzers/universal/schema/migrations.js.map +1 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +62 -23
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +393 -1
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts +40 -1
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +222 -50
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/config/defaults.js +2 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/pathProfiles.d.ts +7 -0
- package/dist/config/pathProfiles.d.ts.map +1 -1
- package/dist/config/pathProfiles.js +7 -1
- package/dist/config/pathProfiles.js.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/ledger.d.ts +163 -2
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +528 -7
- package/dist/ledger.js.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts +24 -2
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +364 -58
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/pipeline.d.ts +9 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +165 -18
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +45 -10
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +6 -1
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/ruleAliases.d.ts.map +1 -1
- package/dist/ruleAliases.js +23 -0
- package/dist/ruleAliases.js.map +1 -1
- package/dist/scripts/benchmarkWorkers.js +1 -1
- package/dist/scripts/benchmarkWorkers.js.map +1 -1
- package/dist/services/auditJobService.d.ts +22 -5
- package/dist/services/auditJobService.d.ts.map +1 -1
- package/dist/services/auditJobService.js +67 -26
- package/dist/services/auditJobService.js.map +1 -1
- package/dist/services/fileAccounting.d.ts +83 -0
- package/dist/services/fileAccounting.d.ts.map +1 -0
- package/dist/services/fileAccounting.js +0 -0
- package/dist/services/fileAccounting.js.map +1 -0
- package/dist/styles/styleExtractor.d.ts +2 -2
- package/dist/styles/styleExtractor.d.ts.map +1 -1
- package/dist/styles/styleExtractor.js +83 -11
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts +14 -0
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +117 -16
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindProbe.d.ts +31 -0
- package/dist/styles/tailwindProbe.d.ts.map +1 -1
- package/dist/styles/tailwindProbe.js +145 -4
- package/dist/styles/tailwindProbe.js.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts +3 -0
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js +35 -1
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/styles/types.d.ts +11 -0
- package/dist/styles/types.d.ts.map +1 -1
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +71 -0
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +201 -25
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/workers/auditJobRunner.d.ts +2 -0
- package/dist/workers/auditJobRunner.d.ts.map +1 -0
- package/dist/workers/auditJobRunner.js +37 -0
- package/dist/workers/auditJobRunner.js.map +1 -0
- package/dist/workers/auditWorker.js +26 -0
- package/dist/workers/auditWorker.js.map +1 -1
- package/package.json +2 -2
- 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
|
-
|
|
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')
|