code-auditor-mcp 3.9.6 → 3.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +106 -0
  3. package/README.md +18 -0
  4. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  5. package/dist/analyzers/reactAnalyzer.js +4 -1
  6. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  7. package/dist/analyzers/ruleRegistry.d.ts +8 -0
  8. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  9. package/dist/analyzers/ruleRegistry.js +20 -10
  10. package/dist/analyzers/ruleRegistry.js.map +1 -1
  11. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  12. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +23 -20
  13. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  14. package/dist/auditRouter.d.ts.map +1 -1
  15. package/dist/auditRouter.js +6 -1
  16. package/dist/auditRouter.js.map +1 -1
  17. package/dist/auditRunner.d.ts.map +1 -1
  18. package/dist/auditRunner.js +53 -1
  19. package/dist/auditRunner.js.map +1 -1
  20. package/dist/cli.js +119 -4
  21. package/dist/cli.js.map +1 -1
  22. package/dist/config/defaults.d.ts +1 -0
  23. package/dist/config/defaults.d.ts.map +1 -1
  24. package/dist/config/defaults.js +14 -6
  25. package/dist/config/defaults.js.map +1 -1
  26. package/dist/config/effectiveConfig.d.ts +22 -1
  27. package/dist/config/effectiveConfig.d.ts.map +1 -1
  28. package/dist/config/effectiveConfig.js +69 -2
  29. package/dist/config/effectiveConfig.js.map +1 -1
  30. package/dist/config/lintConfigReader.d.ts +56 -0
  31. package/dist/config/lintConfigReader.d.ts.map +1 -0
  32. package/dist/config/lintConfigReader.js +263 -0
  33. package/dist/config/lintConfigReader.js.map +1 -0
  34. package/dist/dataPaths.d.ts +8 -0
  35. package/dist/dataPaths.d.ts.map +1 -1
  36. package/dist/dataPaths.js +21 -0
  37. package/dist/dataPaths.js.map +1 -1
  38. package/dist/dismissals.d.ts +59 -0
  39. package/dist/dismissals.d.ts.map +1 -0
  40. package/dist/dismissals.js +128 -0
  41. package/dist/dismissals.js.map +1 -0
  42. package/dist/enforcement/gate.d.ts.map +1 -1
  43. package/dist/enforcement/gate.js +5 -0
  44. package/dist/enforcement/gate.js.map +1 -1
  45. package/dist/installConfig.d.ts +40 -0
  46. package/dist/installConfig.d.ts.map +1 -0
  47. package/dist/installConfig.js +84 -0
  48. package/dist/installConfig.js.map +1 -0
  49. package/dist/languages/go/analyzer +0 -0
  50. package/dist/languages/go/analyzer-src/solid.go +14 -6
  51. package/dist/mcp.js +54 -0
  52. package/dist/mcp.js.map +1 -1
  53. package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
  54. package/dist/reporting/csvReportGenerator.js +1 -0
  55. package/dist/reporting/csvReportGenerator.js.map +1 -1
  56. package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
  57. package/dist/reporting/htmlReportGenerator.js +5 -0
  58. package/dist/reporting/htmlReportGenerator.js.map +1 -1
  59. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  60. package/dist/reporting/jsonReportGenerator.js +6 -0
  61. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  62. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  63. package/dist/reporting/sarifReportGenerator.js +3 -0
  64. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  65. package/dist/structuralSignature.d.ts +41 -0
  66. package/dist/structuralSignature.d.ts.map +1 -0
  67. package/dist/structuralSignature.js +93 -0
  68. package/dist/structuralSignature.js.map +1 -0
  69. package/dist/telemetry.d.ts +80 -0
  70. package/dist/telemetry.d.ts.map +1 -0
  71. package/dist/telemetry.js +148 -0
  72. package/dist/telemetry.js.map +1 -0
  73. package/dist/types.d.ts +34 -0
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js.map +1 -1
  76. package/package.json +1 -1
  77. package/plugin/.claude-plugin/plugin.json +1 -1
  78. package/plugin/skills/code-auditor/SKILL.md +24 -2
  79. package/server.json +2 -2
package/dist/cli.js CHANGED
@@ -20,6 +20,10 @@ import { queryParser } from './search/QueryParser.js';
20
20
  import { CodeIndexDB } from './codeIndexDB.js';
21
21
  import { getFilesProcessed, getFactsConsumed, isVisitorStatus, isReducerStatus } from './pipeline.js';
22
22
  import { createBaselineFromFindings, saveBaseline, loadBaseline, diffBaselines } from './baseline.js';
