ovrin

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

README

ovrin

CI Go Reference

Turn documents into structured data.

Ovrin is a Go library that reads PDFs, scans and images and returns a typed Go struct — with per-field confidence, a record of where every value came from, and an explicit signal when a human should look at it.

Define what you want:

type Invoice struct {
    Number   string  `ovrin:"invoice number,required"`
    Vendor   string  `ovrin:"vendor company name"`
    Currency string  `ovrin:"currency code,required,enum=UGX|USD|EUR|GBP"`
    Total    float64 `ovrin:"total amount including tax,required,min=0"`
}

Ask for it:

res, err := ovrin.Extract[Invoice](ctx, client, ovrin.File("invoice.pdf"))
if err != nil {
    return err
}

fmt.Println(res.Data.Total)        // 2500.00, a float64
fmt.Println(res.Confidence)        // 0.96
fmt.Println(res.NeedsReview)       // false

Ovrin handles the rest: detecting the format, reading the text layer, rasterising and running OCR when there isn't one, normalising the content, constraining the model to your schema, validating the result, checking that every value actually appears in the document, and scoring what it found.


Why ovrin

Typed, not map[string]any res.Data.Total is a float64 at compile time. Rename a field and the compiler finds every use.
A pipeline, not a prompt Text layer first, OCR on demand, vision as a distinct reading. Staged extraction measurably beats handing a model raw pages.
Confidence you can decompose Every score breaks down into named signals. No number is produced that you cannot take apart.
Every value points back Page, bounding box and source span for each field. Review interfaces can highlight; auditors can check.
Fabrication is detected Values that appear nowhere in the document are flagged, not returned as fact.
Provider independent Three small interfaces. Bring OpenAI, Anthropic, Gemini, Tesseract, Textract, Ollama, or your own.
Zero dependencies in the core go get pulls nothing. No cgo. Cross-compiles and builds static.
Untrusted input by default Documents are parsed with finite limits and prompted as data, never as instruction.

What it is not

Ovrin is not "send a PDF to a model and get JSON back". That takes an afternoon, costs more, and is measurably less accurate on real documents. It also cannot tell you how confident to be, where a value came from, or whether the model invented it — which is the part that matters when the extracted number is a payment.


Install

go get github.com/BAGOMBEKA-JOB-DEV/ovrin

The core has no external dependencies. You add exactly the providers you use, and nothing else enters your go.sum:

go get github.com/BAGOMBEKA-JOB-DEV/ovrin/model/skyl      # OpenAI, Anthropic, Gemini, Ollama, …
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/ocr/tesseract   # local OCR
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/ocr/google      # Cloud Vision / Document AI
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/render/pdfium   # v0.2 — rasterise scanned PDFs, no cgo
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/otel            # v0.2 — OpenTelemetry

Go 1.22 or newer. (That is a language floor. For the toolchain to build with, see SECURITY.md.)

Development

Every command in this project is a make target. Nothing is hidden in a script or a CI file — the Makefile is the single definition, and CI calls these same targets, so a green run on your machine is a green run on the build.

Setting up
git clone https://github.com/BAGOMBEKA-JOB-DEV/ovrin.git
cd ovrin

make setup      # commit sign-off hook + golangci-lint and govulncheck at CI's versions
make check      # the whole gate, across all nine modules

That is the entire setup. No credentials are needed to build or test — the default suite runs against in-process fakes and loopback servers, offline (ADR-0022).

Run make with no arguments at any time to list every target.

The two you will use most
Command What it does
make check The gate to pass before opening a pull request: gofmt, build, go vet under every build tag, tests with the race detector, tests over real sockets, go mod tidy, golangci-lint, govulncheck, documentation checks — for every module.
make ci Everything above plus what only CI used to do: the coverage floor, the zero-dependency assertion, and the cgo-free cross-compile.
Every target

Getting started

Command What it does
make / make help List every target
make setup Install the sign-off hook and both tools
make hooks Just the Signed-off-by commit hook
make tools Just golangci-lint and govulncheck, at the versions CI pins

Build and test

Command What it does
make build Compile every module
make test The offline suite, with the race detector
make test-sandbox The same over real sockets, against an adversarial fake server
make test-cover The suite with a coverage profile — what CI runs
make cover-floor Assert coverage is at or above 85%
make cover-html Open the coverage profile in a browser
make bench Benchmarks (render/pdfium)
make fuzz Every fuzz target; FUZZTIME=5m make fuzz to run longer
make test-integration Against real providers. Costs money
make eval Accuracy against the corpus. Needs OPENAI_API_KEY, costs money

Quality — each is one step of CI

Command What it does
make fmt Format every module
make fmt-check Fail if anything is not gofmt'd
make vet go vet under every build tag
make lint golangci-lint
make actions Validate the GitHub Actions workflow files
make vuln govulncheck
make tidy / make tidy-check go mod tidy; the check fails if it left a diff
make deps-check Assert the core has zero external dependencies
make cross Assert it builds with CGO_ENABLED=0 for linux/arm64, darwin/arm64, windows/amd64

Documentation and generated files

Command What it does
make docs Check links, citations, ADR hygiene and API references
make api Regenerate api/ovrin.txt from the source
make corpus Regenerate the synthetic evaluation corpus
make report Regenerate the committed no-run evaluation report

Running and releasing

Command What it does
make run-example Extract the example receipt with a real model. Needs OPENAI_API_KEY
make release-check VERSION=v0.3.0 Report whether the tree is fit to tag. Never tags, never pushes. Takes a module-prefixed tag too, e.g. model/skyl/v0.1.0
make clean Remove build and coverage output

Docker — the toolchain pinned, nothing to install

Command What it does
make docker-build Build the image
make docker-ci The whole gate, in a container
make docker-shell A shell with the toolchain, your checkout mounted
make docker-test Just the test suites
make docker-test-offline The suite with --network=none, proving it needs no network
make docker-example The receipt example. Pass OPENAI_API_KEY through
make docker-eval The evaluation harness, with eval/report mounted back out
make docker-clean Remove the images

The container is worth knowing about for two specific reasons.

It ships Tesseract's English language data, so the six engine-backed tests in ocr/tesseract that skip on a machine without a language pack actually run there.

And it pins the Go toolchain. make vuln reports vulnerabilities in the standard library of whichever Go you are running, not only in ovrin — so on an older toolchain it fails with a long list that no change to this repository can fix. If that happens, either upgrade Go or run make docker-ci, which uses a pinned modern one. See SECURITY.md for why the go 1.22 in go.mod is a language floor and not a claim that 1.22.0 is safe to run this on.

Working on one module

The repository is nine Go modules. Every target loops over all of them; pass MODULES to narrow it, which is exactly how CI's matrix invokes them:

make test MODULES=ocr/azure
make build MODULES="ocr/azure ocr/textract"
Environment variables

None are needed for make check. These matter only for the targets that contact a real provider:

Variable Used by Notes
OPENAI_API_KEY make run-example, make eval Required by both; they refuse to start without it
OPENAI_BASE_URL make eval Defaults to https://api.openai.com/v1
OVRIN_MODEL make run-example Defaults to gpt-5.2
OVRIN_EVAL_MODEL make eval Defaults to gpt-5.2

Adapters never read the environment themselves — every credential is a function argument (rules.md §6.4). The variables above are read by the example programme and the evaluation harness, which are programmes rather than library code.

Inputs

Input v0.1 How
PDF with a text layer yes read directly — exact and nearly free
PNG, JPEG, TIFF yes OCR or vision
Scanned PDF yes, via cloud OCR providers that accept a PDF rasterise server-side
Scanned PDF, offline yes render/pdfium rasterises locally, ocr/tesseract reads — neither needs cgo or a network
DOCX, XLSX, CSV yes read directly; no OCR and no renderer

Document types are never hardcoded. Invoices, receipts, government forms, transcripts, bank statements, medical forms and contracts are all the same mechanism — you write a struct.


Reading a result

type Result[T any] struct {
    Data        T                        // typed, partially populated
    Valid       bool                     // every validation rule passed
    Confidence  float64
    Fields      map[string]FieldResult   // one per schema field
    NeedsReview bool
    Reasons     []ReviewReason
    Metadata    Metadata
}

err != nil means nothing usable came back. It does not mean the data is good — that is Valid. A field that could not be read is marked absent and is never filled with a zero value, because a payments system must be able to tell "the total is zero" from "we could not read the total".

res, err := ovrin.Extract[Invoice](ctx, client, ovrin.File("invoice.pdf"))
if err != nil {
    return err                              // unreadable, no provider, limit hit
}
if !res.Valid || res.NeedsReview {
    return review.Queue(res)                // usable, but not automatically
}
return ledger.Post(res.Data)

Ask why:

e, _ := res.Explain("total")
fmt.Println(e)
Field:       total
Value:       2500.00
Confidence:  0.99

Signals
  grounding    1.00  ×0.30   found verbatim, page 1
  ocr          0.97  ×0.20   12 backing words, mean 0.97
  schema       1.00  ×0.15   float64, min=0 satisfied
  cross_field  1.00  ×0.05   line items sum to total
  format       1.00  ×0.05   parsed as currency
  agreement       —          only one reading

Provenance
  ocr:tesseract   page 1   box (412,688)-(486,702)   exact

Validation
  required  pass
  min=0     pass

Documentation

Document What it covers
Getting started First extraction, end to end
The idea The problem, the goals, and the non-goals
Architecture Modules, seams, and which way the arrows point
Pipeline All nine stages in detail
Schemas The tag grammar and the rule vocabulary
Confidence Signals, weights, and what the number does not mean
Explainability Provenance, review, and audit
Observability Hooks, spans and metric names — all of them API
Threat model Prompt injection, resource limits, exfiltration
Data handling What leaves the process, and to whom
Providers Writing an adapter
Feature matrix What each provider supports — and silently ignores
Evaluation How accuracy is measured
Roadmap What is next, and what is deliberately deferred
Rules The engineering rules this codebase is held to
Decisions 31 ADRs — why it is like this
Glossary Terms used throughout

Contributors and coding agents should start with AGENTS.md.


Status

Pre-v1. The library is implemented — nine Go modules, the core with zero dependencies, and every feature on the roadmap through v0.3 — on top of thirty-one architecture decision records, most of them written before the code.

