fix: dates in Ava's prose read as 'Jul 26, 2026', not 2026-07-26

Display-only rewrite (format.prose) on every surface that shows her text —
headline, work statement, full story, recommendations, re-check, queue cards,
slides, notes. Stored text keeps ISO dates so deadline and spike parsers still
read them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-16 19:17:08 +05:30
parent 75483f347e
commit 4087d2c2e0
5 changed files with 35 additions and 31 deletions

View File

@ -5,18 +5,7 @@ import type { Slice } from '../api/charts';
import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink';
import { Badge, Card, Facts } from './core';
import type { AvaDecision, AvaTraceStep, Row } from '../api/types';
import {
firstDeadline,
headline,
money,
num,
pct,
recommendations,
shortDate,
splitRecommendation,
sqlSteps,
str,
} from '../format';
import { firstDeadline, headline, money, num, pct, prose, recommendations, shortDate, splitRecommendation, sqlSteps, str } from '../format';
/**
* Ava's findings, laid out for a person who has ninety seconds.
@ -46,8 +35,8 @@ export function Verdict({
}) {
const summary = str(row.summary);
if (!summary) return null;
const line = headline(summary);
const scanned = str(row.scanned_summary);
const line = prose(headline(summary));
const scanned = prose(row.scanned_summary);
const affectedHref = affectedClaimsHref(kind, slice, anchor);
const recoverableHref = recoverableClaimsHref(kind, slice, anchor);
const atRisk = num(row.amount_at_risk);
@ -109,7 +98,7 @@ export function Verdict({
/** The decision list. Rendered directly above the Approve / Send back / Reject buttons. */
export function Recommendations({ row }: { row: Row }) {
const recs = recommendations(row.recommendations);
const recs = recommendations(row.recommendations).map(prose);
if (recs.length === 0) return null;
return (
<section className="lift rounded-xl border border-line bg-panel">
@ -140,9 +129,9 @@ export function Recommendations({ row }: { row: Row }) {
/** The paragraph version — for the reader who wants the whole account. Collapsed. */
export function FullStory({ row }: { row: Row }) {
const [open, setOpen] = useState(false);
const summary = str(row.summary);
const cause = str(row.root_cause);
const secondary = str(row.secondary_findings);
const summary = prose(row.summary);
const cause = prose(row.root_cause);
const secondary = prose(row.secondary_findings);
if (!summary) return null;
const hasSecondary = secondary && !/^none found/i.test(secondary.trim());
@ -244,7 +233,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 = str(row.recheck_summary);
const summary = prose(row.recheck_summary);
if (!summary) return null;
const current = num(row.current_rate);
const baseline = num(row.baseline_rate);

View File

@ -184,6 +184,21 @@ export function firstDeadline(recs: unknown): string | null {
}
/** 2026-10-02 → "Oct 2" */
/** 2026-07-26 → "Jul 26, 2026". For prose; shortDate is for axes and chips. */
export function longDate(iso: string | null): string {
if (!iso) return '';
const d = new Date(iso + 'T00:00:00');
return Number.isNaN(d.getTime()) ? iso : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
/** Ava writes dates as YYYY-MM-DD so the code can read them (deadlines, the
* spike week, the claims links). People should not have to. This rewrites
* every ISO date inside a piece of prose for display, and nothing else —
* the stored text, and every parser that reads it, still sees the ISO form. */
export function prose(v: unknown): string {
return str(v).replace(/\b(\d{4})-(\d{2})-(\d{2})\b/g, (m) => longDate(m) || m);
}
export function shortDate(iso: string | null): string {
if (!iso) return '';
const d = new Date(iso + 'T00:00:00');

View File

@ -2,7 +2,7 @@ import pptxgen from 'pptxgenjs';
import type { BriefingData, Slice } from '../api/charts';
import { RECOVERY_ORDER } from '../api/charts';
import type { Row } from '../api/types';
import { headline, money, num, recommendations, shortDate, splitRecommendation, str } from '../format';
import { headline, money, num, prose, recommendations, shortDate, splitRecommendation, str } from '../format';
/**
* The deck. Six slides a manager can walk into a CFO's office with.
@ -63,7 +63,7 @@ export async function buildDeck(row: Row, data: BriefingData, slice: Slice | nul
// ---- 1. What happened -------------------------------------------------------
{
const s = pres.addSlide();
chrome(s, headline(row.summary, 240), 'Summary');
chrome(s, prose(headline(row.summary, 240)), 'Summary');
const tiles: Array<[string, string, string]> = [
['Claims denied', num(row.affected_claims)?.toLocaleString('en-US') ?? '—', INK],
['Money at stake', money(row.amount_at_risk), 'B31E23'],
@ -75,7 +75,7 @@ export async function buildDeck(row: Row, data: BriefingData, slice: Slice | nul
s.addText(label.toUpperCase(), { x: x + 0.25, y: 2.05, w: 3.3, h: 0.3, fontSize: 10, bold: true, color: MUTED, charSpacing: 1 });
s.addText(value, { x: x + 0.25, y: 2.4, w: 3.3, h: 0.9, fontSize: 36, bold: true, color });
});
const cause = str(row.root_cause);
const cause = prose(row.root_cause);
if (cause) {
s.addText('ROOT CAUSE', { x: 0.6, y: 3.95, w: 12, h: 0.3, fontSize: 10, bold: true, color: MUTED, charSpacing: 1 });
s.addText(cause, { x: 0.6, y: 4.25, w: 12.1, h: 2.5, fontSize: 13, color: INK, valign: 'top' });
@ -194,7 +194,7 @@ export async function buildDeck(row: Row, data: BriefingData, slice: Slice | nul
}
// ---- 5. What she recommends -------------------------------------------------
const recs = recommendations(row.recommendations);
const recs = recommendations(row.recommendations).map(prose);
if (recs.length) {
const s = pres.addSlide();
chrome(s, 'Recommended actions', 'For approval');
@ -221,7 +221,7 @@ export async function buildDeck(row: Row, data: BriefingData, slice: Slice | nul
const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true';
const conf = num(row.confidence);
const bullets = [
str(row.scanned_summary) || 'Reviewed every payer, department and front desk against an 8-week baseline.',
prose(row.scanned_summary) || 'Reviewed every payer, department and front desk against an 8-week baseline.',
reconciled ? 'Claims data reconciled against the finance report before any analysis.' : 'Claims data could not be fully reconciled against finance — treat numbers as provisional.',
kind === 'payer_policy'
? `Cause matched to the payer's own policy bulletin${slice?.spikeStart ? ` effective ${shortDate(slice.spikeStart)}` : ''}.`

View File

@ -19,7 +19,7 @@ import { Disclosure, FullStory, Recheck, Recommendations, SqlEvidence, Verdict }
import { Briefing, BriefingSkeleton } from '../components/Briefing';
import { loadBriefing, sliceOf } from '../api/charts';
import type { Action } from '../api/config';
import { dateTime, num, str } from '../format';
import { dateTime, num, prose, str } from '../format';
/**
* One investigation, for a person with ninety seconds.
@ -170,7 +170,7 @@ export function CaseScreen() {
<section className="lift rounded-xl border border-bad-edge bg-bad-soft px-5 py-4">
<p className="text-[11px] font-semibold tracking-wide text-bad uppercase">Escalated — requires a decision</p>
<p className="mt-1.5 text-[17px] leading-snug font-semibold text-ink">
{str(row.escalation_reason) || 'No reason recorded.'}
{prose(row.escalation_reason) || 'No reason recorded.'}
</p>
</section>
)}
@ -257,8 +257,8 @@ export function CaseScreen() {
? `${str(row.period_weeks)} weeks`
: '6 weeks',
},
...(str(row.review_note) ? [{ label: 'Reviewer note', value: str(row.review_note), wide: true }] : []),
...(str(row.approval_note) ? [{ label: 'Approval note', value: str(row.approval_note), wide: true }] : []),
...(str(row.review_note) ? [{ label: 'Reviewer note', value: prose(row.review_note), wide: true }] : []),
...(str(row.approval_note) ? [{ label: 'Approval note', value: prose(row.approval_note), wide: true }] : []),
]}
/>
</section>

View File

@ -4,7 +4,7 @@ import { useQuery, useZino } from '../api/provider';
import { FINDING_KIND, QUEUES, RECORD_VIEW, STAGE_TONE } from '../api/config';
import { Badge, Button, Empty, ErrorNote, Spinner } from '../components/core';
import type { Row } from '../api/types';
import { dateTime, headline, money, num, str } from '../format';
import { dateTime, headline, money, num, prose, str } from '../format';
/**
* A queue, as a morning briefing.
@ -115,8 +115,8 @@ function Briefing({ rows, onRun }: { rows: Row[]; onRun: () => void }) {
/** A case, led by its verdict and its money. */
function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
const state = str(row.current_state_name);
const summary = str(row.summary);
const scanned = str(row.scanned_summary);
const summary = prose(row.summary);
const scanned = prose(row.scanned_summary);
const flagged = num(row.flagged_count);
const kind = str(row.finding_kind);
const recoverable = num(row.recoverable_amount);