// The ports the documents module's use cases are written against. Core imports no SDK (spec ยง2), so // the shapes of an OCR result and of an extractor are declared here, structurally: the SDK's own // `OcrDocument` is assignable to `ReadDocument`, and the adapter in adapters/ocr.ts is the one place // that knows the SDK exists. import { type TenantScope, token } from "@conusai-kit/core" import type { FailureCode, LineNote, LineVerification, PageClass } from "../contract/index.js" import type { StageCache } from "./stage-cache.js" /** `[x0, y0, x1, y1]` in pixels of the page image, origin top-left. */ export type PixelBox = readonly [number, number, number, number] export interface ReadLine { /** The line as the reader returned it. A quote is this string, unchanged. */ readonly text: string readonly bbox: PixelBox /** 0 to 1. */ readonly confidence: number /** Reading order within the page. */ readonly order: number /** True when the line's text is on most pages of the document: a running header, footer or stamp, not the document's content. */ readonly repeated: boolean } /** A run of lines the reader grouped, with the stable id a citation points at. */ export interface ReadBlock { /** `p{page}-b{n}`: the 1-based page of the source document and the block's place in reading order. */ readonly id: string readonly bbox: PixelBox readonly order: number /** Indexes into the page's `lines`. */ readonly lines: readonly number[] } export interface ReadPage { /** 0-based position in the source document. */ readonly index: number readonly width: number readonly height: number /** What the page is mostly made of; decides whether the extraction reads it, and whether it is shown the image. */ readonly class: PageClass /** `text_layer`: read from the file's own text; `ocr`: recognised from the page image (a scan). */ readonly source?: "ocr" | "text_layer" | "spreadsheet" | undefined /** The page's text in reading order, one row per line. */ readonly text: string readonly lines: readonly ReadLine[] readonly blocks: readonly ReadBlock[] } export interface ReadDocument { readonly engine: { readonly name: string; readonly version: string } readonly page_count: number readonly pages: readonly ReadPage[] } /** A box as fractions of the page, so it survives a render at any width (kit-proposals AT-D27). */ export interface FractionBox { readonly x: number readonly y: number readonly width: number readonly height: number } /** What an extractor proposes: a value, where it was read, and the words it was read from. */ export interface CandidateLine { readonly label: string readonly value: string /** 1-based. */ readonly page: number /** What the extractor quoted: verbatim from the page text, unless `verification` says it was not found there. */ readonly quote: string /** The reader's id of the block the quote sits in, when the quote was found on the page's lines. */ readonly blockId?: string /** The block's box (or, without a block, the box of the lines the quote falls on). */ readonly box?: FractionBox /** The reader's confidence in the words, 0 to 1. */ readonly confidence: number /** Set by code, never by a model: was the quote found on the page it cites. */ readonly verification: LineVerification /** Why a reviewer should look at the line besides `verification`. */ readonly note?: LineNote } /** The image of one page, for a model that must read what the text layer could not. */ export interface PageImage { readonly bytes: Uint8Array /** `image/png` or `image/jpeg`. */ readonly mediaType: string } /** What an extractor is given: the read document, and what it needs to avoid paying twice. */ export interface ExtractionInput { readonly document: ReadDocument /** The image of a 0-based page, or undefined when there is none to give. */ readonly pageImage: (pageIndex: number) => Promise /** Model replies are cached here by content hash, so a retried job pays for nothing twice. */ readonly cache: StageCache readonly signal?: AbortSignal } /** The failure codes an extraction can end a document with. */ export type ExtractionFailureCode = Extract /** * A permanent failure of the extraction, as a code the document records: the model declined * (`extraction_refused`), kept running into its token ceiling (`extraction_truncated`) or never * answered in the schema (`extraction_invalid`), each after one retry. Anything else an extractor * throws is transient, and the queue retries the job. */ export class ExtractionFailed extends Error { constructor(public readonly code: ExtractionFailureCode) { super(`the extraction failed: ${code}`) this.name = "ExtractionFailed" } } /** A page the extraction did not read, and the class that left it out. */ export interface SkippedPage { /** 1-based. */ readonly page: number readonly pageClass: PageClass } /** What an extractor produced: the lines it proposes, and the pages it left out on purpose. */ export interface Extraction { readonly lines: readonly CandidateLine[] readonly skipped: readonly SkippedPage[] } /** * Turns a read document into candidate lines. The template ships one behind the `ModelGateway` * port (adapters/model-extractor.ts); a product replaces it -- a table reader, a domain's own rules, * a different prompt -- by passing its own to the job, and nothing else in the module changes. */ export interface LineExtractor { /** Recorded on every proposal as its prompt version: `.v`. */ readonly version: string /** Recorded on every proposal as its producing model, besides the OCR engine. */ readonly name: string /** Throws `ExtractionFailed` for a failure no retry would change. */ extract(input: ExtractionInput): Promise } /** * Asynchronous OCR: `submit` returns when the platform has accepted the document, `wait` when it * has read it. Two calls, because a retried job resumes a job it already submitted instead of * paying for the document twice. */ export interface DocumentOcr { submit(bytes: Uint8Array, options: { filename: string; idempotencyKey: string }): Promise<{ jobId: number }> /** Throws `OcrJobFailed` when the platform says the job failed, which no retry will change. */ wait(jobId: number, options?: { signal?: AbortSignal }): Promise /** * The image of a 1-based page of a finished job, drawn by the platform. Undefined when the platform * has none to give (the job's input expired, the page is past the end): the page is then read from its text. */ page(jobId: number, page: number, options?: { signal?: AbortSignal }): Promise } /** The platform read the document and could not: permanent, so the document is marked failed. */ export class OcrJobFailed extends Error { constructor(message = "the OCR job failed") { super(message) this.name = "OcrJobFailed" } } // An OCR read is metered and leaves the process: only an effect (a job) may resolve it. export const DOCUMENT_OCR = token("documents.ocr", { access: "effect" }) /** * The project a job runs in, with a system actor. The worker binds it per job from the enqueueing * scope (the same arrangement as `FILES_SCOPE`, decision AT-F6): a payload never names a tenant. */ export const DOCUMENTS_SCOPE = token("documents.scope")