What that means concretely:

  • No release is tagged yet. The install commands above will not resolve until one is. Until then, build from a checkout.
  • The API is not stable. What the documentation shows is what the code does — the two are checked against each other on every commit — but it will change as it meets real documents.
  • No accuracy figure has been published, and none will be until the evaluation harness can reproduce it (ADR-0023).
  • Confidence weights are provisional. Confidence is a ranking signal today, not a probability. See docs/confidence.md.

Ovrin will remain on v0 until the design has been used on real documents by people who are not the maintainer. The conditions for v1.0 are written down in ADR-0024.

Contributing

Read CONTRIBUTING.md and docs/rules.md. Commits are Conventional Commits and must be signed off (DCO). The most valuable contribution right now is a document for the evaluation corpus that we are legally allowed to redistribute.

License

Apache-2.0. See LICENSE and NOTICE.

Documentation

Overview

Package ovrin turns documents into structured data.

Give it a Go struct describing what you want and a document — a PDF, a scan, a photograph — and it returns that struct populated, alongside the evidence for every value: a confidence score you can decompose, a record of which page and which region each value came from, and an explicit signal when a person should look before the data is used.

type Invoice struct {
	Number   string  `ovrin:"invoice number,required"`
	Vendor   string  `ovrin:"vendor company name"`
	Currency string  `ovrin:"currency code,required,enum=UGX|USD|EUR|GBP"`
	Total    float64 `ovrin:"total amount including tax,required,min=0"`
}

c := ovrin.New(ovrin.WithModel(model))

res, err := ovrin.Extract[Invoice](ctx, c, ovrin.File("invoice.pdf"))
if err != nil {
	return err
}
if !res.Valid || res.NeedsReview {
	return review.Queue(res)
}
return ledger.Post(res.Data)

Two answers, not one

A non-nil error means nothing usable came back: the source could not be read, no provider was configured for it, a limit was exceeded, or the context was cancelled. It does not mean the data is good — that is Result.Valid.

A field that could not be read is marked absent rather than filled with a zero value. A payments system must be able to tell "the total is zero" from "we could not read the total", so FieldResult.Found reports presence and nothing is ever guessed to satisfy a struct.

Not a prompt with extra steps

Ovrin runs a staged pipeline rather than handing a document to a model and hoping. When a PDF carries its own text, reading it is exact and nearly free; rendering those characters to pixels for a model to read back is a lossy round trip. OCR runs when there is no text layer. Vision is a third reading, not a shortcut past the pipeline.

That staging is also what makes the rest possible. Confidence is computed from named signals that fail in uncorrelated ways — whether the value appears in the document at all, how cleanly the characters read, whether it satisfies its declared rules, whether it agrees with its siblings — because a model's self-reported confidence is uncorrelated with correctness, and token logprobs are unavailable on one major provider and saturate to a constant under the constrained decoding used here.

Provider independent

Three small interfaces — Model, OCR and Renderer — and no vendor is privileged. Implementations live in their own modules, so a user who wants Tesseract does not inherit a cloud SDK, and a user who wants neither inherits nothing.

This package has no external dependencies and uses no cgo, so it cross-compiles and builds static.

Untrusted input

Documents arrive from claimants, suppliers and email attachments. They are parsed with finite limits on every dimension, and their text reaches a model as data, never as instruction. Ovrin does not promise that prompt injection is impossible — nobody can — but a value an injected instruction produces tends not to appear anywhere in the document, and ovrin reports that rather than accepting it silently.

See docs/pipeline.md for the nine stages, docs/schema.md for the struct tag grammar, docs/confidence.md for what the score does and does not mean, and docs/threat-model.md before processing documents from the public.

Index

Examples

Constants

View Source
const (
	// SignalOCR is character-recognition confidence over the words backing
	// the value.
	SignalOCR = "ocr"

	// SignalSchema is whether the value satisfied its declared type and rules.
	SignalSchema = "schema"

	// SignalCrossField is consistency with sibling fields — line items summing
	// to a total, an issue date before a due date.
	SignalCrossField = "cross_field"

	// SignalAgreement is whether two independent readings produced the same
	// value. Available only when two readings ran.
	SignalAgreement = "agreement"

	// SignalFormat is whether the value matches the expected shape for its
	// kind: a date that parses, a currency code that exists.
	SignalFormat = "format"

	// SignalGrounding is whether the value actually appears in the source.
	//
	// The cheapest strong signal available, and the one that catches the
	// failure that matters most: a value appearing nowhere in the document it
	// was read from was not read from it.
	SignalGrounding = "grounding"
)

The names of the built-in confidence signals.

These are untyped string constants rather than a named enum type, because the set is open at the edges: a custom Scorer fitted to somebody's own corpus may emit signals ovrin has never heard of, and a named type would either forbid that or need an escape hatch that defeats the point. This is the same shape as http.MethodGet — well-known values in an open set.

The six below are what ovrin's own scorer produces. A signal that does not apply to a field is absent, not zero: a text-layer PDF has no OCR signal, and scoring that as 0.0 would penalise the most accurate acquisition path ovrin has.

View Source
const (
	// DefaultMaxSourceBytes bounds the source document.
	DefaultMaxSourceBytes int64 = 64 << 20 // 64 MiB

	// DefaultMaxDecompressedBytes bounds decompressed output across the whole
	// document. Cumulative, because a thousand streams of one mebibyte is the
	// same attack as one stream of a gibibyte.
	DefaultMaxDecompressedBytes int64 = 512 << 20 // 512 MiB

	// DefaultMaxStreamBytes bounds decompressed output from any single stream.
	DefaultMaxStreamBytes int64 = 64 << 20 // 64 MiB

	// DefaultMaxTextBytes bounds extracted text.
	DefaultMaxTextBytes int64 = 32 << 20 // 32 MiB

	// DefaultMaxPages bounds the page count. Also bounds spend: ten thousand
	// pages sent to a per-page-priced OCR provider is not a crash, it is an
	// invoice.
	DefaultMaxPages = 1000

	// DefaultMaxDepth bounds recursion through the object graph.
	DefaultMaxDepth = 64

	// DefaultMaxObjects bounds the object count.
	DefaultMaxObjects = 500_000

	// DefaultMaxPagePixels bounds one rasterised page.
	DefaultMaxPagePixels = 50_000_000 // 50 M
)

The default limits.

Every limit has a default and every default is finite. Ovrin parses attacker-controlled binary formats, and the resource attacks against them are documented rather than hypothetical: a 600 KB PDF whose streams are nested FlateDecode expands to ten gigabytes in memory, a cross-reference cycle recurses until the stack is gone, a media box can declare a page that rasterises larger than physical memory.

These numbers are judgement, not measurement — round values chosen to sit comfortably above real documents and comfortably below dangerous ones. They will be revised against the evaluation corpus. Raising a default is not breaking; lowering one is.

They are exported so a caller raising a limit can express it relative to the default rather than restating a number that may change.

See docs/adr/0020-resource-limits.md.

View Source
const (
	// DefaultReviewThreshold is the field confidence below which a result is
	// flagged for review.
	DefaultReviewThreshold = 0.70

	// DefaultMinTextDensity is the minimum characters per square inch for a
	// page's text layer to be considered usable.
	DefaultMinTextDensity = 0.5

	// DefaultMaxReplacementRatio is the maximum proportion of U+FFFD
	// replacement characters a usable text layer may contain.
	DefaultMaxReplacementRatio = 0.02

	// DefaultMinDecodableRatio is the minimum proportion of characters that
	// must map through a ToUnicode entry or a standard encoding for a text
	// layer to be considered usable.
	//
	// A PDF with a broken ToUnicode table can produce plausible-looking
	// rubbish that would poison everything downstream, so a page failing this
	// falls through to OCR rather than being trusted.
	DefaultMinDecodableRatio = 0.90
)

The default policy thresholds.

View Source
const (
	WeightGrounding  = 0.30
	WeightAgreement  = 0.25
	WeightOCR        = 0.20
	WeightSchema     = 0.15
	WeightFormat     = 0.05
	WeightCrossField = 0.05
)

The weights the default scorer gives each signal.

They are **provisional**. Until they are calibrated against a corpus, confidence is a ranking signal — good for ordering a review queue — and not a probability. Nothing here means "correct this often", and docs/confidence.md says so at greater length.

defaults_doc_test.go asserts these equal the table in docs/confidence.md, so changing one here without changing the other turns the build red.

View Source
const (
	// CapRuleFailed applies when a declared rule other than required failed.
	CapRuleFailed = 0.40

	// CapUngrounded applies when the value appears nowhere in the source. It
	// is lower than CapRuleFailed because a well-formed value that is not in
	// the document is the more dangerous of the two: it looks correct.
	CapUngrounded = 0.35

	// CapDisagreement applies when two readings produced different values.
	CapDisagreement = 0.50

	// CapSuspicious applies when the source page carried content that looked
	// like an injection attempt.
	CapSuspicious = 0.60
)

The ceilings applied after the weighted mean.

A floor exists where averaging would be wrong: a value that satisfies four signals and fails its declared rule is not four-fifths correct, it is unusable. Averaging alone would let strong signals hide the one that matters.

View Source
const DefaultBreakerCooldown = 30 * time.Second

DefaultBreakerCooldown is how long a breaker stays open before it tries again.

Thirty seconds is long enough that a provider restarting or a rate limit resetting has had a chance, and short enough that recovery is not something a person has to notice and act on.

View Source
const DefaultBreakerFailures = 5

DefaultBreakerFailures is how many consecutive failures open a breaker.

Five, because a provider that has failed five times in a row is not having a bad moment. Lower trips on ordinary noise and sends traffic to a fallback that may be worse; higher spends real money and latency discovering something already known.

Variables