23
+ import { fingerprint, buildFingerprintInput } from './fingerprint.js';
24
+ import { buildDismissalEntry, upsertDismissal } from './dismissals.js';
25
+ import { resolveTelemetryConfig, signatureForFinding, buildTelemetryPayload, formatTelemetryPreview, sendTelemetry, languageHint } from './telemetry.js';
26
+ import { getInstallId } from './installConfig.js';
23
27
  import { ALL_ANALYZERS } from './analyzers/ruleRegistry.js';
24
28
  import { computeGatingDecision } from './enforcement/gate.js';
25
29
  import { BLOCKING_SEVERITIES } from './types.js';
@@ -139,6 +143,10 @@ program
139
143
  });
140
144
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
141
145
  const baseline = result.metadata?.baseline;
146
+ // Spec 57 — dismissed count is reported alongside the total, never
147
+ // subtracted from it ("43 findings, 3 dismissed").
148
+ const dismissedCount = result.summary.dismissed ?? 0;
149
+ const dismissedSuffix = dismissedCount > 0 ? `, ${dismissedCount} dismissed` : '';
142
150
  // ── Coverage panel leads the report (Spec 47 R2) ─────────────
143
151
  // A diagnostic report opens with what was measured before it lists any
144
152
  // readings, so a zero-reading report can't be mistaken for a clean tree.
@@ -215,7 +223,7 @@ program
215
223
  }
216
224
  else if (!baseline) {
217
225
  // No baseline: current behavior + hint
218
- console.log(`\nFound ${result.summary.totalViolations} readings`);
226
+ console.log(`\nFound ${result.summary.totalViolations} findings${dismissedSuffix}`);
219
227
  console.log(`Critical: ${result.summary.criticalIssues}`);
220
228
  console.log(`Severe: ${result.summary.severe}`);
221
229
  console.log(`High: ${result.summary.high}`);
@@ -224,7 +232,7 @@ program
224
232
  }
225
233
  else {
226
234
  // --full with baseline: full itemized inventory (current behavior)
227
- console.log(`\nFound ${result.summary.totalViolations} readings`);
235
+ console.log(`\nFound ${result.summary.totalViolations} findings${dismissedSuffix}`);
228
236
  console.log(`Critical: ${result.summary.criticalIssues}`);
229
237
  console.log(`Severe: ${result.summary.severe}`);
230
238
  console.log(`High: ${result.summary.high}`);
@@ -381,6 +389,9 @@ program
381
389
  const severityOrder = ['critical', 'severe', 'high'];
382
390
  const failIndex = severityOrder.indexOf(failOnSeverity);
383
391
  const hasAtOrAbove = evaluableViolations.some((v) => {
392
+ // Spec 57 — a dismissed finding never blocks the gate.
393
+ if (v.dismissed)
394
+ return false;
384
395
  const vIndex = severityOrder.indexOf(v.severity);
385
396
  return vIndex >= 0 && vIndex <= failIndex;
386
397
  });
@@ -466,6 +477,10 @@ program
466
477
  }
467
478
  // Collect all violations
468
479
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
480
+ // Spec 57 — dismissed count is reported alongside the total, never
481
+ // subtracted from it. The hook surface (`changed`) is agent-facing, so it
482
+ // must show the same "N findings, M dismissed" figure as the full report.
483
+ const dismissedCount = result.summary.dismissed ?? 0;
469
484
  // Blocking gate decision (Spec 45 R1/R4, Spec 54 R3), computed once so the
470
485
  // agent-facing before/after count (Spec 45 A2) and the exit code agree on
471
486
  // the same number. Every registered rule participates (R1); enforcement is
@@ -506,7 +521,9 @@ program
506
521
  enclosingSymbol: v.symbol || v.enclosingFunction || '',
507
522
  suggestion: v.suggestion || '',
508
523
  details: v.details || '',
509
- ...(v.new !== undefined && { new: v.new })
524
+ ...(v.new !== undefined && { new: v.new }),
525
+ ...(v.dismissed !== undefined && { dismissed: v.dismissed }),
526
+ fingerprint: fingerprint(buildFingerprintInput(v))
510
527
  };
511
528
  });
512
529
  process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
@@ -522,6 +539,9 @@ program
522
539
  console.log('');
523
540
  printCountSummary(violations);
524
541
  console.log(chalk.bold(`gate before/after: ${violations.length} → ${blocking.length} blocking`));
542
+ if (dismissedCount > 0) {
543
+ console.log(chalk.dim(` ${dismissedCount} dismissed — still counted above, excluded from the gate`));
544
+ }
525
545
  console.log(chalk.gray('── Violations ────────────────────────────────────'));
