fix: "$1.56M" is not two sentences, and one attempt is not two

Recommendation titles were cut at the first period, so "$1.56M billed"
rendered as a title of "$1" and a body starting "56M". The split now breaks
only on a period followed by a space, a colon, or a newline — decimals and
codes like CO-197 survive.

Her evidence counted every attempt on the case: a run that failed to submit
leaves its queries in the trace beside the run that succeeded, and 16 + 20
read as "36 database queries". The panel now scopes to the attempt whose
decision was submitted (trace events carry decision_id), so the count is
what she actually did to reach these findings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-16 15:17:01 +05:30
parent 5eb0890e97
commit 5df97f989c
3 changed files with 21 additions and 8 deletions

View File

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { AlertTriangle, CheckCircle2, ChevronDown, FileSearch } from 'lucide-react'; import { AlertTriangle, CheckCircle2, ChevronDown, FileSearch } from 'lucide-react';
import { Badge, Card, Facts } from './core'; import { Badge, Card, Facts } from './core';
import type { AvaTraceStep, Row } from '../api/types'; import type { AvaDecision, AvaTraceStep, Row } from '../api/types';
import { import {
firstDeadline, firstDeadline,
headline, headline,
@ -159,9 +159,14 @@ export function FullStory({ row }: { row: Row }) {
* paste 16 full queries into a form field was what blew the tool call past * paste 16 full queries into a form field was what blew the tool call past
* the output cap and lost a whole investigation. * the output cap and lost a whole investigation.
*/ */
export function SqlEvidence({ row, steps }: { row: Row; steps: AvaTraceStep[] }) { export function SqlEvidence({ row, steps, decisions }: { row: Row; steps: AvaTraceStep[]; decisions: AvaDecision[] }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const queries = steps.filter((s) => s.type === 'api' && s.detail); // Only the attempt that produced the findings. A case can carry an earlier
// attempt that failed to submit (its queries are still in the trace); showing
// both would double the count and make her look twice as busy as she was.
const submitted = decisions.filter((d) => d.status === 'submitted').slice(-1)[0];
const scoped = submitted ? steps.filter((s) => s.decision_id == null || String(s.decision_id) === String(submitted.id)) : steps;
const queries = scoped.filter((s) => s.type === 'api' && s.detail);
const notes = sqlSteps(row.sql_log); const notes = sqlSteps(row.sql_log);
if (queries.length === 0 && notes.length === 0) return null; if (queries.length === 0 && notes.length === 0) return null;

View File

@ -101,9 +101,17 @@ export function recommendations(v: unknown): string[] {
/** "1. APPEALS — FILE RETRO-AUTH ($1,091,000 …)" → title + body. */ /** "1. APPEALS — FILE RETRO-AUTH ($1,091,000 …)" → title + body. */
export function splitRecommendation(block: string): { title: string; body: string } { export function splitRecommendation(block: string): { title: string; body: string } {
const m = block.match(/^\s*(\d+)\.\s*([^.\n]+?)(?:\.|\n|$)([\s\S]*)$/); // Strip the leading "N." and take the title up to the first sentence break —
if (!m) return { title: '', body: block }; // a period FOLLOWED BY A SPACE, a colon, or a newline. Never a bare period:
return { title: m[2].trim(), body: (m[3] ?? '').trim() }; // "$1.56M" and "CO-197" must survive intact.
const rest = block.replace(/^\s*\d+\.\s*/, '');
const m = rest.match(/^([\s\S]*?)(?:\.\s|:\s|\n|$)([\s\S]*)$/);
if (!m) return { title: '', body: rest };
const title = m[1].trim();
const body = (m[2] ?? '').trim();
// A "title" that swallowed the whole block means there was no break — show it as body.
if (title.length > 140) return { title: '', body: rest };
return { title, body };
} }
/** Ava's SQL log is one statement per block, each prefixed with its step. */ /** Ava's SQL log is one statement per block, each prefixed with its step. */

View File

@ -198,7 +198,7 @@ export function CaseScreen() {
{hasFindings && ( {hasFindings && (
<> <>
<FullStory row={row} /> <FullStory row={row} />
<SqlEvidence row={row} steps={trace.data ?? []} /> <SqlEvidence row={row} steps={trace.data ?? []} decisions={decisions.data ?? []} />
{!aiOwned && ( {!aiOwned && (
<Disclosure <Disclosure
open={showWork} open={showWork}
@ -216,7 +216,7 @@ export function CaseScreen() {
)} )}
{quiet && ( {quiet && (
<> <>
<SqlEvidence row={row} steps={trace.data ?? []} /> <SqlEvidence row={row} steps={trace.data ?? []} decisions={decisions.data ?? []} />
<Disclosure <Disclosure
open={showWork} open={showWork}
onToggle={() => setShowWork(!showWork)} onToggle={() => setShowWork(!showWork)}