View Source
var (
	// ErrUnsupportedFormat means the source is not a format ovrin can read.
	// Format is determined by content, so this is not a filename problem.
	ErrUnsupportedFormat = errors.New("ovrin: unsupported document format")

	// ErrNoContent means the document was read but yielded nothing usable —
	// a PDF whose text layer decodes to rubbish, or a blank scan.
	ErrNoContent = errors.New("ovrin: no readable content in document")

	// ErrNoProvider means no configured provider can read this document. The
	// message names the ways to fix it, because the remedy is never obvious
	// from the condition alone.
	ErrNoProvider = errors.New("ovrin: no provider configured for this document")

	// ErrSchema means the Go type cannot be turned into a schema: an unknown
	// rule, an unsupported field type, a recursive type, a malformed tag. It
	// is raised before any provider is contacted, so a typo costs nothing.
	ErrSchema = errors.New("ovrin: invalid schema")

	// ErrLimitExceeded means a resource limit was reached. The message names
	// the limit and the option that raises it. See
	// docs/adr/0020-resource-limits.md.
	ErrLimitExceeded = errors.New("ovrin: resource limit exceeded")

	// ErrAuth means a provider rejected the credential. A fallback chain never
	// advances past this: a misconfigured key should fail loudly on the first
	// provider rather than silently degrade to the third.
	ErrAuth = errors.New("ovrin: provider authentication failed")

	// ErrRateLimit means a provider is throttling. A fallback chain advances.
	ErrRateLimit = errors.New("ovrin: provider rate limited")

	// ErrUnavailable means a provider could not be reached or returned a
	// server error. A fallback chain advances.
	ErrUnavailable = errors.New("ovrin: provider unavailable")

	// ErrBadResponse means a provider replied with something unusable — most
	// often JSON that does not parse. The offending bytes are attached to the
	// [Error] so the failure can be diagnosed rather than guessed at.
	ErrBadResponse = errors.New("ovrin: provider returned an unusable response")

	// ErrUnsupported means a provider cannot do what was asked: images sent to
	// a model without vision, a URL to an adapter that requires inline data.
	// An adapter returns this rather than quietly producing a worse answer.
	ErrUnsupported = errors.New("ovrin: unsupported by this provider")

	// ErrEncrypted means the document is encrypted. The message names the
	// encryption. Password support is a later decision.
	ErrEncrypted = errors.New("ovrin: document is encrypted")

	// ErrInternal means ovrin failed, rather than the document, a provider or
	// a limit: a broken entropy source, or a pipeline stage handed input its
	// contract forbids.
	//
	// The remedy is distinct and is the reason this exists — file a bug. Do
	// not re-scan the document, switch provider or raise a limit. See
	// docs/adr/0030-an-internal-failure-sentinel.md.
	ErrInternal = errors.New("ovrin: internal failure")

	// ErrBadRequest means a provider rejected a request ovrin considers valid
	// — most often a JSON Schema dialect it does not accept.
	//
	// This is distinct from [ErrSchema], and the distinction is the remedy:
	// ErrSchema means fix the struct, ErrBadRequest means change provider or
	// simplify the schema.
	ErrBadRequest = errors.New("ovrin: provider rejected the request")
)

The conditions an extraction can fail on.

These are the values to test with errors.Is. Nothing in ovrin, and nothing calling it, should ever branch on the text of an error message: a provider rewording a response must not change how a program behaves. See docs/rules.md §2.2.

Functions

This section is empty.

Types

type BatchResult

type BatchResult[T any] struct {
	// Index is the position of this source in the slice passed to
	// [ExtractBatch].
	//
	// Results come back in that same order, so this is redundant for a caller
	// ranging over them — and load-bearing for one that filters, sorts, or
	// collects only the failures and still needs to say which document.
	Index int

	// Result is what was extracted, or nil when Err is set.
	Result *Result[T]

	// Err is why this source produced nothing, or nil.
	//
	// It is classified exactly as [Extract]'s error is: test it with
	// [errors.Is] against the sentinels, never by its text.
	Err error
}

BatchResult is what one source in a batch produced.

Exactly one of Result and Err is set. They are kept as separate fields rather than collapsed into a Result with an error inside it for the reason ADR-0004 gives: an extraction that failed produced nothing usable, and a half-filled Result invites a caller to read fields that were never extracted.

func ExtractBatch

func ExtractBatch[T any](ctx context.Context, c *Client, srcs []Source, opts ...Option) []BatchResult[T]

ExtractBatch extracts from many sources, several at a time.

Results are returned in the order the sources were given, whatever order they finished in. One document failing does not fail the batch: its entry carries the error and every other document is still extracted. That is the difference between a batch API and a loop — a loop that stops at the first bad scan in a thousand-file directory has thrown away nine hundred and ninety-nine good extractions.

Concurrency is bounded by WithConcurrency, which also bounds page-level work inside each extraction. The two multiply, so a batch of eight with a concurrency of four can have thirty-two page reads in flight; set it with the provider's rate limit in mind rather than the machine's core count.

Cancelling ctx stops sources that have not started. Sources already running observe the cancellation through their own provider calls and return whatever error that produced, so a cancelled batch reports per document what happened to it rather than one error for the whole run.

Passing no sources returns nil. That is not an error: a directory with no documents in it is an ordinary thing to point this at.

type BreakerOption

type BreakerOption func(*breaker)

BreakerOption configures a breaker.

func WithBreakerCooldown

func WithBreakerCooldown(d time.Duration) BreakerOption

WithBreakerCooldown sets how long the breaker stays open. A value of zero or less is ignored: a breaker that reopens immediately has not broken anything.

func WithBreakerFailures

func WithBreakerFailures(n int) BreakerOption

WithBreakerFailures sets how many consecutive failures open the breaker. A value below one is ignored, because a breaker that opens on zero failures is a provider that is never called.

type Candidate

type Candidate struct {
	// Value is this reading's answer.
	Value any

	// Reading is which reading produced it.
	Reading Reading

	// Source is where in the document it came from.
	Source Provenance
}

Candidate is one reading's answer for a field, when readings disagreed.

Disagreement is recorded rather than resolved. Two readings fail in uncorrelated ways, so when they differ at least one is definitely wrong and silently preferring either is the failure this exists to prevent.

type Cell

type Cell struct {
	// Row is the 0-based index of the cell's first row.
	Row int

	// Column is the 0-based index of the cell's first column.
	Column int

	// RowSpan is how many rows the cell covers. Zero and one both mean one:
	// providers differ on whether they report a span of one, and normalising
	// it here means every adapter does not have to.
	RowSpan int

	// ColumnSpan is how many columns the cell covers, with zero meaning one,
	// as for RowSpan.
	ColumnSpan int

	// Kind is what the provider said the cell is.
	Kind CellKind

	// Text is the cell's content as the provider read it, unnormalised.
	//
	// It is document content. It belongs on a field or in front of a person,
	// never in a log line, an error, an event or a metric (rule §2.5, §7.5).
	// Use [Table.Ref] when you need to say which cell you mean.
	Text string

	// Box is the cell's region on the page. A zero Rect means the provider
	// gave no geometry.
	Box Rect

	// Confidence is the provider's own for this cell, on 0..1.
	Confidence float64
}

Cell is one cell of a Table.

func (Cell) Columns

func (c Cell) Columns() int

Columns returns how many columns the cell covers, treating an unreported span of zero as one, as for Cell.Rows.

func (Cell) Covers

func (c Cell) Covers(row, column int) bool

Covers reports whether the cell occupies a grid position, spans included.

It is the operation every lookup in a merged table needs: the value under "Quantity" may be stored at row 3 and asked for at row 4 because the row above it spans two, and a caller comparing Row and Column directly would conclude the position is empty.

func (Cell) Rows

func (c Cell) Rows() int

Rows returns how many rows the cell covers, treating an unreported span of zero as one.

It exists so that the "zero and one both mean one" rule Cell.RowSpan states is applied in one place. Every caller that walks a merged table needs it, and a caller that reads RowSpan directly gets a table one row short wherever a provider left the span unreported.

type CellKind

type CellKind string

CellKind is what a provider said a cell is.

The set is closed and is the intersection of what the supported providers report, so all of them map onto it without loss. A provider that says nothing leaves CellUnknown, which is not CellData: "this is a data cell" and "this provider does not label cells" are different facts, and collapsing them would make a header row silently wrong rather than silently absent.

const (
	// CellUnknown is a provider that does not label cells.
	CellUnknown CellKind = ""

	// CellData is a cell the provider labelled as content.
	CellData CellKind = "data"

	// CellColumnHeader labels the column it sits above.
	CellColumnHeader CellKind = "column_header"

	// CellRowHeader labels the row it sits beside.
	CellRowHeader CellKind = "row_header"
)

The cell kinds.

func (CellKind) Header

func (k CellKind) Header() bool

Header reports whether the cell labels other cells rather than carrying content of its own.

func (CellKind) String

func (k CellKind) String() string

String returns the kind as written in a struct tag and in documentation.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client holds the providers, limits and policy an extraction runs under.

Build one and share it: every method is safe for concurrent use by multiple goroutines, and an Extract call never mutates it. A service processing trusted internal documents and untrusted public uploads should hold two, with different limits.

func New

func New(opts ...Option) *Client

New returns a Client configured by opts.

The only required option is WithModel. New panics if given a nil Model, because that is programmer error at construction and the alternative is a nil dereference on the first extraction, thousands of lines from the mistake. Omitting WithModel entirely is configuration rather than a mistake, and surfaces as ErrNoProvider from Extract.

type Content

type Content struct {
	// Reading is which reading produced this content.
	Reading Reading

	// Page is 1-based.
	Page int

	// Text is set when Reading is text or OCR.
	Text string

	// Image is set when Reading is vision. Raw bytes, never base64 — encoding
	// is the adapter's job, and doing it twice corrupts the image.
	Image []byte

	// MediaType is the IANA media type, required when Image is set.
	MediaType string
}

Content is one piece of material handed to a Model. It is always untrusted.

type CrossFieldResult

type CrossFieldResult struct {
	// Name identifies the rule, for a review reason. A name you chose, never
	// a value from the document.
	Name string

	// Fields are the field keys the rule read, so a reviewer knows where to
	// look. Keys, never values (docs/rules.md §7.5).
	Fields []string

	// Applicable reports whether the rule could run at all. A rule whose
	// inputs were not extracted has not failed — the missing field is already
	// reported by its own required rule, and counting it again would punish a
	// document twice for one absence.
	Applicable bool

	// Passed reports the outcome, and is meaningful only when Applicable.
	Passed bool

	// Message says why not, and is empty when Passed. It describes the
	// inconsistency and never states the amounts: these strings reach logs.
	Message string
}

CrossFieldResult is the outcome of one cross-field rule.

Separate from RuleResult because a cross-field rule is not a rule on a field: it names the several fields it read, and it can be inapplicable in a way a field rule cannot.

type CrossFieldRule

type CrossFieldRule interface {
	// Name identifies the rule in results and review reasons. It is a name
	// you choose, and it appears in a ReviewReason, so it must not contain a
	// value from the document.
	Name() string

	// Check reports the outcome.
	Check(f CrossFields) CrossFieldResult
}

CrossFieldRule checks one consistency property across sibling fields.

It is the check no single field can make: line items adding to a total, an issue date before a due date. Those catch the misread digit that every other signal accepts, because a wrong number is still a number and still passes its type, its format and its range.

