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