From 811a0ca283fa2dca1a86327eab3023e0d01a4dce Mon Sep 17 00:00:00 2001 From: Yashas Date: Thu, 17 Sep 2026 15:49:21 +0530 Subject: [PATCH] fix: review coverage renders as a comparison and a list, not chip soup The key=value row was being humanised before it was parsed, so the period tiles never appeared and raw window=... chips showed instead. Parse the raw text; humanise only what is displayed. Findings are now a bulleted list at reading size, and the watched / checked lines are one small line each. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/Findings.tsx | 92 +++++++++++++++++++++++++------------ src/format.ts | 28 +++++++++-- 2 files changed, 87 insertions(+), 33 deletions(-) diff --git a/src/components/Findings.tsx b/src/components/Findings.tsx index 6206e16..4cf2ee7 100644 --- a/src/components/Findings.tsx +++ b/src/components/Findings.tsx @@ -38,7 +38,7 @@ export function Verdict({ const summary = str(row.summary); if (!summary) return null; const line = prose(headline(summary)); - const scanned = prose(row.scanned_summary); + const scanned = str(row.scanned_summary); const affectedHref = affectedClaimsHref(kind, slice, dates); const recoverableHref = recoverableClaimsHref(kind, slice, dates); const atRisk = num(row.amount_at_risk); @@ -98,6 +98,8 @@ export function Verdict({ * bullets so nothing renders as a wall of text. */ /** The comparison, drawn: this period against that one, then what was set aside. */ function Periods({ f }: { f: ReviewedFacts }) { + // longDate passes non-ISO text through unchanged, so an already-humanised + // range renders as it was written. const span = (r: { from: string; to: string } | null) => (r ? `${longDate(r.from)} – ${longDate(r.to)}` : '—'); return (
@@ -105,12 +107,12 @@ function Periods({ f }: { f: ReviewedFacts }) {

This period

{span(f.window)}

- {f.windowClaims &&

{f.windowClaims} claims

} + {f.windowClaims &&

{f.windowClaims} claims reviewed

}

Compared with

{span(f.baseline)}

- {f.baselineClaims &&

{f.baselineClaims} claims

} + {f.baselineClaims &&

{f.baselineClaims} claims as the normal picture

}

@@ -135,35 +137,67 @@ function Periods({ f }: { f: ReviewedFacts }) { function WorkStatement({ text, heading = 'Review coverage', bare = false }: { text: string; heading?: string; bare?: boolean }) { const rows = workStatement(text); const reviewed = rows ? reviewedFacts(rows.find((r) => r.key === 'REVIEWED')?.items ?? []) : null; - const TONE: Record = { - FOUND: 'text-warn', WATCHED: 'text-muted', CLEAR: 'text-good', REVIEWED: 'text-ink', - 'SINCE APPROVAL': 'text-ink', ACTIONS: 'text-ink', RATE: 'text-ink', VERDICT: 'text-warn', - }; - return ( -

- {heading &&

{heading}

} - {rows && reviewed && } - {rows ? ( -
- {rows.filter((r) => !(reviewed && r.key === 'REVIEWED')).map((r) => ( -
-
{r.label}
-
- {r.items.map((it, i) => ( - - {it} - - ))} -
-
- ))} -
- ) : ( + const row = (k: string) => rows?.find((r) => r.key === k); + + // Older cases wrote a paragraph; bullets beat a wall of text. + if (!rows) { + return ( +
+ {heading &&

{heading}

}
    - {sentences(text).map((s, i) => ( + {sentences(prose(text)).map((s, i) => (
  • {s}
  • ))}
+
+ ); + } + + // The four lines have different jobs, so they are not four identical chip + // rows: the periods are a comparison, the findings are the point, the rest + // is reassurance and belongs in small type. + const found = row('FOUND') ?? row('ACTIONS'); + const watched = row('WATCHED'); + const clear = row('CLEAR'); + const rest = rows.filter((r) => !['REVIEWED', 'FOUND', 'WATCHED', 'CLEAR'].includes(r.key)); + + return ( +
+ {heading &&

{heading}

} + {reviewed && } + {!reviewed && row('REVIEWED') && ( +

{row('REVIEWED')!.items.map(prose).join(' · ')}

+ )} + + {found && found.items.length > 0 && ( +
    + {found.items.map((it, i) => ( +
  • +
  • + ))} +
+ )} + + {rest.map((r) => ( +

+ {r.label}: + {r.items.map(prose).join(' · ')} +

+ ))} + + {watched && watched.items.length > 0 && ( +

+ Watched, below the threshold: + {watched.items.map(prose).join(' · ')} +

+ )} + {clear && clear.items.length > 0 && ( +

+ Checked, nothing found: + {clear.items.map(prose).join(' · ')} +

)}
); @@ -306,7 +340,7 @@ export function SqlEvidence({ row, steps, decisions }: { row: Row; steps: AvaTra /** The re-check. Same shape either way, so "fixed" and "not fixed" read alike. */ export function Recheck({ row }: { row: Row }) { - const summary = prose(row.recheck_summary); + const summary = str(row.recheck_summary); if (!summary) return null; const current = num(row.current_rate); const baseline = num(row.baseline_rate); diff --git a/src/format.ts b/src/format.ts index 7987fe0..43ade57 100644 --- a/src/format.ts +++ b/src/format.ts @@ -225,8 +225,24 @@ export function workStatement(v: unknown): Array<{ key: string; label: string; i const m = line.match(/^([A-Z][A-Z ]{2,20}):\s*(.*)$/); if (!m) continue; const key = m[1].trim(); - const items = m[2].split(/;\s*/).map((x) => x.trim().replace(/[.]$/, '')).filter(Boolean); - rows.push({ key, label: WORK_LABELS[key] ?? key.charAt(0) + key.slice(1).toLowerCase(), items }); + // Split on ";" but not inside brackets — "(… $6,764,000; 60.4% pending)" + // is one fact, not two. + const items: string[] = []; + let depth = 0; + let buf = ''; + for (const ch of m[2]) { + if (ch === '(' || ch === '[') depth++; + else if (ch === ')' || ch === ']') depth = Math.max(0, depth - 1); + if (ch === ';' && depth === 0) { + items.push(buf); + buf = ''; + continue; + } + buf += ch; + } + items.push(buf); + const cleaned = items.map((x) => x.trim().replace(/[.]$/, '')).filter(Boolean); + rows.push({ key, label: WORK_LABELS[key] ?? key.charAt(0) + key.slice(1).toLowerCase(), items: cleaned }); } return rows.length >= 2 ? rows : null; } @@ -253,9 +269,13 @@ export function reviewedFacts(items: string[]): ReviewedFacts | null { if (m) kv[m[1].toLowerCase()] = m[2].trim(); } if (!kv.window && !kv.baseline) return null; + // Accept ISO ("2026-08-02..2026-09-13") and already-humanised dates, since + // the same text is sometimes rendered before it is parsed. const range = (v?: string) => { - const m = v?.match(/(\d{4}-\d{2}-\d{2})\s*(?:\.\.|to|–|-)\s*(\d{4}-\d{2}-\d{2})/); - return m ? { from: m[1], to: m[2] } : null; + const iso = v?.match(/(\d{4}-\d{2}-\d{2})\s*(?:\.\.|to|–|-)\s*(\d{4}-\d{2}-\d{2})/); + if (iso) return { from: iso[1], to: iso[2] }; + const human = v?.match(/^(.+?)\s*(?:\.\.|\bto\b|–)\s*(.+)$/); + return human ? { from: human[1].trim(), to: human[2].trim() } : null; }; return { window: range(kv.window),