A rule reads values and returns a verdict. It cannot fail, cannot do I/O and cannot see the document — an inconsistency is a finding, not an error (docs/adr/0004-partial-results.md).

func Before

func Before(earlier, later string) CrossFieldRule

Before returns a rule requiring that one date is not after another.

Equal dates pass: an invoice issued and due the same day is a document, not an inconsistency.

func CrossFieldFunc

func CrossFieldFunc(name string, check func(CrossFields) CrossFieldResult) CrossFieldRule

CrossFieldFunc returns a rule from a function.

The extension point. Consistency worth checking is specific to a document type, and the three rules above are the ones common enough to ship rather than an attempt at a complete set.

func Sum

func Sum(total string, tol Tolerance, parts ...string) CrossFieldRule

Sum returns a rule requiring that named fields add up to a total.

The everyday case is a subtotal and a tax adding to a total: the single most checkable claim on an invoice.

ovrin.Sum("total", ovrin.Tolerance{Absolute: 0.01}, "subtotal", "vat")

func SumItems

func SumItems(total, slice string, tol Tolerance, leaves ...string) CrossFieldRule

SumItems returns a rule requiring that a slice's line items add up to a total.

The leaf fields are multiplied together within each element, so a line's quantity and unit price make its amount:

ovrin.SumItems("total", "items", tol, "quantity", "unit_price")

type CrossFields

type CrossFields map[string]any

CrossFields is the converted value of every extracted field, keyed by the path used in Result.Fields: "total", "vendor.name", "items[0].unit_price".

Only values that converted appear. A rule therefore never sees a fabricated zero, and a missing key means the value was not read — which is what CrossFieldResult.Applicable is for.

func (CrossFields) Count

func (f CrossFields) Count(slice string) int

Count returns how many elements of a slice field were extracted.

func (CrossFields) Number

func (f CrossFields) Number(key string) (float64, bool)

Number returns a numeric field, and whether it was read.

func (CrossFields) Text

func (f CrossFields) Text(key string) (string, bool)

Text returns a string field, and whether it was read.

func (CrossFields) Time

func (f CrossFields) Time(key string) (time.Time, bool)

Time returns a date field, and whether it was read.

type DateOrder

type DateOrder string

DateOrder resolves ambiguous numeric dates such as 03/04/2026.

The zero value does not guess. An ambiguous date is flagged for review instead, because silently reading 3 April as 4 March is exactly the kind of confidently wrong answer this library exists to catch.

const (
	// DateOrderUnknown flags ambiguous dates rather than resolving them.
	DateOrderUnknown DateOrder = ""

	// DayFirst reads 03/04/2026 as 3 April.
	DayFirst DateOrder = "dmy"

	// MonthFirst reads 03/04/2026 as 4 March.
	MonthFirst DateOrder = "mdy"

	// YearFirst reads 2026/03/04 as 4 March.
	YearFirst DateOrder = "ymd"
)

type Document

type Document struct {
	// Kind is the detected format.
	Kind Kind

	// Pages is the page count, or 0 when it is not yet known.
	//
	// Detection resolves it only where the format fixes it structurally — a
	// PNG is one page. Counting the pages of a PDF means parsing the PDF,
	// which is a later stage, and 0 means "not yet known" rather than a
	// placeholder 1 (docs/rules.md §8.5).
	Pages int

	// Size is the length of Data, in bytes.
	Size int64

	// Data is the document itself.
	//
	// A [Renderer] and a [DocumentOCR] are asked to read this document, so
	// they have to be able to reach it — an earlier version of this type
	// carried only metadata, which made both seams unimplementable. The bytes
	// are already in memory by the time a Document exists, so carrying them
	// costs a slice header rather than a copy.
	//
	// Treat it as read-only. It is shared with the pipeline, and it is
	// untrusted (docs/rules.md §7.1).
	Data []byte
}

Document is a Source whose format has been identified.

It is what the pipeline works on after detection, and what a Renderer and a DocumentOCR receive.

type DocumentOCR

type DocumentOCR interface {
	OCR

	// RecogniseDocument reads every page, returning one Recognition per page
	// in page order.
	RecogniseDocument(ctx context.Context, doc Document) ([]*Recognition, error)
}

DocumentOCR is an OCR provider that also accepts a whole document.

Cloud providers that rasterise server-side implement this, and it is what lets a scanned PDF be processed with no local renderer at all — the route that makes scanned documents work before render/pdfium exists.

type Error

type Error struct {
	// Op is the pipeline stage that failed.
	Op Op

	// Provider names the adapter involved, if one was.
	Provider string

	// Page is 1-based, and zero when the failure is not page-specific.
	Page int

	// Field is the schema field, when the failure is field-specific. It is a
	// field name, never a field value.
	Field string

	// Kind is the sentinel this error is an instance of.
	Kind error

	// Message adds detail. It never contains document content.
	Message string
	// contains filtered or unexported fields
}

Error carries the detail behind one of the sentinels above.

Kind holds the sentinel, so errors.Is finds it. Unwrap also returns the underlying cause, so a single value answers both "what kind of failure was this?" and "was it ultimately a cancelled context?".

Message never contains content read from the document. A document is somebody's invoice or medical record, and an error string is a log line that ends up in systems nobody audited. See docs/rules.md §2.5 and §7.5.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

The sentinel's own "ovrin: " prefix is trimmed so it is not printed twice.

func (*Error) Unwrap

func (e *Error) Unwrap() []error

Unwrap returns both the sentinel and the underlying cause, so that errors.Is(err, ovrin.ErrRateLimit) and errors.Is(err, context.Canceled) can both succeed against the same value.

func (*Error) WithCause

func (e *Error) WithCause(err error) *Error

WithCause attaches an underlying error, which becomes reachable through errors.Is and errors.As without appearing in the message.

Adapters live in their own modules and must be able to build an Error that carries both a sentinel and the transport error underneath it — that is the whole promise of the multi-error Error.Unwrap, and without an exported setter an adapter could only satisfy it by printing the cause into the message, which is how a provider quoting a prompt back puts document content in a log line.

There is deliberately no matching getter. ADR-0019 settles that the cause is reached through Unwrap, and a second way to ask the same question is a second thing to keep in step.

type Event

type Event struct {
	// Op is the stage that ran.
	Op Op

	// Provider names the adapter that served it, if one did.
	Provider string

	// Page is 1-based, and zero for whole-document stages.
	Page int

	// Attempt is 1 for the first try.
	Attempt int

	// Duration is how long the stage took.
	Duration time.Duration

	// Err is non-nil if the stage failed. A failed stage still emits an event.
	Err error

	// Bytes read or produced by the stage.
	Bytes int64

	// Pages in the document.
	Pages int

	// Fields is a count. Not the names, and certainly not the values.
	Fields int

	// Usage is what the stage consumed.
	Usage Usage

	// Confidence is the aggregate, set on the final stage only.
	Confidence float64

	// Review reports whether the result needs a person, set on the final
	// stage only.
	Review bool
}

Event is one pipeline stage, reported as it completes.

There is deliberately no field an extracted value could occupy — no map[string]any, no Raw, no free-text note. A span attribute carrying a value would ship somebody's national ID number to an observability vendor they never heard of, and a rule saying "do not do that" gets violated the first time it is convenient. Field counts are here; field values are not representable. See docs/adr/0021-observability.md and docs/rules.md §7.5.

type Explanation

type Explanation struct {
	Field      string
	Value      any
	Found      bool
	Confidence float64

	// Signals are every input to Confidence, with its weight and a one-line
	// note.
	Signals []Signal

	// Provenance is where the value came from.
	Provenance []Provenance

	// Candidates holds competing readings, if any.
	Candidates []Candidate

	// Validation is each rule, whether it passed, and why not.
	Validation []RuleResult

	// Reasons is why this field needs review, if it does.
	Reasons []ReviewReason
}

Explanation is the decomposition of one field's result.

A value rather than formatted text, because the consumers are review queues, audit stores, dashboards and JSON APIs, none of which want a string. The terminal rendering is Explanation.String.

It is assembled from what the pipeline already recorded, so it cannot disagree with the Result it came from.

func (*Explanation) String

func (e *Explanation) String() string

String renders an Explanation for a person reading a terminal.

This format is not part of the compatibility promise. Anyone parsing it has taken a dependency that will break; the struct fields are the stable interface.

type FieldEvidence

type FieldEvidence struct {
	// Field is the field key.
	Field string

	// Value is what was extracted.
	Value any

	// Found reports whether the field was present at all.
	Found bool

	// Reading is which reading produced the value.
	Reading Reading

	// OCRConfidence is the mean confidence of the words backing the value, or
	// nil when the value did not come from OCR.
	OCRConfidence *float64

	// Grounding is on 0..1: 1.0 verbatim, 0.8 normalised, 0.5 derived, 0.0 not
	// found in the source.
	Grounding float64

	// Ambiguous reports whether a declared format parsed under more than one
	// reading — 03/04/2026, which is a valid date in two conventions.
	//
	// It exists so the format signal can drop without going to zero. The text
	// is a well-formed date; what is unknown is which date it is. Scoring that
	// as a format failure would say the value is malformed, which is a
	// different and wrong claim, and would rank it below values that are
	// genuinely unparseable (docs/schema.md, "Ambiguous dates").
	Ambiguous bool

	// Provenance is where the value came from.
	Provenance []Provenance

	// Candidates holds competing values when two readings disagreed.
	Candidates []Candidate

	// Agreement is whether two independent readings produced the same value,
	// or nil when only one reading ran.
	//
	// Nil rather than zero, because "no second opinion was taken" and "the
	// second opinion differed" are opposite facts and scoring them alike would
	// penalise every single-reading extraction (docs/confidence.md §Signals).
	Agreement *float64

	// AgreementNote says which, in one line, for the signal's Note.
	AgreementNote string

	// Validation is each rule and its outcome.
	Validation []RuleResult

	// Suspicious reports whether the source page carried content that looked
	// like an injection attempt.
	Suspicious bool
}

FieldEvidence is everything the pipeline collected about one field.

It is the input to a Scorer, and it is deliberately everything rather than a summary: a caller fitting a scorer to their own labelled documents should not be limited to the signals ovrin happened to think of.

type FieldResult

