| 1 | import type { Translator } from "./i18n"; |
| 2 | import type { WireCompletionSummary } from "./types"; |
| 3 | import { normalizeCompletionReceipt, turnChangeText, turnCheckText, turnResultHasContent } from "./turnResult"; |
| 4 | |
| 5 | function count(value: number): number { |
| 6 | return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0; |
| 7 | } |
| 8 | |
| 9 | export function normalizeCompletionSummary(summary: WireCompletionSummary): WireCompletionSummary { |
| 10 | return { |
| 11 | receipt: normalizeCompletionReceipt(summary.receipt), |
| 12 | turnId: summary.turnId, |
| 13 | checkpointTurn: Number.isSafeInteger(summary.checkpointTurn) && summary.checkpointTurn! >= 0 ? summary.checkpointTurn : undefined, |
| 14 | checking: summary.checking === true, |
| 15 | liveChecks: Array.isArray(summary.liveChecks) ? summary.liveChecks : undefined, |
| 16 | preset: String(summary.preset ?? "").trim().toLowerCase(), |
| 17 | verdict: String(summary.verdict ?? "").trim().toLowerCase(), |
| 18 | mutations: count(summary.mutations), |
| 19 | changed_files: summary.changed_files === undefined ? undefined : count(summary.changed_files), |
| 20 | checks_passed: count(summary.checks_passed), |
| 21 | checks_failed: count(summary.checks_failed), |
| 22 | checks_suppressed: count(summary.checks_suppressed), |
| 23 | review: String(summary.review ?? "").trim().toLowerCase(), |
| 24 | gap_kinds: [...new Set((Array.isArray(summary.gap_kinds) ? summary.gap_kinds : []).map((gap) => String(gap).trim().toLowerCase()).filter(Boolean))].slice(0, 8), |
| 25 | constraint_degraded: Boolean(summary.constraint_degraded), |
| 26 | floor: String(summary.floor ?? "").trim().toLowerCase(), |
| 27 | attention: Boolean(summary.attention), |
| 28 | }; |
| 29 | } |
| 30 | |
| 31 | export function sessionQualityFloor(meta?: { qualityFloor?: string; tokenMode?: string } | null): "standard" | "delivery" { |
| 32 | if ((meta?.qualityFloor ?? "").trim().toLowerCase() === "delivery") return "delivery"; |
| 33 | if ((meta?.tokenMode ?? "").trim().toLowerCase() === "delivery") return "delivery"; |
| 34 | return "standard"; |
| 35 | } |
| 36 | |
| 37 | export function completionSummaryNeedsAttention( |
| 38 | summary?: WireCompletionSummary, |
| 39 | _floor: "standard" | "delivery" = "standard", |
| 40 | ): boolean { |
| 41 | if (!summary) return false; |
| 42 | if (summary.receipt?.assessmentKind === "facts") { |
| 43 | return Boolean(summary.receipt.verifications?.some(check => !check.passed || check.interrupted)); |
| 44 | } |
| 45 | const recordedFloor = (summary.floor ?? "").trim().toLowerCase(); |
| 46 | if (recordedFloor === "standard" || recordedFloor === "delivery") return Boolean(summary.attention); |
| 47 | const verdict = summary.verdict.trim().toLowerCase(); |
| 48 | const kinds = new Set((summary.gap_kinds ?? []).map((gap) => gap.trim().toLowerCase()).filter(Boolean)); |
| 49 | if (verdict === "blocked" || summary.checks_failed > 0 || summary.checks_suppressed > 0) return true; |
| 50 | if (kinds.has("unbacked_claim") || kinds.has("failed_verification")) return true; |
| 51 | return false; |
| 52 | } |
| 53 | |
| 54 | |
| 55 | export function completionSummaryPresentation( |
| 56 | summary: WireCompletionSummary, |
| 57 | fallbackFloor: "standard" | "delivery", |
| 58 | t: Translator, |
| 59 | ): { level: "info" | "warn"; title: string; body: string } | undefined { |
| 60 | if (!turnResultHasContent(summary)) return undefined; |
| 61 | return { |
| 62 | level: completionSummaryNeedsAttention(summary, fallbackFloor) ? "warn" : "info", |
| 63 | title: t("notice.completionChangesTitle"), |
| 64 | body: `${turnChangeText(summary, t)}\n${turnCheckText(summary, t)}`, |
| 65 | }; |
| 66 | } |
| 67 |