From b49911601dc4026691c30d60f70c26a2a372a3dd Mon Sep 17 00:00:00 2001 From: Dave Date: Sat, 17 Oct 2026 10:12:42 +0200 Subject: [PATCH] fix(collection-ai): grey ticks when pairing failed, show line findings on the card I3: ai_check.pair_failed marks lines whose covering document couldn't be paired; computeLineTicks returns all grey for them. The card findings box now also lists each row's ai_check.findings after the document findings. Co-Authored-By: Claude Opus 5.5 --- .../collection-confirm/po-card-ai.test.tsx | 15 ++++++++++++- src/lib/collection-ai-card.test.ts | 22 +++++++++++++++++++ src/lib/collection-ai-card.ts | 19 +++++++++++----- src/lib/collection-ai-ticks.test.ts | 5 +++++ src/lib/collection-ai-ticks.ts | 2 ++ src/lib/server/collection-ai-service.test.ts | 4 +++- src/lib/server/collection-ai-service.ts | 10 ++++++--- src/lib/types.ts | 2 ++ 8 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/components/collection-confirm/po-card-ai.test.tsx b/src/components/collection-confirm/po-card-ai.test.tsx index f7996fe..d802e97 100644 --- a/src/components/collection-confirm/po-card-ai.test.tsx +++ b/src/components/collection-confirm/po-card-ai.test.tsx @@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import type { ConfirmRow } from "@/lib/server/collection-confirm-service"; -import { makeAiRow } from "./ai-fixtures"; +import { makeAiRow, makeCheck } from "./ai-fixtures"; import { PoCard } from "./po-card"; const props = { @@ -100,6 +100,19 @@ describe("PoCard AI layer", () => { expect(screen.getByTestId("ai-findings")).toHaveTextContent("Freight not on PO"); }); + it("shows a line's own findings (e.g. a pairing failure) in the findings box", () => { + const row = makeAiRow({ + ai_check: makeCheck({ + doc_line: null, + same_item: null, + pair_failed: true, + findings: ["AI couldn't pair this document: timeout"], + }), + }); + render(); + expect(screen.getByTestId("ai-findings")).toHaveTextContent("AI couldn't pair this document: timeout"); + }); + it("Check with AI calls the handler and shows a spinner while pending", async () => { let resolve!: () => void; const onCheck = vi.fn(() => new Promise((r) => (resolve = r))); diff --git a/src/lib/collection-ai-card.test.ts b/src/lib/collection-ai-card.test.ts index 87ab586..3a63128 100644 --- a/src/lib/collection-ai-card.test.ts +++ b/src/lib/collection-ai-card.test.ts @@ -24,6 +24,28 @@ describe("collection-ai-card", () => { expect(cardFindings(rows)).toEqual(["a", "b", "c"]); }); + it("adds each row's line findings after the document findings, deduped", () => { + const rows = [ + makeAiRow({ + id: "1", + ai_doc: doc("r1", ["a"]), + ai_check: makeCheck({ findings: ["Not found on r1.", "a"] }), + }), + makeAiRow({ + id: "2", + ai_doc: doc("r1", ["a"]), + ai_check: makeCheck({ findings: ["Not found on r1.", "AI couldn't pair this document: boom"] }), + }), + makeAiRow({ id: "3", ai_doc: doc("r2", ["b"]) }), + ]; + expect(cardFindings(rows)).toEqual([ + "a", + "b", + "Not found on r1.", + "AI couldn't pair this document: boom", + ]); + }); + it("picks the newest document for the chip", () => { const rows = [ makeAiRow({ id: "1", ai_doc: doc("r1", [], "2026-10-10") }), diff --git a/src/lib/collection-ai-card.ts b/src/lib/collection-ai-card.ts index ba85636..8a92f3c 100644 --- a/src/lib/collection-ai-card.ts +++ b/src/lib/collection-ai-card.ts @@ -5,20 +5,27 @@ type AiDoc = NonNullable; const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; -/** Union of document findings across a card's distinct reads, deduped, in order. */ +/** + * Union of the document findings across a card's distinct reads, then each + * row's own line findings (`ai_check.findings`), deduped, in order. + */ export function cardFindings(rows: ConfirmRow[]): string[] { const seenReads = new Set(); const seen = new Set(); const out: string[] = []; + const add = (finding: string) => { + if (!finding || seen.has(finding)) return; + seen.add(finding); + out.push(finding); + }; for (const row of rows) { const doc = row.ai_doc; if (!doc || seenReads.has(doc.read_id)) continue; seenReads.add(doc.read_id); - for (const finding of doc.findings ?? []) { - if (!finding || seen.has(finding)) continue; - seen.add(finding); - out.push(finding); - } + for (const finding of doc.findings ?? []) add(finding); + } + for (const row of rows) { + for (const finding of row.ai_check?.findings ?? []) add(finding); } return out; } diff --git a/src/lib/collection-ai-ticks.test.ts b/src/lib/collection-ai-ticks.test.ts index dc1a061..11cb5b1 100644 --- a/src/lib/collection-ai-ticks.test.ts +++ b/src/lib/collection-ai-ticks.test.ts @@ -101,6 +101,11 @@ describe("computeLineTicks", () => { ).qty, ).toBe("green"); }); + it("pairing failed gives all grey (can't check), not red", () => { + expect(computeLineTicks(row(), check({ doc_line: null, same_item: null, pair_failed: true }))).toEqual({ + sku: "grey", qty: "grey", desc: "grey", price: "grey", + }); + }); it("a different or unrated pairing is never green, even when values agree", () => { for (const same_item of ["different", null] as const) { expect(computeLineTicks(row(), check({ same_item }))).toEqual({ diff --git a/src/lib/collection-ai-ticks.ts b/src/lib/collection-ai-ticks.ts index 13df2f8..4b1064d 100644 --- a/src/lib/collection-ai-ticks.ts +++ b/src/lib/collection-ai-ticks.ts @@ -34,6 +34,8 @@ export function effectiveSku(row: Pick): string { export function computeLineTicks(row: TickRow, check: CollectionAiCheck): LineTicks { const doc = check.doc_line; + // Pairing failed: nothing was compared, so nothing can be judged. + if (check.pair_failed && !doc) return { sku: "grey", qty: "grey", desc: "grey", price: "grey" }; // Only a "sure"/"likely" pairing is a match; anything else doesn't match. if (!doc || (check.same_item !== "sure" && check.same_item !== "likely")) { return { sku: "red", qty: "red", desc: "red", price: "red" }; diff --git a/src/lib/server/collection-ai-service.test.ts b/src/lib/server/collection-ai-service.test.ts index d7ea028..ea436eb 100644 --- a/src/lib/server/collection-ai-service.test.ts +++ b/src/lib/server/collection-ai-service.test.ts @@ -1006,11 +1006,13 @@ describe("runCollectionAiTick", () => { expect(r.summary?.cards_checked).toBe(2); expect(r.summary?.errors).toEqual(["PO202610002XX: pairing failed: pairing exploded"]); expect(aiCheck("order_items", "c1")?.doc_line).not.toBeNull(); - // The failure is recorded on the line (all red), not silently dropped. + // The failure is recorded on the line (grey ticks), not silently dropped. expect(aiCheck("order_items", "c2")).toMatchObject({ doc_line: null, + pair_failed: true, findings: ["AI couldn't pair this document: pairing exploded"], }); + expect(aiCheck("order_items", "c1")?.pair_failed).toBe(false); }); it("does not retry a card whose pairing failed on the next tick; a manual check does", async () => { diff --git a/src/lib/server/collection-ai-service.ts b/src/lib/server/collection-ai-service.ts index ab48fed..8aa9fec 100644 --- a/src/lib/server/collection-ai-service.ts +++ b/src/lib/server/collection-ai-service.ts @@ -779,11 +779,14 @@ async function checkCard( } } const docNo = chosen.doc.document_no || "the document"; + // No pairing and no successfully paired document covers this line, + // but one failed: the AI couldn't compare it (grey, not red). + const pairFailed = !chosen.docLine && failureFindings.length > 0 && covering.length === 0; const lineFindings = chosen.docLine ? [] - : checkedDocs.length > 0 - ? [`Not found on ${docNo}.`, ...failureFindings] - : failureFindings; + : pairFailed + ? failureFindings + : [`Not found on ${docNo}.`, ...failureFindings]; const check: CollectionAiCheck = { read_id: chosen.doc.id, document_no: chosen.doc.document_no, @@ -796,6 +799,7 @@ async function checkCard( findings: lineFindings, checked_at: now.toISOString(), model: chosen.model, + pair_failed: pairFailed, }; await pb.collection(line.collection).update(line.id, { ai_check: check }); result.lines_checked += 1; diff --git a/src/lib/types.ts b/src/lib/types.ts index 9a268a0..8e545c9 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -602,6 +602,8 @@ export interface CollectionAiCheck { findings: string[]; checked_at: string; model: string; + /** The AI couldn't pair the document(s) covering this line: ticks are grey. */ + pair_failed?: boolean; } export interface CollectionAiRunSummary {