type FieldResult struct {
	// Value is what was extracted.
	Value any

	// Found reports presence.
	//
	// This is not Value != zero, and the distinction is the point: a payments
	// system must be able to tell "the total is zero" from "we could not read
	// the total".
	//
	// Found does not promise Value is usable. A field the model answered with
	// something that could not be converted to its declared type is Found with
	// a nil Value and Valid false, because "answered unusably" and "not
	// answered at all" are different facts and both are worth having. Check
	// Valid, or take Value through a comma-ok assertion, before using it.
	Found bool

	// Confidence is on 0..1.
	Confidence float64

	// Valid reports whether every rule on this field passed.
	Valid bool

	// Signals are the inputs that produced Confidence.
	Signals []Signal

	// Provenance is where the value came from.
	Provenance []Provenance

	// Candidates holds every competing value when two readings disagreed.
	// Value holds the higher-confidence one, so a caller who ignores this
	// still gets the better answer.
	Candidates []Candidate

	// Validation is each declared rule and whether it passed.
	//
	// Distinct from Errors: a rule that failed appears in both, but Validation
	// also records the rules that passed, which is what makes a confidence
	// score checkable by hand rather than merely reported.
	Validation []RuleResult

	// Errors says why the field is not Valid, or why it was not Found.
	Errors []error
}

FieldResult is one field, and the evidence for it.

This type carries []error and so does not marshal usefully to JSON. Use Result.Explain for a value that does.

type Hook

type Hook func(ctx context.Context, ev Event)

Hook receives an Event for each pipeline stage.

Hooks run synchronously on the calling goroutine. A hook that blocks slows the extraction and a hook that does I/O will, which is the caller's responsibility — making it asynchronous is one line in your own hook, and doing it here would hide ordering and leak a goroutine per client.

The core emits hooks and depends on nothing. OpenTelemetry lives in its own module, so a user who wants no telemetry carries none and a user on a different stack writes five lines instead of adopting OTel.

type Kind

type Kind string

Kind is a document format.

It is always determined by content. A file named invoice.pdf that is actually a JPEG is common enough — mail systems rename things — that trusting the name is how a parser gets handed input it was not written for.

const (
	// KindUnknown is the zero value, for a format detection has not resolved.
	KindUnknown Kind = ""

	KindPDF  Kind = "pdf"
	KindPNG  Kind = "png"
	KindJPEG Kind = "jpeg"
	KindTIFF Kind = "tiff"
	KindWebP Kind = "webp"
	KindDOCX Kind = "docx"
	KindXLSX Kind = "xlsx"
	KindCSV  Kind = "csv"
)

func (Kind) String

func (k Kind) String() string

String returns the format name, or "unknown" for the zero value.

type Layout

type Layout struct {
	// Tables are in reading order, top to bottom then left to right.
	Tables []Table

	// Pairs are in reading order.
	Pairs []Pair
}

Layout is the structure a provider recognised on one page: its tables and its key-value pairs.

One Layout per page, matching Recognition, so a provider that rasterises server-side and returns one recognition per page returns one of these per page too and nothing has to be re-split.

An empty Layout and no Layout are different facts. An empty one is a provider that looked and found no structure; no Layout at all is a provider that does not report structure. That is why Recognition.Layout is a pointer, and it is the whole reason this type exists rather than two slices on Recognition: a caller deciding whether to fall back to reading the page as prose needs to tell "there are no tables here" from "nobody looked".

Nothing in ovrin requires a provider to fill this in. A table detected and reported is a table the model is told about in the page content; a table nobody detected is prose, which still extracts, only with less to go on.

func (Layout) At

func (l Layout) At(r Ref) (Cell, bool)

At returns the cell r names, and whether there was one.

A position no cell covers returns false rather than a zero Cell, because a sparse table's empty position is a place the provider read nothing and an empty Cell would claim it read an empty string. Spans are honoured: a cell covering four positions is returned for all four.

func (Layout) Check

func (l Layout) Check() error

Check reports the first structural mistake in the layout, or nil.

It is what an adapter's own tests run over the layouts they build from recorded provider responses, and it is the reason those tests can be shared: "the cells are inside the table, nothing overlaps, and every confidence is a probability" is the same requirement whichever provider produced them. An adapter that mapped a percentage without dividing it, or flattened a merged cell into two overlapping ones, fails here rather than in a confidence score nobody can explain six weeks later.

Every error wraps ErrBadResponse, because an incoherent layout is a response nothing can be done with. It is deliberately not three new sentinels of its own: ovrin has one error vocabulary and adding to it for one subsystem is how a caller ends up with two (ADR-0027). The message names which check failed and where.

The errors name page, table and cell indexes and nothing else. A cell's text is document content and never appears in one (rule §2.5, §7.5).

func (*Layout) Order

func (l *Layout) Order()

Order puts a provider's output into the order the rest of ovrin assumes, and fills in the boxes a provider left empty.

It exists so that "cells are in reading order" is something an adapter achieves by calling one function rather than something each adapter reimplements — three implementations of reading order is three orders, and the difference only shows up on the documents nobody tested with. It is the counterpart of Layout.Check: Check says whether the structure is coherent, Order says how it is arranged.

The sorts are stable, so a provider that already emits a defensible order keeps it wherever this has no opinion. Order mutates the receiver's slices in place and does not copy: a Layout is built once by an adapter and handed on, and copying every cell to sort it would be the largest allocation on the path for no benefit.

func (Layout) Ref

func (l Layout) Ref(i int, c Cell) Ref

Ref returns the position of c within table i of l, for logging or provenance.

It reads nothing from the cell but its coordinates, which is the point.

type Line

type Line struct {
	Text string
	Box  Rect
	Page int
}

Line is a run of words sharing a baseline.

type Metadata

type Metadata struct {
	// Readings is which readings were taken, in order.
	Readings []Reading

	// Providers names the adapter that served each stage, so a result carries
	// the evidence of where its content went — which matters when a fallback
	// chain means that was not decided in advance.
	Providers map[Op]string

	// Kind is the detected format.
	Kind Kind

	// Pages in the document.
	Pages int

	// Usage is the total across every provider call, the second attempt
	// included when Retried is true.
	Usage Usage

	// Retried reports whether the model was asked a second time because its
	// first reply was malformed — not JSON, or a value that could not be the
	// type the field declares.
	//
	// It is not a warning. A retried extraction that came back clean is as
	// good as one that never needed asking twice; what it tells you is that
	// this provider, prompt or document produced a bad reply once, which is
	// worth knowing in aggregate when choosing between providers.
	Retried bool

	// Duration is the wall time of the extraction.
	Duration time.Duration
}

Metadata records how a result was produced.

type Model

type Model interface {
	Generate(ctx context.Context, req ModelRequest) (*ModelResponse, error)
}

Model produces structured JSON from document content.

One method, deliberately. Ovrin makes exactly one kind of call — given this content and this JSON Schema, return an object matching the schema — so a chat abstraction would make every adapter author implement messages, roles, tool calling and streaming that ovrin never uses.

Prompt construction stays on this side of the seam. An injected instruction in a document must not be able to reach a position where a model reads it as a directive, and that property holds identically across every provider only because the core builds the request. See docs/adr/0007-model-seam.md and docs/adr/0017-untrusted-document-content.md.

Implementations must be safe for concurrent use by multiple goroutines.

func BreakModel

func BreakModel(m Model, opts ...BreakerOption) Model

BreakModel wraps a Model under the same rules as BreakOCR.

func ModelChain

func ModelChain(models ...Model) Model

ModelChain returns a Model that tries each model in order, under the same advance rules as OCRChain.

type ModelRequest

type ModelRequest struct {
	// Instruction is built by ovrin from the schema. It never contains
	// document content.
	Instruction string

	// Content is the untrusted material, already delimited and labelled.
	Content []Content

	// Schema is the JSON Schema the reply must satisfy, as bytes so an adapter
	// can pass it to a provider verbatim.
	//
	// It is emitted fully expanded, with no $ref, additionalProperties false,
	// and every property listed in required — the narrowest dialect the major
	// providers agree on. A provider that still rejects it must surface
	// [ErrBadRequest] naming the construct, never silently relax it.
	Schema []byte

	// Temperature is nil for the provider's default. Extraction wants
	// determinism, so ovrin sets it low rather than leaving it unset.
	Temperature *float64
}

ModelRequest is one extraction call.

Instruction and Content never mix. An adapter maps Instruction to the provider's system role and Content to the user role, or to the nearest equivalent; it never concatenates them.

type ModelResponse

type ModelResponse struct {
	// JSON is the raw reply. It is not unmarshalled by the adapter: a model
	// returning invalid JSON must produce one ovrin error with the offending
	// bytes attached, rather than a different error per provider.
	JSON []byte

	// Usage is what the call consumed.
	Usage Usage

	// Raw is the provider's own response, for callers willing to type-assert.
	Raw any
}

ModelResponse is what a provider returned.

type OCR

type OCR interface {
	// Recognise reads one page.
	Recognise(ctx context.Context, page Page) (*Recognition, error)

	// Name identifies the provider, and appears in [Provenance.Method] so a
	// result records which provider produced each value.
	Name() string
}

OCR recovers text and layout from a rasterised page.

Recognition is not a string. Ovrin needs word positions for provenance and per-word confidence as a scoring signal, so a seam returning text alone would discard the inputs two other subsystems are built on.

Implementations must be safe for concurrent use by multiple goroutines.

func BreakOCR

func BreakOCR(o OCR, opts ...BreakerOption) OCR

BreakOCR wraps an OCR so that a provider which is failing consistently is left alone for a while instead of being asked again on every page.

This is a decorator, not a change to OCRChain, for the reason ADR-0018 gives: fallback policy belongs outside the pipeline, so a caller who wants different policy writes it rather than arguing with ovrin about the built-in one. It composes:

ovrin.OCRChain(
    ovrin.BreakOCR(primary),
    ovrin.BreakOCR(secondary),
)

What it does

After DefaultBreakerFailures consecutive failures the breaker opens and every call returns ErrUnavailable without contacting the provider, for DefaultBreakerCooldown. It then admits exactly one trial call: if that succeeds the breaker closes, and if it fails the cooldown starts again. One trial rather than all of them, because a provider that is still down should cost one request to discover, not a thundering herd of them.

A refusal is ErrUnavailable deliberately. That is a condition OCRChain advances on, so a chain of broken providers moves to the next one rather than stopping — which is the entire point of putting a breaker in a chain.

What it counts

Only failures the provider is responsible for. ErrAuth, ErrBadRequest, ErrUnsupported and ErrSchema do not open a breaker: they will fail identically after any cooldown, so counting them would hide a misconfiguration behind a circuit-breaker message instead of surfacing it. Cancellation is the caller's, not the provider's, and is not counted either.

