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