fix(260716-pgr): confidence display shows percent, not raw 0-1 scale

classifications.confidence is 0.0-1.0 (1 = max confidence). ClassificationCard
appended "%" directly to the raw value, so confidence=1 rendered as "1%
confidence" -- read as near-zero, the opposite of its true meaning. Found live
while reviewing a Breach Secure Now simulated-phishing classification.
This commit is contained in:
lorentz 2026-07-16 18:21:40 -04:00
parent 40dd1e10e9
commit 3f16268152
3 changed files with 146 additions and 2 deletions

View file

@ -0,0 +1,109 @@
---
phase: quick
plan: 260716-pgr
type: execute
wave: 1
depends_on: []
files_modified:
- components/phishing/classification-card.tsx
autonomous: true
requirements: [QUICK-CONFIDENCE-DISPLAY]
must_haves:
truths:
- "A maximum-confidence classification (confidence='1') displays as '100% confidence', not '1% confidence'"
- "A 0.85-confidence classification displays as '85% confidence'"
- "If confidence is null or does not parse to a finite number, no confidence text renders"
artifacts:
- path: components/phishing/classification-card.tsx
provides: "Corrected confidence display (0-1 scale rendered as 0-100 percent)"
contains: "Math.round(Number(classification.confidence) * 100)"
key_links:
- from: components/phishing/classification-card.tsx
to: classifications.confidence (0.0-1.0 NUMERIC, serialized as string)
via: "Number() coercion + *100 + Math.round in the render guard"
pattern: "Number\\(classification.confidence\\) \\* 100"
---
<objective>
Fix a confidence display bug in the phishing ClassificationCard. The `classifications.confidence`
column stores a 0.0-1.0 scale (computed by `campaign-classifier.ts` `computeConfidence()`, where
`confidence=1` means maximum/100% confidence). The card currently appends "%" directly to the raw
0-1 value, so a maximum-confidence classification renders as "1% confidence" — the exact opposite
of its true meaning to an operator.
Purpose: Operators reading a phishing classification must see the correct confidence percentage.
Output: One corrected render expression in `components/phishing/classification-card.tsx`.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/quick/260716-pgr-fix-confidence-display-bug-in-classifica/260716-pgr-PLAN.md
<interfaces>
From components/phishing/classification-card.tsx:
```typescript
export interface ClassificationCardData {
id: string;
verdict: 'SPAM' | 'UNWANTED' | 'THREAT';
confidence: string | null; // API serializes the 0.0-1.0 NUMERIC column as a string
// ...
}
```
Current render block (lines 118-122) — the exact block to replace:
```tsx
{classification.confidence != null && (
<span className="text-sm text-muted-foreground">
{classification.confidence}% confidence
</span>
)}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Convert confidence to percent before rendering</name>
<files>components/phishing/classification-card.tsx</files>
<action>Replace the confidence render block at lines 118-122. Parse the value once
with `Number(classification.confidence)` and multiply by 100 with `Math.round(... * 100)`
so the 0.0-1.0 scale renders as a 0-100 integer percent. Extend the existing
`classification.confidence != null` guard so the block ONLY renders when the parsed number
is finite (use `Number.isFinite(...)`), matching the existing null-guard style already
wrapping this block — if the string does not parse, render nothing rather than "NaN%".
Keep the same `<span className="text-sm text-muted-foreground">...% confidence</span>`
markup; only the numeric expression and the guard condition change. Do NOT change the
`confidence: string | null` type or any other part of the file.</action>
<verify>
<automated>npx tsc --noEmit --pretty 2>&1 | grep -i "classification-card" || echo "no type errors in classification-card"</automated>
<automated>grep -q "Math.round(Number(classification.confidence) \* 100)" components/phishing/classification-card.tsx && echo "conversion present"</automated>
<automated>grep -q "Number.isFinite" components/phishing/classification-card.tsx && echo "finite guard present"</automated>
</verify>
<done>The card renders confidence='1' as "100% confidence" and confidence='0.85' as
"85% confidence"; null or non-parsing values render no confidence text; `npx tsc --noEmit`
reports no new errors.</done>
</task>
</tasks>
<verification>
- `npx tsc --noEmit --pretty` passes (tsc is the only safety net; components/** has no vitest coverage).
- Manual read-through: the render expression multiplies by 100 and rounds, and the guard checks `Number.isFinite`.
</verification>
<success_criteria>
- confidence='1' → "100% confidence"
- confidence='0.85' → "85% confidence"
- confidence=null or unparseable → no confidence text rendered
- No other behavior in the component changes; type check clean.
</success_criteria>
<output>
Create `.planning/quick/260716-pgr-fix-confidence-display-bug-in-classifica/260716-pgr-SUMMARY.md` when done.
</output>

View file

@ -0,0 +1,35 @@
---
status: complete
---
# Quick Task 260716-pgr: Fix confidence display bug — Summary
**`ClassificationCard` rendered the 0.0-1.0 `classifications.confidence` value by appending "%" directly, so a maximum-confidence classification (`confidence='1'`) displayed as "1% confidence" — read by an operator as near-zero, the opposite of its true meaning. Fixed by multiplying by 100 and rounding, guarded by `Number.isFinite`.**
## Change
`components/phishing/classification-card.tsx` — confidence render block:
```tsx
{classification.confidence != null && Number.isFinite(Number(classification.confidence)) && (
<span className="text-sm text-muted-foreground">
{Math.round(Number(classification.confidence) * 100)}% confidence
</span>
)}
```
## Verification
- `npx tsc --noEmit --pretty` clean.
- `Math.round(Number(classification.confidence) * 100)` present.
- `Number.isFinite` guard present.
- Manual trace: `confidence='1'` → "100% confidence"; `confidence='0.85'` → "85% confidence"; `confidence=null` or unparseable → no confidence text renders.
## Deviations from Plan
None — plan executed exactly as written.
## Discovered while investigating this bug
This was found live while reviewing ticket 699415's classification in the Phase 22 review page (a Breach Secure Now/KnowBe4 simulated-phishing report, correctly classified `UNWANTED` with `confidence=1`, displaying as "1% confidence" before this fix). The same investigation surfaced two follow-on items tracked separately:
- A dead Autotask webhook (no delivery since 2026-04-29) — fixed same session by re-registering the Tickets/TicketNotes subscriptions.
- A request to add a dedicated "User Awareness"/simulation verdict (today forced into the generic `UNWANTED` bucket) plus a per-company automation gate — scoped as a new phase, not part of this quick task.

View file

@ -115,9 +115,9 @@ export function ClassificationCard({
<StatusBadge variantClass={VERDICT_VARIANT_CLASS[classification.verdict]}>
{classification.verdict}
</StatusBadge>
{classification.confidence != null && (
{classification.confidence != null && Number.isFinite(Number(classification.confidence)) && (
<span className="text-sm text-muted-foreground">
{classification.confidence}% confidence
{Math.round(Number(classification.confidence) * 100)}% confidence
</span>
)}
</div>