Every state change is reported through the hook a Client was built with. A breaker that opens silently is the failure ADR-0018 exists to prevent, one level down.

The returned OCR is safe for concurrent use.

func OCRChain

func OCRChain(providers ...OCR) OCR

OCRChain returns an OCR that tries each provider in order.

A chain is an ordinary provider, so the pipeline cannot tell the difference and no fallback logic lives in the core. That also means a caller who wants circuit breaking, weighted routing or cost-aware selection writes their own implementation and passes it to WithOCR.

It advances on ErrRateLimit, ErrUnavailable and unclassified transport errors. It never advances on ErrAuth, ErrBadRequest, ErrUnsupported or ErrSchema: a misconfigured credential should fail loudly on the first provider rather than silently degrade to the third.

Every attempt is reported through the hook, because the dangerous failure mode of fallback is a system running on its worst provider for three weeks with nobody aware. Exhausting the chain returns an error wrapping every attempt, not only the last.

type Op

type Op string

Op names a stage of the extraction pipeline.

The same vocabulary is used by Error and by Event, so an operator reading a trace and a developer reading an error see the same word, and both can look it up in docs/pipeline.md. See docs/adr/0027-twelve-sentinels-and-one-op-vocabulary.md.

const (
	// OpUnknown is the zero value. An Error or Event that does not know its
	// stage says so rather than claiming one.
	OpUnknown Op = ""

	OpDetect    Op = "detect"
	OpAcquire   Op = "acquire"
	OpRender    Op = "render"
	OpOCR       Op = "ocr"
	OpNormalise Op = "normalise"
	OpSchema    Op = "schema"
	OpPrompt    Op = "prompt"
	OpGenerate  Op = "generate"
	OpValidate  Op = "validate"
	OpGround    Op = "ground"
	OpScore     Op = "score"
)

The pipeline stages, in the order a document passes through them.

OpRender and OpOCR happen within acquisition; they are named separately because a failure in either is worth distinguishing from a failure to choose a reading at all.

func (Op) String

func (o Op) String() string

String returns the stage name, or "unknown" for the zero value.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures a Client, or a single Extract call.

The interface is closed — apply is unexported — so options cannot be implemented outside this package. That keeps the configuration surface a thing ovrin controls, and it keeps godoc honest: an exported func type over an unexported struct would render as func(*config), naming a type the reader cannot see.

func WithConcurrency

func WithConcurrency(n int) Option

WithConcurrency bounds page-level parallelism.

The default is min(4, GOMAXPROCS), so ovrin does not monopolise a host it shares. Model calls are not parallelised across pages: extraction needs the whole document.

func WithCrossField

func WithCrossField(rules ...CrossFieldRule) Option

WithCrossField declares rules checked across fields after extraction.

They produce the SignalCrossField signal, and a failure sets NeedsReview with a reason naming the rule.

Rules are declared here rather than in a struct tag because a rule spans several fields and has no natural home on any one of them — and because ADR-0006 fixes the tag vocabulary at five rules, none of which could express "these three fields must add up".

func WithDateOrder

func WithDateOrder(d DateOrder) Option

WithDateOrder resolves ambiguous numeric dates for a corpus whose convention you know. Without it they are flagged rather than guessed.

func WithHook

func WithHook(h Hook) Option

WithHook sets a function called once per pipeline stage.

Hooks run synchronously on the calling goroutine.

func WithMaxDecompressedBytes

func WithMaxDecompressedBytes(n int64) Option

WithMaxDecompressedBytes bounds decompressed output across the document.

func WithMaxDepth

func WithMaxDepth(n int) Option

WithMaxDepth bounds recursion through the document's object graph.

func WithMaxObjects

func WithMaxObjects(n int) Option

WithMaxObjects bounds the number of objects in a document.

func WithMaxPagePixels

func WithMaxPagePixels(n int) Option

WithMaxPagePixels bounds the size of one rasterised page.

func WithMaxPages

func WithMaxPages(n int) Option

WithMaxPages bounds the page count.

func WithMaxReplacementRatio

func WithMaxReplacementRatio(r float64) Option

WithMaxReplacementRatio sets the maximum proportion of U+FFFD characters a usable text layer may contain.

func WithMaxSourceBytes

func WithMaxSourceBytes(n int64) Option

WithMaxSourceBytes bounds the source document.

func WithMaxStreamBytes

func WithMaxStreamBytes(n int64) Option

WithMaxStreamBytes bounds decompressed output from any single stream.

func WithMaxTextBytes

func WithMaxTextBytes(n int64) Option

WithMaxTextBytes bounds extracted text.

func WithMinDecodableRatio

func WithMinDecodableRatio(r float64) Option

WithMinDecodableRatio sets the minimum proportion of decodable characters a usable text layer must contain.

func WithMinTextDensity

func WithMinTextDensity(d float64) Option

WithMinTextDensity sets the minimum characters per square inch for a text layer to be considered usable.

func WithModel

func WithModel(m Model) Option

WithModel sets the model that turns document content into structured JSON. Required.

func WithOCR

func WithOCR(o OCR) Option

WithOCR sets the provider used for pages with no usable text layer.

Without one, a scanned document reaches a vision-capable model if there is one, and otherwise fails with ErrNoProvider.

func WithReading

func WithReading(m ReadingMode) Option

WithReading selects how a document is read.

The default, ReadingAuto, tries the text layer first because when it works it is exact and nearly free. ModeBoth runs two independent readings and compares them, which roughly doubles cost and is the strongest quality signal available.

func WithRenderer

func WithRenderer(r Renderer) Option

WithRenderer sets the renderer used to rasterise pages for OCR.

Not needed for images, nor for a DocumentOCR provider that accepts a PDF and rasterises server-side.

func WithReviewThreshold

func WithReviewThreshold(t float64) Option

WithReviewThreshold sets the field confidence below which a result is flagged for review.

func WithScorer

func WithScorer(s Scorer) Option

WithScorer replaces the confidence scorer.

The default is a weighted mean over the signals that applied, with hard floors. A caller with labelled documents can fit a better one to their own corpus; the consequence is that confidence is then comparable within that deployment rather than across organisations.

type Page

type Page struct {
	// Number is 1-based.
	Number int

	// Image is the rasterised page.
	Image image.Image

	// Width and Height are the page size in points, which is what lets a
	// provider return coordinates in points regardless of the DPI it was
	// given.
	Width  float64
	Height float64

	// DPI is the resolution the page was rendered at.
	DPI int
}

Page is one rasterised page, handed to an OCR provider.

type Pair

type Pair struct {
	// Page is 1-based.
	Page int

	// Key is the label.
	Key Region

	// Value is what the label labels.
	//
	// Its Text may be empty: a form field the provider found and read nothing
	// in is a fact about the document, not an absent pair. Dropping it would
	// turn "this box was blank" into "there is no such box".
	Value Region

	// Confidence is the provider's own for the association, on 0..1.
	//
	// It is about the pairing and not about either region's text. A provider
	// that reports confidence per region and none for the pairing leaves this
	// zero rather than averaging two numbers that mean something else.
	Confidence float64
}

Pair is a label and the thing it labels — a form field, in effect.

func (Pair) Box

func (p Pair) Box() Rect

Box is the region a review interface highlights for the whole pair.

A label and the value it labels are routinely on different lines — that separation is what makes a pair worth reporting rather than five words in reading order — so the pair's region is the union of the two and not either one of them.

type Provenance

type Provenance struct {
	// Reading is which reading produced the value.
	Reading Reading

	// Page is 1-based.
	Page int

	// Box is the region on the page, or nil if the reading gave no geometry.
	// Always present for OCR, usually for the text layer, rarely for vision.
	Box *Rect

	// Span is the range in the normalised text, or nil if unknown.
	Span *Span

	// Method names the reading and the provider that served it, as
	// "text-layer", "ocr:tesseract" or "vision:gpt-5.2".
	Method string

	// Exact reports whether the value appears verbatim in the source, rather
	// than having been reformatted or derived.
	Exact bool
}

Provenance records where a value came from.

It is what makes human review practical — an interface can highlight the region instead of making a reviewer search a 40-page scan — and it is what grounding is built on. It cannot be reconstructed after extraction, which is why it is always collected rather than being an option.

A nil Box or Span means the position is not known, never that the value is not in the document. Some values are legitimately not groundable: a total computed from line items, a date normalised from prose.

type Reading

type Reading string

Reading is how a value was actually read. It describes the past, and appears on Provenance and Candidate.

A value is read by exactly one reading. Requesting more than one is a different type, ReadingMode, so that a Provenance claiming two readings at once is not representable. See docs/adr/0028-reading-and-readingmode.md.

const (
	// ReadingUnknown is the zero value, for a value whose origin was not
	// recorded. It is never a claim that the origin does not exist.
	ReadingUnknown Reading = ""

	// ReadingText is a PDF's own text layer: exact, and nearly free.
	ReadingText Reading = "text"

	// ReadingOCR is optical character recognition of a rasterised page.
	ReadingOCR Reading = "ocr"

	// ReadingVision is a multimodal model reading a page image.
	ReadingVision Reading = "vision"
)

func (Reading) String

func (r Reading) String() string

String returns the reading name, or "unknown" for the zero value.

type ReadingMode

type ReadingMode string

ReadingMode selects how a document should be read. It describes an intention, and is the argument to WithReading.

Distinct from Reading because ModeBoth has no meaning as a record of what happened: two readings produce two Candidate values, not one Provenance claiming both.

const (
	// ReadingAuto tries the text layer, then OCR, then vision, using the first
	// that can serve the page. It is the default, and it is what most callers
	// want: cost scales with document difficulty rather than document count.
	ReadingAuto ReadingMode = "auto"

	// ModeText uses the text layer only, and fails rather than falling back.
	ModeText ReadingMode = "text"

	// ModeOCR rasterises and recognises, even where a text layer exists.
	ModeOCR ReadingMode = "ocr"

	// ModeVision sends page images to the model.
	ModeVision ReadingMode = "vision"

	// ModeBoth runs two independent readings and compares them field by field.
	// Roughly doubles cost and latency, which is why it is not the default;
	// disagreement between readings is the strongest signal ovrin has that a
	// value should not be trusted.
	ModeBoth ReadingMode = "both"
)

type Recognition

