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 {