526
546
  for (const v of violations) {
527
547
  const icon = v.severity === 'critical' ? 'šŸ”“' :
@@ -529,7 +549,10 @@ program
529
549
  const statusTag = v.new === false
530
550
  ? chalk.dim(' [known — still open]')
531
551
  : '';
532
- console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.message}${statusTag}`);
552
+ const dismissedTag = v.dismissed
553
+ ? chalk.dim(' [dismissed]')
554
+ : '';
555
+ console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.message}${statusTag}${dismissedTag}`);
533
556
  }
534
557
  }
535
558
  else {
@@ -947,6 +970,90 @@ program
947
970
  process.exit(1);
948
971
  }
949
972
  });
973
+ // Dismiss command (Spec 57)
974
+ program
975
+ .command('dismiss <fingerprint>')
976
+ .description('Dismiss one finding by fingerprint — a written reason is required')
977
+ .requiredOption('--reason <reason>', 'Why this finding is dismissed (required — a dismissal without a reason is a config error)')
978
+ .option('-p, --path <path>', 'Project path', process.cwd())
979
+ .option('--telemetry-endpoint <url>', 'Feedback service endpoint override (defaults to the opt-in endpoint recorded by the telemetry tool)')
980
+ .option('--json', 'Output as JSON')
981
+ .action(async (fingerprintArg, options) => {
982
+ try {
983
+ const reason = (options.reason ?? '').trim();
984
+ if (!reason) {
985
+ console.error(chalk.red('Error: --reason is required and must be non-empty. A dismissal without a reason is a config error, not a suppression.'));
986
+ process.exit(1);
987
+ }
988
+ const projectRoot = resolve(options.path || process.cwd());
989
+ await initParsers();
990
+ // Re-audit so the fingerprint resolves against the live finding. The
991
+ // fingerprint is a content hash of rule+file+symbol, so it is stable
992
+ // across runs on the same machine (same tuple the baseline uses).
993
+ const runner = createAuditRunner({ projectRoot });
994
+ const result = await runner.run();
995
+ const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
996
+ const match = violations.find((v) => fingerprint(buildFingerprintInput(v)) === fingerprintArg);
997
+ if (!match) {
998
+ console.error(chalk.red(`Error: no finding matches fingerprint "${fingerprintArg}".`));
999
+ console.error(chalk.gray(`The fingerprint is a content hash of rule+file+symbol. Get the exact value from `));
1000
+ console.error(chalk.gray(`the JSON report: run ${chalk.cyan('code-audit audit --format json')} (or ${chalk.cyan('code-audit changed --json')}) `));
1001
+ console.error(chalk.gray(`and copy the "fingerprint" field of the finding you want to dismiss.`));
1002
+ process.exit(1);
1003
+ }
1004
+ const entry = buildDismissalEntry(match, reason, packageJson.version);
1005
+ const dismissals = upsertDismissal(projectRoot, entry);
1006
+ // Spec 57 — opt-in telemetry. Nothing is sent unless the user opted in via
1007
+ // the `telemetry` MCP tool (default OFF) AND an endpoint is configured. The
1008
+ // payload is printed for review before sending; any send failure is silent
1009
+ // and never affects the dismissal or the exit code.
1010
+ const telemetry = resolveTelemetryConfig({ endpoint: options.telemetryEndpoint });
1011
+ if (telemetry.enabled && telemetry.endpoint) {
1012
+ const signature = signatureForFinding(match.file, match.line ?? match.start?.line ?? 1, match.column ?? match.start?.column ?? 1) ?? 'unknown';
1013
+ const payload = buildTelemetryPayload({
1014
+ install_id: getInstallId(),
1015
+ toolVersion: packageJson.version,
1016
+ rule: entry.rule,
1017
+ level: match.severity,
1018
+ reason: entry.reason,
1019
+ signature,
1020
+ lang: languageHint(match.file),
1021
+ });
1022
+ process.stderr.write(formatTelemetryPreview(payload));
1023
+ const sendResult = await sendTelemetry(payload, telemetry.endpoint);
1024
+ if (options.json) {
1025
+ // Surface the send outcome in machine output without ever failing on it.
1026
+ process.stderr.write(` telemetry: ${sendResult.sent ? 'sent' : 'not sent'}${sendResult.error ? ` (${sendResult.error})` : ''}\n`);
1027
+ }
1028
+ else if (!sendResult.sent) {
1029
+ console.log(chalk.gray(` Telemetry not sent${sendResult.error ? ` (${sendResult.error})` : ''} — offline is never an error.`));
1030
+ }
1031
+ else {
1032
+ console.log(chalk.gray(' Telemetry sent.'));
1033
+ }
1034
+ }
1035
+ if (options.json) {
1036
+ process.stdout.write(JSON.stringify({
1037
+ success: true,
1038
+ fingerprint: entry.fingerprint,
1039
+ rule: entry.rule,
1040
+ file: entry.file,
1041
+ symbol: entry.symbol,
1042
+ reason: entry.reason,
1043
+ totalDismissed: dismissals.entries.length,
1044
+ }, null, 2) + '\n');
1045
+ }
1046
+ else {
1047
+ console.log(chalk.green(`\nāœ“ Dismissed ${chalk.bold(entry.rule)} @ ${entry.symbol || entry.file} — "${entry.reason}"`));
1048
+ console.log(chalk.gray(` ${dismissals.entries.length} dismissal(s) recorded in .codeauditor.dismissals.json.`));
1049
+ console.log(chalk.gray(` The finding is still counted in every report, but excluded from the gate.`));
1050
+ }
1051
+ }
1052
+ catch (error) {
1053
+ console.error(chalk.red('Error:'), error);
1054
+ process.exit(1);
1055
+ }
1056
+ });
950
1057
  // Generate configuration command
