diff --git a/specs/reporters.md b/specs/reporters.md index bb78e82..88767ec 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,7 @@ 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]` | | `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 1b083b4..f3a7b99 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'; @@ -192,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/output/formatters.test.ts b/src/cli/output/formatters.test.ts index 68b074a..fe4c3b9 100644 --- a/src/cli/output/formatters.test.ts +++ b/src/cli/output/formatters.test.ts @@ -9,6 +9,7 @@ import { padRight, formatStatsCompact, formatSeverityBadge, + formatConfidenceBadge, } from './formatters.js'; import type { Severity, UsageStats, AuxiliaryUsageMap } from '../../types/index.js'; @@ -92,6 +93,19 @@ 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('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 b0a9ca4..b57d6b8 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,26 @@ 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 file location string. */ diff --git a/src/cli/output/index.ts b/src/cli/output/index.ts index d3a6407..9f06e48 100644 --- a/src/cli/output/index.ts +++ b/src/cli/output/index.ts @@ -8,6 +8,7 @@ export { formatSeverityBadge, formatSeverityDot, formatSeverityPlain, + formatConfidenceBadge, formatFindingCounts, formatFindingCountsPlain, formatProgress, diff --git a/src/cli/terminal.test.ts b/src/cli/terminal.test.ts index fce90f8..291b039 100644 --- a/src/cli/terminal.test.ts +++ b/src/cli/terminal.test.ts @@ -145,6 +145,49 @@ describe('renderTerminalReport', () => { expect(output).toContain('Test Finding'); expect(output).toContain('This is a test finding'); }); + + 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', + }), + ], + }); + + const output = renderTerminalReport([report], { + isTTY: true, + supportsColor: false, + columns: 80, + }); + + 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', () => { + 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 0373bb5..f03fcca 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, @@ -61,9 +62,8 @@ function formatFindingTTY(finding: Finding, options?: RenderOptions): string[] { const badge = formatSeverityBadge(finding.severity); const color = SEVERITY_COLORS[finding.severity]; - // Title line with severity dot - const titleParts = [badge, 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) { @@ -105,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('');