Closes out the two items deferred from the v2.2.0 release, plus a version-alignment ruling. 3 PRs, 15 commits since v2.2.0.
Behaviour change — read this if you call ingestion
- Issue 206 — a fully-failed document ingestion previously returned normally with zero facts and no exception, byte-identical on disk to a genuinely empty document. That silence is what let issue 204's text-only vision wiring survive undetected.
attach_and_extractnow raisesIngestionErrorwhen every page fails; no facts sidecar is written, so the document reads as not-ingested and is safely re-runnable. Any caller relying on the old silent-empty return will now see an exception. Partial failure is unchanged — surviving pages' facts are kept, and the facts sidecar now recordspages_total/failed_pagesso partial degradation is durable on disk instead of invisible. The pre-wiredextraction_failure_ratedashboard alert (threshold, explanation text, and banner all existed but were permanently fedNone) is now populated. Measured live in-container before and after, against both a total-failure case and an 8-fact success control.
Reproducible container builds
- Issue 213 — the container build was not reproducible: bounds narrow the resolution window but do not pin it, and transitives (notably
starlette, the layer actually under FastAPI) were unbounded. The container — the actual shipped artifact — now installs from a hash-lockedrequirements.txtgenerated by pip-compile inside the same digest-pinned base image the Dockerfile uses, with--require-hashesenforced at install. CI gains a job that builds the image (previously no workflow built it at all) and audits the lock. Verified empirically at review: double--no-cachebuilds byte-identical, lock regeneration byte-identical to the committed file, and a corrupted hash genuinely fails the build.
Version alignment
- Issue 214 — the package previously declared
version = "0.1.0"while releases were tagged v2.x. By owner ruling, the package version tracks the release tag from this release onward; pyproject, the app, and the OpenAPI document all read2.3.0, and this tag closes the loop. A red-first test forces every future release PR to bump deliberately.
Known limits, stated plainly
- The hash lock covers the container only. Dev venvs and CI's
pip install -e ".[dev]"remain range-based by design — the lock is the machine-facing contract for the shipped artifact; the pyproject ranges stay the human-facing one. - The issue-204 vision-capability pre-flight is a name heuristic; a vision-named model that is not actually pulled passes it. Ruled acceptable because the issue-206 change makes that case raise loudly at first ingestion instead of failing silently.
--allow-unsafeproduced no setuptools/pip pins because nothing in the runtime graph declares them; the build-backend closure is locked via the digest-pinned base image plus--no-build-isolationinstead. A base-digest bump mandates lock regeneration.
Full test state at tag, verified independently on main immediately before tagging (not inherited from the last PR's CI): 1393 passed, 1 skipped, 18 deselected, 12 xfailed (agent), 383 passed, 12 deselected, 15 xfailed (evals), mypy clean (64 source files), CI green on 5f213718.