951
1058
  program
952
1059
  .command('generate-config')
@@ -2903,6 +3010,7 @@ program
2903
3010
  const { getDefaultConfig, DEFAULT_CODE_INDEX_CONFIG } = await import('./config/defaults.js');
2904
3011
  const { computeEffectiveConfig, computeTopLevelConfig, } = await import('./config/effectiveConfig.js');
2905
3012
  const { getPreset, PRESET_IDS } = await import('./presets/presets.js');
3013
+ const { readProjectLintThresholds, thresholdsToAnalyzerConfig } = await import('./config/lintConfigReader.js');
2906
3014
  const projectRoot = pathModule.resolve(options.path);
2907
3015
  const filePath = pathModule.resolve(file);
2908
3016
  const configPath = pathModule.join(projectRoot, '.codeauditor.json');
@@ -2937,6 +3045,10 @@ program
2937
3045
  }
2938
3046
  }
2939
3047
  const presets = presetIds.map((id) => getPreset(id));
3048
+ // Spec 50 R2 — read the project's own lint config so size thresholds sourced
3049
+ // from ESLint are named `project-lint-config` rather than `default`.
3050
+ const lintResult = await readProjectLintThresholds(projectRoot);
3051
+ const lintConfig = lintResult ? thresholdsToAnalyzerConfig(lintResult.thresholds) : {};
2940
3052
  const effective = computeEffectiveConfig({
2941
3053
  filePath,
2942
3054
  projectRoot,
@@ -2944,6 +3056,7 @@ program
2944
3056
  pathProfiles,
2945
3057
  enabledAnalyzers,
2946
3058
  presets,
3059
+ lintConfig,
2947
3060
  });
2948
3061
  const topLevel = computeTopLevelConfig(pristine, config);
2949
3062
  if (options.json) {
@@ -2952,6 +3065,7 @@ program
2952
3065
  projectRoot,
2953
3066
  configPath,
2954
3067
  presets: presetIds,
3068
+ lintConfigPath: lintResult?.configPath ?? null,
2955
3069
  matchedProfiles: effective.matchedProfiles,
2956
3070
  excludeFromGate: effective.excludeFromGate ?? null,
2957
3071
  topLevel,
@@ -2964,6 +3078,7 @@ program
2964
3078
  console.log(`${chalk.bold('file:')} ${effective.relativePath}`);
2965
3079
  console.log(`${chalk.bold('project root:')} ${projectRoot}`);
2966
3080
  console.log(`${chalk.bold('presets:')} ${presetIds.length > 0 ? presetIds.join(', ') : chalk.gray('(none)')}`);
3081
+ console.log(`${chalk.bold('lint config:')} ${lintResult?.configPath ? chalk.cyan(lintResult.configKind) + ' ' + chalk.dim(lintResult.configPath) : chalk.gray('(none — defaults)')}`);
2967
3082
  console.log(`${chalk.bold('profiles:')} ${effective.matchedProfiles.length > 0 ? effective.matchedProfiles.join(', ') : chalk.gray('(none)')}`);
2968
3083
  if (effective.excludeFromGate) {
2969
3084
  console.log(`${chalk.bold('excludeFromGate:')} ${chalk.yellow('true')}`);