type Recognition struct {
	// Words are in reading order.
	Words []Word

	// Lines group the words by baseline.
	Lines []Line

	// Confidence is the provider's own, over the whole page, on 0..1.
	Confidence float64

	// Language is the detected language, or empty if the provider does not
	// report one.
	Language string

	// Layout is the structure the provider recognised on this page — its
	// tables and key-value pairs — or nil for a provider that does not report
	// structure.
	//
	// The pointer is load-bearing. An empty Layout is a provider that looked
	// and found nothing; nil is a provider that does not look. A caller
	// deciding whether to treat the page as a table or as prose needs to tell
	// those apart, and a plain slice cannot say it.
	Layout *Layout

	// Raw is the provider's own response, for callers willing to type-assert.
	// Providers that detect tables or key-value pairs expose them here; ovrin
	// itself uses words and lines.
	Raw any

	// Usage is what recognising this page consumed.
	//
	// OCR providers bill per page rather than per token, and without a place
	// to report that the cost of a reading cannot reach [Metadata.Usage] or a
	// metric at all: the seam would be the one stage of the pipeline whose
	// spend is invisible. A provider that does not meter a request leaves this
	// zero rather than guessing a page count.
	Usage Usage
}

Recognition is what an OCR provider read from one page.

Every implementation normalises to this shape: coordinates in page points with the origin top left, confidence on 0..1, and words in reading order rather than in whatever order the provider's API returned them.

type Rect

type Rect struct {
	MinX float64
	MinY float64
	MaxX float64
	MaxY float64
}

Rect is a region of a page, in points, with the origin at the top left.

That origin is neither PDF's (bottom left) nor an image format's (pixels, top left). One convention had to be chosen and adapters normalise to it, so the confidence engine and any review interface are written against one shape.

type Ref

type Ref struct {
	// Page is 1-based; Table is the index within the page's Layout; Row and
	// Column are 0-based, as on [Cell].
	Page   int
	Table  int
	Row    int
	Column int
}

Ref locates one cell by position alone.

It is the loggable form of a claim about a table: "page 4, table 1, row 3, column 2" says which value is meant without repeating the value, so it can go in a provenance entry, an event, or a review interface under the rule that document content never reaches any of them (rule §2.5, §7.5).

func (Ref) String

func (r Ref) String() string

String renders the reference for a diagnostic.

It exists so that a Ref reaching a log line reads as a position rather than as a struct of four integers, and it is safe to call anywhere document content may not go: a Ref has nowhere to put any (rule §2.5, §7.5).

type Region

type Region struct {
	// Text is what the provider read, unnormalised. It is document content;
	// see [Cell.Text].
	Text string

	// Box is the region on the page. A zero Rect means no geometry.
	Box Rect

	// Confidence is the provider's own, on 0..1.
	Confidence float64
}

Region is a run of recognised text and where it was.

type Renderer

type Renderer interface {
	Render(ctx context.Context, doc Document, page, dpi int) (image.Image, error)
}

Renderer rasterises a document page to an image, so it can be given to an OCR provider or a vision model.

There is no default implementation, and that is the hardest constraint in the project: rasterising PDF means implementing a large part of the PDF imaging model, and nobody has done it well in pure Go. The recommended implementation runs PDFium as WebAssembly, which needs no cgo and cross-compiles, at the cost of speed. A renderer using cgo must say so on the first line of its package documentation.

Most extractions never need one. A PDF with a text layer needs no rasterising, an image is already an image, and a DocumentOCR provider rasterises server-side. See docs/adr/0010-no-cgo-in-core.md.

Implementations must be safe for concurrent use by multiple goroutines.

type Result

type Result[T any] struct {
	// Data holds every field that was read, whether or not Valid.
	//
	// A field that could not be read is left at its zero value here and marked
	// absent in Fields. Nothing is ever guessed to satisfy the struct.
	Data T

	// Valid reports whether every validation rule passed.
	Valid bool

	// Confidence is the aggregate over fields, weighted by whether a field is
	// required, so a missing optional field does not drag down a clean
	// document.
	//
	// Until the weights are calibrated this is a ranking signal, not a
	// probability. It orders a review queue well; it does not mean "correct
	// this often".
	Confidence float64

	// Fields holds one entry per schema field, including fields that were not
	// found.
	//
	// Keys are the Go field path in snake case, not the description from the
	// ovrin tag: a field declared UnitPrice float64 `ovrin:"price per unit"`
	// is Fields["unit_price"]. The tag describes the field to the model and is
	// free to be reworded; the Go name is the caller's own identifier and does
	// not change under them when a description is improved.
	//
	// A nested struct is keyed with a dot ("vendor.name") and a slice field
	// additionally contributes one entry per extracted element ("items[0]"),
	// so the number of keys depends on what was read. See docs/schema.md.
	Fields map[string]FieldResult

	// NeedsReview reports whether a person should look before this is used.
	NeedsReview bool

	// Reasons says why, one entry per triggering condition.
	Reasons []ReviewReason

	// Metadata records how the result was produced.
	Metadata Metadata
}

Result is what an extraction produced.

The error returned alongside it and the Valid field answer different questions. A non-nil error means nothing usable came back and Result is nil. Valid reports whether every validation rule passed, which is independent: data can come back that is real, useful and not yet good enough to act on.

See docs/adr/0004-partial-results.md.

func Extract

func Extract[T any](ctx context.Context, c *Client, src Source, opts ...Option) (*Result[T], error)

Extract reads src and returns it as a T.

T is a struct whose fields carry `ovrin:"…"` tags describing what to extract. Reflection over it happens once per Client per type, and a malformed schema is ErrSchema raised before any provider is contacted, so a typo costs nothing.

opts override the Client's configuration for this call only; the Client is not modified, so concurrent extractions with different options cannot interfere. Options that configure a provider — WithModel, WithOCR, WithRenderer, WithHook — are rejected here rather than silently ignored.

A non-nil error means nothing usable came back and the Result is nil. It does not mean the data is good: that is Result.Valid, and the two are independent.

res, err := ovrin.Extract[Invoice](ctx, c, ovrin.File("invoice.pdf"))
if err != nil {
	return err
}
if !res.Valid || res.NeedsReview {
	return review.Queue(res)
}
return ledger.Post(res.Data)
Example

Extracting an invoice from an image.

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"image"
	"image/color"
	"image/png"

	"github.com/BAGOMBEKA-JOB-DEV/ovrin"
)

// Invoice is the schema used throughout these examples.
type Invoice struct {
	Number   string  `ovrin:"invoice number,required"`
	Vendor   string  `ovrin:"vendor company name"`
	Currency string  `ovrin:"currency code,required,enum=UGX|USD|EUR|GBP"`
	Total    float64 `ovrin:"total amount including tax,required,min=0"`
}

// fakeModel returns a fixed reply, so the examples are deterministic and run
// offline. A real programme passes ovrinskyl.OpenAI(key, model) — the interface
// is the same either way, which is the point of the seam.
type fakeModel struct{ reply map[string]any }

func (m fakeModel) Generate(context.Context, ovrin.ModelRequest) (*ovrin.ModelResponse, error) {
	b, _ := json.Marshal(m.reply)
	return &ovrin.ModelResponse{JSON: b, Usage: ovrin.Usage{InputTokens: 812, OutputTokens: 46}}, nil
}

// fakeOCR returns fixed words with positions, standing in for a real provider.
// It matters that it returns *text*: grounding checks an extracted value
// against the document it came from, and a vision reading has no source text
// to check against.
type fakeOCR struct{ words []string }

func (fakeOCR) Name() string { return "example" }

func (o fakeOCR) Recognise(context.Context, ovrin.Page) (*ovrin.Recognition, error) {
	rec := &ovrin.Recognition{Confidence: 0.97}
	x := 10.0
	for i, w := range o.words {
		rec.Words = append(rec.Words, ovrin.Word{
			Text:       w,
			Box:        ovrin.Rect{MinX: x, MinY: 100, MaxX: x + 40, MaxY: 112},
			Confidence: 0.97,
			Line:       0,
		})
		x += 45
		_ = i
	}
	return rec, nil
}

// pngBytes is a tiny valid PNG, so the examples have a real image source.
func testPNG() []byte {
	m := image.NewRGBA(image.Rect(0, 0, 4, 4))
	m.Set(0, 0, color.RGBA{A: 255})
	var buf bytes.Buffer
	_ = png.Encode(&buf, m)
	return buf.Bytes()
}

func main() {
	c := ovrin.New(
		ovrin.WithModel(fakeModel{reply: map[string]any{
			"number": "INV-2026-0417", "vendor": "Kampala Supplies Ltd",
			"currency": "UGX", "total": 2500000.0,
		}}),
		ovrin.WithOCR(fakeOCR{words: []string{"INV-2026-0417", "Kampala", "Supplies", "Ltd", "UGX", "2,500,000"}}),
	)

	res, err := ovrin.Extract[Invoice](context.Background(), c, ovrin.Bytes(testPNG()))
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Println(res.Data.Number)
	fmt.Println(res.Data.Currency, res.Data.Total)
	fmt.Println("valid:", res.Valid)
}
Output:
INV-2026-0417
UGX 2.5e+06
valid: true

func (*Result[T]) Explain

func (r *Result[T]) Explain(field string) (*Explanation, bool)

Explain returns the decomposition of one field, and whether that field exists in the schema.

The key is the field path as it appears in Result.Fields: "total", "vendor.name", "items[0].unit_price".

Example (Fabrication)

A value the model invented is caught, because it appears nowhere in the document it was supposedly read from. This is the failure ovrin exists to notice: the value is well formed, it satisfies every rule, and it is wrong.

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"image"
	"image/color"
	"image/png"

	"github.com/BAGOMBEKA-JOB-DEV/ovrin"
)

// Invoice is the schema used throughout these examples.
type Invoice struct {
	Number   string  `ovrin:"invoice number,required"`
	Vendor   string  `ovrin:"vendor company name"`
	Currency string  `ovrin:"currency code,required,enum=UGX|USD|EUR|GBP"`
	Total    float64 `ovrin:"total amount including tax,required,min=0"`
}

// fakeModel returns a fixed reply, so the examples are deterministic and run
// offline. A real programme passes ovrinskyl.OpenAI(key, model) — the interface
// is the same either way, which is the point of the seam.
type fakeModel struct{ reply map[string]any }

func (m fakeModel) Generate(context.Context, ovrin.ModelRequest) (*ovrin.ModelResponse, error) {
	b, _ := json.Marshal(m.reply)
	return &ovrin.ModelResponse{JSON: b, Usage: ovrin.Usage{InputTokens: 812, OutputTokens: 46}}, nil
}

