DocumentCard โ "what this document
IS": its type, dates, parties, amounts, and a handful of
contradiction-eligible assertions. Every value carries the
SourceSpans that supposedly prove it. But a producer's word is
not proof. Persist that card unchecked and two catastrophes wait. First, the
company-ending one: a span that lies โ a quote the model paraphrased
or hallucinated, or one that drifted when the page was re-OCR'd โ gets stored
as a "citation," surfaced to counsel, and cited into a
real filing in a real court, where opposing counsel opens the
document, finds the quote isn't there, and the evidence is struck. Second, the
quieter one: a page of garbage OCR (a smudged scan at 0.4 confidence)
becomes a fake fact nobody can see is fake. CE-7 is the turnstile between
Blake's card and the record: every span is re-verified byte-for-byte against
the real page bytes, low-confidence pages are barred, and only a card that
passes is stored and announced. A card that fails is a typed rejection
โ never a confidently-stored wrong fact.
The whole story is ingest_document_card() in
src/shared/context/card_ingest.py. It takes a validated
DocumentCard and, in the caller's transaction, runs three
fail-closed gates and โ only if all pass โ performs two side effects:
| Step | What it does | RFD |
|---|---|---|
| OCR-confidence floor | Load the document's pages; exclude any whose OCR mean_confidence is below CONTEXT_CARD_OCR_CONFIDENCE_FLOOR. Native-extracted pages (no confidence) always stay. | ยง5.1 M-8a |
| Byte-exact span resolution | Every span the card claims must byte-resolve against an admitted page โ reusing the CE-2 resolver (the live V-CITE primitive). Any failure โ reject the whole card. | ยง4 |
| Persist (on pass) | Upsert the card row whole into context.document_cards (JSONB envelope + queryable columns), replaced per run. | ยง5.1 |
| Announce (on pass, in-txn) | Emit document_card.updated through the outbox, in the same transaction as the row write. | ยง9 |
Wiring: the registration consumer, on a clean registered
terminal, ingests Blake's card (in its own isolated transaction) and fires a
direct SQS enrich kick for the per-document work downstream
(chunk + embed, CE-3).
Blake is a paralegal who hands the clerk a summary sheet: "this contract was executed 5 May 2019 โ see page 3, line 4." The clerk does not take his word. She pulls page 3 and reads line 4 with a magnifying glass, checking the words match letter for letter (byte-exact, no whitespace-forgiveness, no case-folding). If the page is a smudged photocopy she can't read clearly (below the confidence floor), she sets it aside as unreadable โ a claim resting on an unreadable page is no claim. Only when every quote checks out does she file the sheet in the firm's cage and stamp the intake log. If a single quote is wrong, the whole sheet bounces back โ and, crucially, the document stays registered anyway: a bad summary sheet never un-files the document itself.
failures = unresolved(card_spans(card), admitted)
if failures:
raise CardSpanUnresolvedError(
f"{len(failures)} card span(s) did not byte-resolve ...",
details={"reasons": sorted({res.reason for _, res in failures if res.reason}), ...},
)
unresolved() (CE-2) returns each span that failed to verify,
paired with a low-cardinality reason code
(quote_not_byte_present, page_not_found, โฆ) โ never
the quote or page text, so a rejection is safe to log. A non-empty result
raises CardSpanUnresolvedError and nothing is written:
no card row, no event. Fail-closed means the default is "not evidence."
def eligible_pages(pages, floor):
return [p for p in pages
if p.mean_confidence is None or p.mean_confidence >= floor]
The floor isn't a separate check โ it's which pages exist for
resolution. A span anchored to a below-floor page finds no page to resolve
against (page_not_found) and the card is rejected. Two design
choices, each titled by its reason: mean_confidence is None stays
admitted because native-extracted text (pdfium) has perfect fidelity and no
OCR score to doubt; >= floor (not >) so a page
exactly at the threshold is trusted, not barred by an off-by-one.
insert_domain_event(session, detail_type="document_card.updated", โฆ)
writes the outbox row on the caller's open session, so the card row and its
announcement commit or roll back together โ a consumer (assertion normalizer
CE-9, bundle rebuilder CE-13) can never see a card with no event, or an event
with no card. One grammar note: RFD prose writes it
document.card.updated, but the audit+event vocabulary is
single-dot <subject>.<verb>, so the wire-valid token
is document_card.updated โ a new document_card
subject in the audit registry, keyed by document_id.
# registration consumer, only on a clean `registered` terminal:
if to_status == "registered":
await _ingest_card(deps, ctx, result) # own txn, swallows CardSpanUnresolvedError
_kick_enrichment(deps, ctx) # direct SQS send, best-effort
_ingest_card runs in its own transaction and swallows every
error into a needs_review log line (RFD ยง5.1 invariant M-12b) โ
the document is already registered and stays so. The enrich kick is a
direct send_message (not via the outbox โ the
event-backbone fold-in is its own later Chore, RFD ยง9 critique m-6); a failed
kick is logged, and the CE-18 backfill re-drives any missed document.
| Gate | Where |
|---|---|
unresolvable-span typed rejection โ a hallucinated quote raises CardSpanUnresolvedError and writes nothing | tests/integration/test_context_card_ingest.py (gate 2) + unit test_hallucinated_quote_is_unresolved |
zero-rows first-run โ a valid card into an EMPTY table writes the row (with queryable columns) and emits the event, as the pramaan_app role | integration gate 1 |
| confidence floor config-named & exercised โ a 0.50 page is rejected at the default floor and admitted once the floor drops below it | integration gate 3 + unit test_below_floor_page_makes_the_card_fail_closed |
The integration test runs against a real Postgres including the empty
first-run state (AGENTS.md "done = system boundary"): a real
INSERT as the runtime role, not a green unit test over a fixture.
Recall in one sentence: CE-7 is the turnstile that re-verifies every span of Blake's card byte-for-byte against the confidence-floored page bytes before it can become evidence โ pass and it's stored and announced in one transaction; fail and it's a typed rejection that never touches the record and never un-registers the document.