From 3cda6264fc3b5f30e60caf57997a04d140c4d7c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 03:39:47 +0000 Subject: [PATCH 1/3] feat(cli): Display confidence alongside severity in interactive output Add confidence badge display to the CLI interactive output, allowing users to assess finding reliability alongside severity. This helps with triaging findings since high-severity + low-confidence is very different from high-severity + high-confidence. Changes: - Add formatConfidenceBadge() and formatConfidencePlain() formatters - Display confidence in TTY mode after severity badge - Display confidence in fix flow interactive prompt - Add tests for new confidence display functionality Closes #190 Co-Authored-By: Claude Opus 4.5 https://claude.ai/code/session_011XdKDgKtk2tuTZ5Y9GsG5i --- src/cli/fix.ts | 8 ++++--- src/cli/output/formatters.test.ts | 26 +++++++++++++++++++++++ src/cli/output/formatters.ts | 30 +++++++++++++++++++++++++- src/cli/output/index.ts | 2 ++ src/cli/terminal.test.ts | 35 +++++++++++++++++++++++++++++++ src/cli/terminal.ts | 10 +++++++-- 6 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/cli/fix.ts b/src/cli/fix.ts index 1b083b43..e89ea436 100644 --- a/src/cli/fix.ts +++ b/src/cli/fix.ts @@ -5,7 +5,7 @@ import chalk from 'chalk'; import figures from 'figures'; import type { Finding, SkillReport } from '../types/index.js'; -import { formatSeverityBadge, pluralize, type Reporter } from './output/index.js'; +import { formatSeverityBadge, formatConfidenceBadge, pluralize, type Reporter } from './output/index.js'; import { ICON_CHECK } from './output/icons.js'; import { Verbosity } from './output/verbosity.js'; import { applyUnifiedDiff } from './diff-apply.js'; @@ -177,9 +177,11 @@ export async function runInteractiveFixFlow( console.error(''); - // Severity + counter + title + // Severity + confidence + counter + title const badge = formatSeverityBadge(finding.severity); - console.error(`${badge} ${chalk.bold(`[${idx + 1}/${displayOrder.length}]`)} ${chalk.bold(finding.title)}`); + const confidenceBadge = formatConfidenceBadge(finding.confidence); + const badges = confidenceBadge ? `${badge} ${confidenceBadge}` : badge; + console.error(`${badges} ${chalk.bold(`[${idx + 1}/${displayOrder.length}]`)} ${chalk.bold(finding.title)}`); console.error(chalk.dim(` ${location.path}:${location.startLine}`)); // Description diff --git a/src/cli/output/formatters.test.ts b/src/cli/output/formatters.test.ts index 68b074a8..e37572e1 100644 --- a/src/cli/output/formatters.test.ts +++ b/src/cli/output/formatters.test.ts @@ -9,6 +9,8 @@ import { padRight, formatStatsCompact, formatSeverityBadge, + formatConfidenceBadge, + formatConfidencePlain, } from './formatters.js'; import type { Severity, UsageStats, AuxiliaryUsageMap } from '../../types/index.js'; @@ -92,6 +94,30 @@ describe('formatSeverityBadge', () => { }); }); +describe('formatConfidenceBadge', () => { + it('includes confidence text for each level', () => { + expect(formatConfidenceBadge('high')).toContain('high confidence'); + expect(formatConfidenceBadge('medium')).toContain('medium confidence'); + expect(formatConfidenceBadge('low')).toContain('low confidence'); + }); + + it('returns empty string for undefined confidence', () => { + expect(formatConfidenceBadge(undefined)).toBe(''); + }); +}); + +describe('formatConfidencePlain', () => { + it('formats confidence in brackets', () => { + expect(formatConfidencePlain('high')).toBe('[high confidence]'); + expect(formatConfidencePlain('medium')).toBe('[medium confidence]'); + expect(formatConfidencePlain('low')).toBe('[low confidence]'); + }); + + it('returns empty string for undefined confidence', () => { + expect(formatConfidencePlain(undefined)).toBe(''); + }); +}); + describe('formatProgress', () => { it('formats progress indicator', () => { // Note: formatProgress uses chalk.dim, so we just check it contains the numbers diff --git a/src/cli/output/formatters.ts b/src/cli/output/formatters.ts index b0a9ca48..0af7fdd1 100644 --- a/src/cli/output/formatters.ts +++ b/src/cli/output/formatters.ts @@ -1,6 +1,6 @@ import chalk from 'chalk'; import figures from 'figures'; -import type { Severity, Finding, FileChange, UsageStats, AuxiliaryUsageMap } from '../../types/index.js'; +import type { Severity, Confidence, Finding, FileChange, UsageStats, AuxiliaryUsageMap } from '../../types/index.js'; /** * Capitalize the first letter of a string. @@ -76,6 +76,34 @@ export function formatSeverityPlain(severity: Severity): string { return `[${severity}]`; } +/** + * Confidence configuration for display. + */ +const CONFIDENCE_CONFIG: Record = { + high: { color: chalk.green }, + medium: { color: chalk.yellow }, + low: { color: chalk.red }, +}; + +/** + * Format a confidence badge for terminal output. + * Returns empty string if confidence is undefined. + */ +export function formatConfidenceBadge(confidence: Confidence | undefined): string { + if (!confidence) return ''; + const config = CONFIDENCE_CONFIG[confidence]; + return config.color(`[${confidence} confidence]`); +} + +/** + * Format a confidence level for plain text (CI mode). + * Returns empty string if confidence is undefined. + */ +export function formatConfidencePlain(confidence: Confidence | undefined): string { + if (!confidence) return ''; + return `[${confidence} confidence]`; +} + /** * Format a file location string. */ diff --git a/src/cli/output/index.ts b/src/cli/output/index.ts index d3a64079..1dfdd6e9 100644 --- a/src/cli/output/index.ts +++ b/src/cli/output/index.ts @@ -8,6 +8,8 @@ export { formatSeverityBadge, formatSeverityDot, formatSeverityPlain, + formatConfidenceBadge, + formatConfidencePlain, formatFindingCounts, formatFindingCountsPlain, formatProgress, diff --git a/src/cli/terminal.test.ts b/src/cli/terminal.test.ts index fce90f8f..68138ee1 100644 --- a/src/cli/terminal.test.ts +++ b/src/cli/terminal.test.ts @@ -145,6 +145,41 @@ describe('renderTerminalReport', () => { expect(output).toContain('Test Finding'); expect(output).toContain('This is a test finding'); }); + + it('renders confidence badge in TTY mode when present', () => { + const report = createReport({ + findings: [ + createFinding({ + confidence: 'high', + title: 'Finding with confidence', + }), + ], + }); + + const output = renderTerminalReport([report], { + isTTY: true, + supportsColor: false, + columns: 80, + }); + + expect(output).toContain('high confidence'); + expect(output).toContain('Finding with confidence'); + }); + + it('does not show confidence badge when not present', () => { + const report = createReport({ + findings: [createFinding({ title: 'Finding without trust level' })], + }); + + const output = renderTerminalReport([report], { + isTTY: true, + supportsColor: false, + columns: 80, + }); + + expect(output).toContain('Finding without trust level'); + expect(output).not.toContain('confidence'); + }); }); describe('CI (non-TTY) rendering', () => { diff --git a/src/cli/terminal.ts b/src/cli/terminal.ts index 0373bb59..2aa0bde8 100644 --- a/src/cli/terminal.ts +++ b/src/cli/terminal.ts @@ -5,6 +5,7 @@ import { filterFindings } from '../types/index.js'; import { formatSeverityBadge, formatSeverityPlain, + formatConfidenceBadge, formatFindingCounts, formatFindingCountsPlain, formatDuration, @@ -59,10 +60,15 @@ interface RenderOptions { function formatFindingTTY(finding: Finding, options?: RenderOptions): string[] { const lines: string[] = []; const badge = formatSeverityBadge(finding.severity); + const confidenceBadge = formatConfidenceBadge(finding.confidence); const color = SEVERITY_COLORS[finding.severity]; - // Title line with severity dot - const titleParts = [badge, color(finding.title)]; + // Title line with severity and confidence badges + const titleParts = [badge]; + if (confidenceBadge) { + titleParts.push(confidenceBadge); + } + titleParts.push(color(finding.title)); lines.push(titleParts.join(' ')); // Location with elapsed time From 407e91dad059e84be42719ada50b5e6800f0a9c2 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Thu, 19 Feb 2026 21:22:52 -0800 Subject: [PATCH 2/3] refactor(cli): Move confidence badge below description in findings Confidence is now rendered after the description and verification lines (separated by a blank line) instead of inline on the title line. This keeps the title line clean and scannable while still surfacing confidence as a distinct metadata field. Updates the reporters spec examples and formatting functions table to document the new placement and the two confidence formatter functions. Closes #190 Co-Authored-By: Claude Opus 4.6 --- specs/reporters.md | 12 ++++++++++-- src/cli/fix.ts | 12 ++++++++---- src/cli/terminal.test.ts | 10 +++++++++- src/cli/terminal.ts | 16 ++++++++-------- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/specs/reporters.md b/specs/reporters.md index bb78e829..25bcabc4 100644 --- a/specs/reporters.md +++ b/specs/reporters.md @@ -256,6 +256,8 @@ After all skills complete, findings are rendered to stdout. This is separate fro │ │ │ User input is interpolated directly into a SQL query... │ │ │ +│ [high confidence] │ +│ │ │ Suggested fix: │ │ - const query = buildQuery(userId); │ │ + const query = buildQuery(sanitize(userId)); │ @@ -268,10 +270,12 @@ After all skills complete, findings are rendered to stdout. This is separate fro │ │ │ External API response is cast without validation. │ │ │ +│ [medium confidence] │ +│ │ └───────────────────────────────────────────────────────────┘ ``` -Each finding shows: severity badge, title, location with elapsed time, source code line (read from disk), description, and suggested fix diff (colored: green for `+`, red for `-`, cyan for `@@`). +Each finding shows: severity badge, title, location with elapsed time, source code line (read from disk), description, confidence badge (when present), and suggested fix diff (colored: green for `+`, red for `-`, cyan for `@@`). The confidence badge appears after the description, separated by a blank line. It is colored by level: green for high, yellow for medium, red for low. When confidence is not provided, the badge and its blank line are omitted. When fixable findings exist and interactive mode is active (TTY, no `--fix`, no `--json`, not quiet, not interrupted), suggested fix diffs are **suppressed** from the report because they will be shown in the interactive fix step-through (see Section below). All other finding fields (severity, title, location, description) still render normally. @@ -453,6 +457,8 @@ Findings are displayed in reading order (file ascending, line ascending). Each f User input is interpolated directly into a SQL query... Use parameterized queries instead + [high confidence] + @@ -42,1 +42,1 @@ - const query = buildQuery(userId); + const query = buildQuery(sanitize(userId)); @@ -460,7 +466,7 @@ Findings are displayed in reading order (file ascending, line ascending). Each f [y]es / [n]o / [a]pply all / [s]kip all ``` -Severity badge, counter, title, location, description, fix description (if present), and colored diff. +Severity badge, counter, title, location, description, fix description (if present), confidence badge (when present), and colored diff. **Prompt keys** (single keypress, no Enter): @@ -608,6 +614,8 @@ All reporters use shared formatters from `src/cli/output/formatters.ts`. | `formatFindingCountsPlain(counts)` | `Record` | Plain text | `2 findings (1 high, 1 medium)` | | `formatSeverityBadge(severity)` | `Severity` | Colored dot + text | `. (high)` | | `formatSeverityPlain(severity)` | `Severity` | Bracketed | `[high]` | +| `formatConfidenceBadge(confidence?)` | `Confidence \| undefined` | Colored bracketed (empty if undefined) | `[high confidence]` | +| `formatConfidencePlain(confidence?)` | `Confidence \| undefined` | Plain bracketed (empty if undefined) | `[high confidence]` | | `countBySeverity(findings)` | `Finding[]` | `Record` | `{ critical: 0, high: 1, ... }` | | `pluralize(count, singular, plural?)` | `number, string` | Pluralized word | `file` / `files` | diff --git a/src/cli/fix.ts b/src/cli/fix.ts index e89ea436..f3a7b992 100644 --- a/src/cli/fix.ts +++ b/src/cli/fix.ts @@ -177,11 +177,9 @@ export async function runInteractiveFixFlow( console.error(''); - // Severity + confidence + counter + title + // Severity + counter + title const badge = formatSeverityBadge(finding.severity); - const confidenceBadge = formatConfidenceBadge(finding.confidence); - const badges = confidenceBadge ? `${badge} ${confidenceBadge}` : badge; - console.error(`${badges} ${chalk.bold(`[${idx + 1}/${displayOrder.length}]`)} ${chalk.bold(finding.title)}`); + console.error(`${badge} ${chalk.bold(`[${idx + 1}/${displayOrder.length}]`)} ${chalk.bold(finding.title)}`); console.error(chalk.dim(` ${location.path}:${location.startLine}`)); // Description @@ -194,6 +192,12 @@ export async function runInteractiveFixFlow( console.error(` ${suggestedFix.description}`); } + // Confidence + if (finding.confidence) { + console.error(''); + console.error(` ${formatConfidenceBadge(finding.confidence)}`); + } + console.error(''); // Display the diff diff --git a/src/cli/terminal.test.ts b/src/cli/terminal.test.ts index 68138ee1..291b0396 100644 --- a/src/cli/terminal.test.ts +++ b/src/cli/terminal.test.ts @@ -146,12 +146,13 @@ describe('renderTerminalReport', () => { expect(output).toContain('This is a test finding'); }); - it('renders confidence badge in TTY mode when present', () => { + it('renders confidence badge after description in TTY mode', () => { const report = createReport({ findings: [ createFinding({ confidence: 'high', title: 'Finding with confidence', + description: 'This is the description', }), ], }); @@ -164,6 +165,13 @@ describe('renderTerminalReport', () => { expect(output).toContain('high confidence'); expect(output).toContain('Finding with confidence'); + // Confidence should appear after the description, not on the title line + const lines = output.split('\n'); + const titleLine = lines.findIndex((l) => l.includes('Finding with confidence')); + const confidenceLine = lines.findIndex((l) => l.includes('high confidence')); + const descriptionLine = lines.findIndex((l) => l.includes('This is the description')); + expect(confidenceLine).toBeGreaterThan(descriptionLine); + expect(confidenceLine).toBeGreaterThan(titleLine); }); it('does not show confidence badge when not present', () => { diff --git a/src/cli/terminal.ts b/src/cli/terminal.ts index 2aa0bde8..f03fcca7 100644 --- a/src/cli/terminal.ts +++ b/src/cli/terminal.ts @@ -60,16 +60,10 @@ interface RenderOptions { function formatFindingTTY(finding: Finding, options?: RenderOptions): string[] { const lines: string[] = []; const badge = formatSeverityBadge(finding.severity); - const confidenceBadge = formatConfidenceBadge(finding.confidence); const color = SEVERITY_COLORS[finding.severity]; - // Title line with severity and confidence badges - const titleParts = [badge]; - if (confidenceBadge) { - titleParts.push(confidenceBadge); - } - titleParts.push(color(finding.title)); - lines.push(titleParts.join(' ')); + // Title line with severity badge + lines.push(`${badge} ${color(finding.title)}`); // Location with elapsed time if (finding.location) { @@ -111,6 +105,12 @@ function formatFindingTTY(finding: Finding, options?: RenderOptions): string[] { lines.push(` ${chalk.dim.italic(finding.verification)}`); } + // Confidence level + if (finding.confidence) { + lines.push(''); + lines.push(` ${formatConfidenceBadge(finding.confidence)}`); + } + // Suggested fix diff if available (suppress when step-through will show it) if (finding.suggestedFix?.diff && !options?.suppressFixDiffs) { lines.push(''); From a010b1d59aef825ebcbeb03d3ce45800ac1a6871 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Thu, 19 Feb 2026 21:40:31 -0800 Subject: [PATCH 3/3] fix: Remove unused formatConfidencePlain function Dead code flagged by Cursor Bugbot. The CI rendering path inlines confidence formatting directly, so this function was never called. Co-Authored-By: Claude Opus 4.6 --- specs/reporters.md | 1 - src/cli/output/formatters.test.ts | 12 ------------ src/cli/output/formatters.ts | 8 -------- src/cli/output/index.ts | 1 - 4 files changed, 22 deletions(-) diff --git a/specs/reporters.md b/specs/reporters.md index 25bcabc4..88767ec5 100644 --- a/specs/reporters.md +++ b/specs/reporters.md @@ -615,7 +615,6 @@ All reporters use shared formatters from `src/cli/output/formatters.ts`. | `formatSeverityBadge(severity)` | `Severity` | Colored dot + text | `. (high)` | | `formatSeverityPlain(severity)` | `Severity` | Bracketed | `[high]` | | `formatConfidenceBadge(confidence?)` | `Confidence \| undefined` | Colored bracketed (empty if undefined) | `[high confidence]` | -| `formatConfidencePlain(confidence?)` | `Confidence \| undefined` | Plain bracketed (empty if undefined) | `[high confidence]` | | `countBySeverity(findings)` | `Finding[]` | `Record` | `{ critical: 0, high: 1, ... }` | | `pluralize(count, singular, plural?)` | `number, string` | Pluralized word | `file` / `files` | diff --git a/src/cli/output/formatters.test.ts b/src/cli/output/formatters.test.ts index e37572e1..fe4c3b96 100644 --- a/src/cli/output/formatters.test.ts +++ b/src/cli/output/formatters.test.ts @@ -10,7 +10,6 @@ import { formatStatsCompact, formatSeverityBadge, formatConfidenceBadge, - formatConfidencePlain, } from './formatters.js'; import type { Severity, UsageStats, AuxiliaryUsageMap } from '../../types/index.js'; @@ -106,17 +105,6 @@ describe('formatConfidenceBadge', () => { }); }); -describe('formatConfidencePlain', () => { - it('formats confidence in brackets', () => { - expect(formatConfidencePlain('high')).toBe('[high confidence]'); - expect(formatConfidencePlain('medium')).toBe('[medium confidence]'); - expect(formatConfidencePlain('low')).toBe('[low confidence]'); - }); - - it('returns empty string for undefined confidence', () => { - expect(formatConfidencePlain(undefined)).toBe(''); - }); -}); describe('formatProgress', () => { it('formats progress indicator', () => { diff --git a/src/cli/output/formatters.ts b/src/cli/output/formatters.ts index 0af7fdd1..b57d6b83 100644 --- a/src/cli/output/formatters.ts +++ b/src/cli/output/formatters.ts @@ -95,14 +95,6 @@ export function formatConfidenceBadge(confidence: Confidence | undefined): strin return config.color(`[${confidence} confidence]`); } -/** - * Format a confidence level for plain text (CI mode). - * Returns empty string if confidence is undefined. - */ -export function formatConfidencePlain(confidence: Confidence | undefined): string { - if (!confidence) return ''; - return `[${confidence} confidence]`; -} /** * Format a file location string. diff --git a/src/cli/output/index.ts b/src/cli/output/index.ts index 1dfdd6e9..9f06e48d 100644 --- a/src/cli/output/index.ts +++ b/src/cli/output/index.ts @@ -9,7 +9,6 @@ export { formatSeverityDot, formatSeverityPlain, formatConfidenceBadge, - formatConfidencePlain, formatFindingCounts, formatFindingCountsPlain, formatProgress,