// fakeOCR returns fixed words with positions, standing in for a real provider.
// It matters that it returns *text*: grounding checks an extracted value
// against the document it came from, and a vision reading has no source text
// to check against.
type fakeOCR struct{ words []string }

func (fakeOCR) Name() string { return "example" }

func (o fakeOCR) Recognise(context.Context, ovrin.Page) (*ovrin.Recognition, error) {
	rec := &ovrin.Recognition{Confidence: 0.97}
	x := 10.0
	for i, w := range o.words {
		rec.Words = append(rec.Words, ovrin.Word{
			Text:       w,
			Box:        ovrin.Rect{MinX: x, MinY: 100, MaxX: x + 40, MaxY: 112},
			Confidence: 0.97,
			Line:       0,
		})
		x += 45
		_ = i
	}
	return rec, nil
}

// pngBytes is a tiny valid PNG, so the examples have a real image source.
func testPNG() []byte {
	m := image.NewRGBA(image.Rect(0, 0, 4, 4))
	m.Set(0, 0, color.RGBA{A: 255})
	var buf bytes.Buffer
	_ = png.Encode(&buf, m)
	return buf.Bytes()
}

func main() {
	c := ovrin.New(
		// The document says 2,500,000. The model reports 9,999,999.
		ovrin.WithModel(fakeModel{reply: map[string]any{
			"number": "INV-2026-0417", "vendor": "Kampala Supplies Ltd",
			"currency": "UGX", "total": 9999999.0,
		}}),
		ovrin.WithOCR(fakeOCR{words: []string{"INV-2026-0417", "Kampala", "Supplies", "Ltd", "UGX", "2,500,000"}}),
	)

	res, _ := ovrin.Extract[Invoice](context.Background(), c, ovrin.Bytes(testPNG()))

	total := res.Fields["total"]
	fmt.Printf("value      %.0f\n", total.Value)
	fmt.Printf("confidence %.2f\n", total.Confidence)
	fmt.Println("review:", res.NeedsReview)
	for _, r := range res.Reasons {
		if r.Field == "total" {
			fmt.Println("reason:", r.Why)
		}
	}
	// The cap is a ceiling, not a floor: grounding contributes 0.0 at weight
	// 0.30 and the rules contribute 1.0 at weight 0.15, so the mean is already
	// 0.33 and CapUngrounded never has to bind.
	//
	

type ReviewReason

type ReviewReason struct {
	// Field is the field key, as in [Result.Fields].
	Field string

	// Why is a short cause, such as "value not found in source; may be
	// inferred" or "readings disagree".
	Why string
}

ReviewReason names a field that needs a person, and why.

It carries a field name and a cause, never the value: a review queue is a system that stores things, and document values should reach it because the caller decided to put them there, not because ovrin leaked them in a reason string.

type RuleResult

type RuleResult struct {
	// Rule is the rule as written in the tag: "required", "min=0",
	// "format=date".
	Rule string

	// Passed reports the outcome.
	Passed bool

	// Message says why not, and is empty when Passed.
	Message string
}

RuleResult is one validation rule and its outcome.

Message is a string rather than an error so that an Explanation marshals to JSON — which is the point of Result.Explain returning data rather than formatted text.

type Scorer

type Scorer interface {
	// Score returns the confidence and the signals that produced it. The
	// signals must account for the score: a caller must be able to check the
	// arithmetic.
	Score(f FieldEvidence) (confidence float64, signals []Signal)
}

Scorer combines evidence into a confidence score.

Pluggable because a user with labelled documents can fit a better scorer to their own corpus than any default will manage. The consequence is that confidence is comparable within a deployment, not across organisations.

type Signal

type Signal struct {
	// Name is one of the Signal* constants.
	Name string

	// Value is on 0..1.
	Value float64

	// Weight is this signal's share of the score, after redistribution across
	// the signals that actually applied.
	Weight float64

	// Note says why, in one line: "found verbatim, page 1", "12 backing words,
	// mean 0.97".
	Note string
}

Signal is one named input to a confidence score.

Every score decomposes into these. No number is produced that a caller cannot take apart, because a confidence figure nobody can interrogate is a figure nobody should act on.

type Source

type Source interface {
	// contains filtered or unexported methods
}

Source is an unread document.

The interface is closed — it has an unexported method — so the only Sources are the ones Reader, Bytes and File return. An open interface would let a caller supply something no pipeline stage knows how to read, turning a compile-time error into a runtime one.

func Bytes

func Bytes(b []byte) Source

Bytes returns a Source reading from b.

The slice is not copied and must not be modified until Extract returns.

func File

func File(path string) Source

File returns a Source reading the file at path.

Opening is deferred to Extract, so a missing file surfaces as an extraction error alongside every other failure rather than needing a separate check.

func Reader

func Reader(r io.Reader) Source

Reader returns a Source reading from r.

This is the primary constructor: a document usually arrives as a stream — an upload, a network body — and buffering it before ovrin can check it against the source-size limit would defeat the limit.

The reader is consumed once. If it is an io.Closer, ovrin does not close it; that is the caller's, since the caller opened it.

type Span

type Span struct {
	Start int
	End   int
}

Span is a byte range into the normalised text.

Bytes rather than runes: Go strings are bytes, converting to []rune costs a copy, and every caller would convert back.

type Table

type Table struct {
	// Page is 1-based, matching [Line.Page].
	Page int

	// Box is the table's region on the page. A zero Rect means the provider
	// gave no geometry for the table as a whole.
	Box Rect

	// Rows and Columns are the table's declared size, as the provider counted
	// it rather than derived from Cells: a table whose last row is empty still
	// has that row, and deriving the size would silently lose it.
	Rows    int
	Columns int

	// Cells are in reading order, by row then by column.
	//
	// A table with merged cells has fewer entries than Rows*Columns, and a
	// sparse one fewer still. A position no cell covers is a position the
	// provider read nothing at, which is a fact about the document and not a
	// gap to be filled with an empty string.
	Cells []Cell

	// Confidence is the provider's own for the table as a whole, on 0..1.
	Confidence float64
}

Table is one table a provider found.

func (Table) At

func (t Table) At(row, column int) (Cell, bool)

At returns the cell covering a grid position within the table, spans included, and whether there was one.

The second result is false when nothing covers the position, which is the honest answer for a sparse table: an empty Cell would be indistinguishable from a cell the provider read as empty, and the two are different facts about the document.

It is a linear scan of Cells rather than a materialised grid, because a table's declared size is the provider's number and building a grid from it would allocate for a table declaring two billion rows. Layout.Check is where that number is bounded before anything is allocated from it.

type Tolerance

type Tolerance struct {
	// Absolute is the largest acceptable difference in the values' own units.
	Absolute float64

	// Relative is the largest acceptable difference as a fraction of the
	// larger value: 0.005 is half a percent.
	Relative float64
}

Tolerance bounds how far two amounts may differ and still be consistent.

Both an absolute and a relative bound, because neither alone is right: an absolute cent covers rounding on one line and fails across a hundred, and a relative fraction is meaningless near zero.

type Usage

type Usage struct {
	InputTokens  int
	OutputTokens int
	PageUnits    int
}

Usage counts what an extraction consumed.

Tokens are what models bill; page units are what OCR providers bill. Both are here because a pipeline that uses both should be costable from one value.

type Word

type Word struct {
	Text string

	// Box is in page points, origin top left.
	Box Rect

	// Confidence is on 0..1. A provider that reports no per-word confidence
	// sets the page confidence here and records that it did, rather than
	// fabricating 1.0.
	Confidence float64

	// Line indexes into [Recognition.Lines].
	Line int
}

Word is one recognised word.

Directories

Path Synopsis
Package eval measures extraction quality against a committed corpus.
Package eval measures extraction quality against a committed corpus.
corpusgen command
Command corpusgen writes the seed evaluation corpus.
Command corpusgen writes the seed evaluation corpus.
schema
Package schema holds the Go structs each corpus category is extracted against.
Package schema holds the Go structs each corpus category is extracted against.
examples
receipt module
internal
adaptertest
Package adaptertest is the contract suite every ovrin adapter must pass.
Package adaptertest is the contract suite every ovrin adapter must pass.
compare
Package compare answers one question: are these two values, read independently, the same value?
Package compare answers one question: are these two values, read independently, the same value?
detect
Package detect identifies a document's format by content and enforces every resource limit before a byte of it is allocated.
Package detect identifies a document's format by content and enforces every resource limit before a byte of it is allocated.
ground
Package ground searches the normalised text for an extracted value and produces the grounding confidence signal.
Package ground searches the normalised text for an extracted value and produces the grounding confidence signal.
img
Package img decodes an image source into pages the pipeline can read.
Package img decodes an image source into pages the pipeline can read.
jsonschema
Package jsonschema turns an internal schema.Schema into the JSON Schema bytes that cross the Model seam.
Package jsonschema turns an internal schema.Schema into the JSON Schema bytes that cross the Model seam.
normalise
Package normalise turns raw positioned page content into one text stream while keeping a mapping from every output byte back to where it came from.
Package normalise turns raw positioned page content into one text stream while keeping a mapping from every output byte back to where it came from.
office
Package office reads the text of a DOCX, XLSX or CSV, with line structure but deliberately without invented geometry.
Package office reads the text of a DOCX, XLSX or CSV, with line structure but deliberately without invented geometry.
pdf
Package pdf reads the text layer of a PDF, with a box for every word.
Package pdf reads the text layer of a PDF, with a box for every word.
prompt
Package prompt builds the model request, and is the boundary between text recovered from an attacker-controlled file and a system that follows instructions.
Package prompt builds the model request, and is the boundary between text recovered from an attacker-controlled file and a system that follows instructions.
retry
Package retry constructs the one follow-up request ovrin makes when a model returns a reply that does not satisfy the schema.
Package retry constructs the one follow-up request ovrin makes when a model returns a reply that does not satisfy the schema.
sandbox
Package sandbox serves a provider's wire protocol over a real socket, adversarially, with no credential and no cost.
Package sandbox serves a provider's wire protocol over a real socket, adversarially, with no credential and no cost.
schema
Package schema turns a Go struct into the description of what to extract.
Package schema turns a Go struct into the description of what to extract.
validate
Package validate converts extracted values into their Go types and checks them against the rules a schema declares.
Package validate converts extracted values into their Go types and checks them against the rules a schema declares.
model
skyl module
ocr
azure module
google module
tesseract module
textract module
otel module
render
pdfium module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL