// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 import type { DocumentItems, Item, ItemCitation } from "@conusai-kit/platform" import { describe, expect, it, vi } from "vitest" import { boxOf, candidateOf, labelAndValueOf, platformItemsExtractor } from "../adapters/platform-items-extractor.js" import { feedbackOf, itemRefOf } from "../core/item-feedback.js" import { ExtractionFailed, type ReadDocument, type ReadPage } from "../core/ports.js" import { noCache } from "../core/stage-cache.js" const SHA = "a".repeat(64) const cited = (page: number): ItemCitation => ({ kind: "pdf_page", file_sha256: SHA, page, lines: [1], bbox: [100, 400, 900, 500], quote: "1 Kabelis Al 4x25 m 120", }) const item = (overrides: Partial = {}): Item => ({ id: "p1-l1", section: { title: "Medžiagos", class: "material", page: 1 }, position: "1", name: "Kabelis Al 4x25", class: "material", unit: { text: "m", code: "MTR" }, quantity: { value: "120", text: "120", column: "Kiekis", refusal: null }, spec: null, citations: [cited(1)], checks: [{ check: "quote_verbatim", status: "passed" }], agreement: { status: "auto_accepted", reader: true, model: true }, method: "reader_and_model", confidence: 0.98, ...overrides, }) const page = (index: number, text: string, cls: ReadPage["class"] = "table"): ReadPage => ({ index, width: 1000, height: 2000, class: cls, text, lines: [], blocks: [], }) const first = page(0, "Medžiagos\n1 Kabelis Al 4x25 m 120") const result: Pick = { run_id: 31, models: [{ stage: "rows", model: "claude-haiku-5-5" }], } const pageOf = (n: number) => (n === 1 ? first : undefined) describe("platform items as lines", () => { it("maps an item to a verified line, its box as fractions of the page read", () => { expect(candidateOf(item(), result, pageOf)).toEqual({ label: "Kabelis Al 4x25", value: "120 m", page: 1, quote: "1 Kabelis Al 4x25 m 120", box: { x: 0.1, y: 0.2, width: 0.8, height: expect.closeTo(0.05) }, confidence: 0.98, model: "claude-haiku-5-5", verification: "verified", itemRef: "31:p1-l1", }) }) it("asks for review when the platform does, when a check failed, or when the quote is not on the page", () => { expect( candidateOf(item({ agreement: { status: "review_required", reader: true, model: false } }), result, pageOf) ?.verification, ).toBe("needs_review") expect( candidateOf(item({ checks: [{ check: "unit_known", status: "failed" }] }), result, pageOf)?.verification, ).toBe("needs_review") expect(candidateOf(item(), result, () => page(0, "another page"))?.verification).toBe("needs_review") }) it("proposes nothing for an item without a citation, and no reference without a run", () => { expect(candidateOf(item({ citations: [] }), result, pageOf)).toBeUndefined() expect(candidateOf(item(), { ...result, run_id: null }, pageOf)?.itemRef).toBeUndefined() }) it("cites only a PDF page: an item cited by a sheet's cells alone is not proposed", () => { const sheet: ItemCitation = { kind: "sheet", file_sha256: SHA, sheet: "BOQ", cell_range: "A3:F3", quote: "1 Kabelis", } expect(candidateOf(item({ citations: [sheet] }), result, pageOf)).toBeUndefined() expect(candidateOf(item({ citations: [sheet, cited(1)] }), result, pageOf)).toMatchObject({ page: 1 }) }) it("keeps no box it cannot place: no geometry, no page size, or an empty box", () => { expect(boxOf(null, first)).toBeUndefined() expect(boxOf([0, 0, 10, 10], undefined)).toBeUndefined() expect(boxOf([10, 10, 10, 20], first)).toBeUndefined() expect(boxOf([-5, 0, 2000, 2000], first)).toEqual({ x: 0, y: 0, width: 1, height: 1 }) }) it("reads a label/value item with no unit as its quantity text", () => { expect( labelAndValueOf( item({ name: "Total", unit: { text: "", code: null }, quantity: { value: null, text: "EUR 5", column: "", refusal: "not_a_number" }, }), ), ).toEqual({ label: "Total", value: "EUR 5", }) }) it("runs the profile over the job that read the document, once per job, and leaves drawings out", async () => { const submit = vi.fn(async () => ({ id: 31, jobId: 4, task: "items" as const, status: "queued" as const }) as never) const wait = vi.fn(async () => ({ schema_version: "conusai.documents.items/v1" as const, job_id: 4, run_id: 31, file_sha256: SHA, profile: { id: "bill_of_quantities", version: 1 }, pipeline_revision: "x", models: result.models, pages: [], items: [item(), item({ id: "p2-l1", citations: [cited(2)] })], numbering: [], usage: { pages_read: 2, pages_sent_to_model: 1, input_tokens: 1, output_tokens: 1, cache_read_tokens: 0, cache_write_tokens: 0, wall_ms: 1, api_ms: 1, cost_usd: "0", }, warnings: [], })) const document: ReadDocument = { engine: { name: "t", version: "1" }, page_count: 2, pages: [first, page(1, "1 Kabelis Al 4x25 m 120", "drawing")], } const extractor = platformItemsExtractor({ items: { submit, wait }, profile: "bill_of_quantities", keep: ["material"], }) const extraction = await extractor.extract({ document, ocrJobId: 4, pageImage: async () => undefined, cache: noCache, }) expect(submit).toHaveBeenCalledWith(4, { profile: "bill_of_quantities", keep: ["material"], idempotencyKey: "documents.items.4.bill_of_quantities", }) expect(extraction.lines.map((line) => line.page)).toEqual([1]) expect(extraction.skipped).toEqual([{ page: 2, pageClass: "drawing" }]) expect(extractor.version).toBe("items.bill_of_quantities.v1") await expect( extractor.extract({ document, pageImage: async () => undefined, cache: noCache }), ).rejects.toBeInstanceOf(ExtractionFailed) }) }) describe("a decision as item feedback", () => { const change = { lineKey: "f".repeat(64), label: "Kabelis Al 4x25", value: "120 m", page: 1, position: 0, quote: "1 Kabelis Al 4x25 m 120", confidence: 0.98, verification: "verified", edited: false, source: { fileSha256: SHA, ocrJobId: 4, ocrEngine: { name: "t", version: "1" }, model: "claude-haiku-5-5", promptVersion: "items.bill_of_quantities.v1", itemRef: "31:p1-l1", }, } it("names the run and the item, and sends the reviewer's words only for a correction", () => { expect(itemRefOf(change)).toEqual({ runId: 31, itemId: "p1-l1" }) expect(feedbackOf("approved", change)).toEqual({ runId: 31, feedback: { item_id: "p1-l1", decision: "approved" } }) expect(feedbackOf("rejected", change)).toEqual({ runId: 31, feedback: { item_id: "p1-l1", decision: "rejected" } }) expect(feedbackOf("approved", { ...change, edited: true, value: "125 m" })).toEqual({ runId: 31, feedback: { item_id: "p1-l1", decision: "edited", fields: { label: "Kabelis Al 4x25", value: "125 m" } }, }) }) it("is nothing for a line the platform did not extract", () => { const { itemRef: _, ...source } = change.source expect(feedbackOf("approved", { ...change, source })).toBeUndefined() expect(feedbackOf("approved", { not: "a line" })).toBeUndefined() }) })