cadrumo.application.ledger.grounding_anchor module¶
The anchor check: a value grounds only if the document can be pointed at.
This is the structural half of the anti-fabrication contract. A prompt asking a model not to invent figures reduces noise and enforces nothing; this module is what makes an anchor mean something, and it runs as ordinary deterministic code over the transcription no matter which reader proposed the value.
The check has TWO parts, and both matter:
The anchor must occur in the transcription AS A WHOLE PRINTED TOKEN. Not the
value – the anchor, the verbatim printed form the reader claims to have read.
The transcription deliberately preserves printed forms (2.420,00 stays
2.420,00), so this search runs against the evidence rather than against a
normalised rewrite of it.
The token boundary is load-bearing rather than tidy. A plain substring search –
which this module originally did – lets a short figure anchor inside any longer
figure ending the same way: 0,00 occurs inside 100,00, 1,00 inside
21,00, bare 00 inside almost any amount. An injected zero total needed no
cleverness at all to ground, because most real invoices carry some amount ending
in ,00. That made the check certify “these digits appear somewhere”, which
for short numeric forms is close to vacuous – structurally weaker than it read.
The typed value must equal the deterministic parse of that anchor. The value
is NOT required to be byte-identical to the anchor – anchor 21% with value
Decimal("21") is the intended case, not a concession. Requiring identity
would make the whole apparatus useless for every field that needs parsing, which
is every monetary field.
Exactly one trailing UNIT marker is removed before that parse
(strip_printed_unit()). Without it the percentage form inverted: 21%
found in the document, coerce_finite_european_decimal returning None on
the percent sign, and the checker reporting CONTRADICTION on a rate the reader
read correctly – punishing the reader that copied more literally, which is what
the field-form contract asks for. A wrong verdict is worse than an absent one,
because an absent verdict prompts review and a wrong one forecloses it.
The parse is coerce_finite_european_decimal(), the repository’s
one extraction-side decimal contract, reused rather than re-spelled. It drops an
ambiguous thousands reading instead of guessing, which is exactly the behaviour
wanted here: an anchor whose reading cannot be settled does not ground.
A weaker case, stated rather than hidden. When the anchor and the rendered
value happen to be identical strings, the second part of the check proves nothing
about parsing – the value is matching itself. The check still runs and still
requires the anchor to occur in the document, so the fabrication bound holds; but
the parse half is vacuous there, and AnchorEvaluation.parse_was_vacuous
says so rather than letting the outcome read stronger than it is.
What this module does NOT do, stated so it is not trusted for more. It decides that a printed form is PRESENT. It does not decide that the form plays the ROLE claimed for it, and it does not decide the value is correct. An injected sentence that prints its own plausible figure passes this check honestly – the figure really is on the page.
The guarantee is therefore the conjunction of two legs, never this one alone:
the anchor check establishes presence, and
closure_findings() establishes that the monetary set
closes. An injected total that is anchored still reds the arithmetic identity,
because the other figures on the document do not reach it. A suite that gated
only the anchor property would imply a guarantee the code does not provide, and
that is exactly how a check ends up trusted for more than it does.
Anchoring bounds fabrication to transcription error. On a vision path there is no independent transcription, so the provenance model must keep any self-reported anchor explicitly unverified.
See also
FieldProvenanceThe envelope this module produces, carrying the anchor it verified.
transcribe_text_layer()The acquisition stage whose verbatim output this checks against.
FieldGroundingOutcomeThe closed outcome axis; this module never widens it.
- strip_printed_unit(anchor, currency_unit=None)[source]¶
Return anchor with exactly one printed unit marker removed.
A percent sign is a UNIT, not a digit. A document prints
IVA (21%)and a reader obeying “copy exactly as printed” cites21%as the anchor for the value21– which is the correct, more literal reading. Passing that string straight to the decimal authority returnsNone, so the anchor was found, the parse failed, and the check reported CONTRADICTION on a field the reader got right. That verdict is worse than no provenance at all: an absent verdict prompts review, a wrong one forecloses it.Exactly ONE trailing marker is removed and the remainder must still satisfy the decimal authority on its own, so
21%%or2%1still fail. The stripping applies only to the anchor’s PARSE; the envelope keeps the verbatim printed form, so anchor and value stay explicitly distinct and a transcription error cannot be laundered into a computed figure.The same holds for the currency a document prints beside an AMOUNT –
1200.00 EURcited as the anchor for the value1200.00– and for the same reason, so the currency rule iswithout_currency_unit(), shared with the reader-side grounding rather than respelled here. A unit is removed only when this rule can name it: a symbol, or the code the same document reported, which is whycurrency_unitis threaded down from the draft.
- normalise_for_anchor_search(text)[source]¶
Return text with Unicode form and whitespace regularised, nothing else.
The narrowest normalisation that survives a real PDF text layer. Two documents printing the same figure can differ in a non-breaking space, in a soft hyphen, or in whether a composed character arrived pre-composed – none of which is a difference in what was printed. Digits, separators and punctuation are left exactly as they are, because those ARE the evidence.
- printed_excerpt_occurs(excerpt, *, transcription)[source]¶
Return whether
excerptoccurs in the document, as a printed token.The presence half of
evaluate_anchor(), exposed on its own for a claim that has no typed value behind it to re-parse. Role evidence is such a claim: it is a printed heading or label the reader copied to justify assigning an identifier to a party, so the only question that can be asked of it is whether the document actually says it.Reusing this module’s own search rather than open-coding a containment test is what keeps one authority over “does the document print this”: the same Unicode and whitespace regularisation that survives a real PDF text layer, and the same numeric-edge boundary rule, so a role evidence excerpt cannot be admitted under weaker matching than an anchor.
- Parameters:
excerpt (
str) – The printed context the reader claims to have copied.transcription (
DocumentTranscription) – The acquisition-stage text, printed forms intact.
- Returns:
it evidences nothing, and the caller must treat that as absent evidence rather than as a permissive match.
- Return type:
Truewhen the excerpt occurs. A blank excerpt isFalse
- printed_excerpt_occurs_in_text(excerpt, *, text)[source]¶
Return whether
excerptoccurs intext, as a printed token.The same search as
printed_excerpt_occurs(), asked of a REGION of a document rather than the whole of one. Party attribution by co-location has to ask “does this value occur inside this party’s block”, which is the same question about a smaller haystack – and asking it through this module keeps one authority over what counts as printed, so a value cannot be admitted to a block under weaker matching than the anchor check applies to the document.Takes plain text rather than a transcription deliberately. A region is a slice of a transcription’s text and not a transcription itself: it has no page count, no content address and no transcriber, and manufacturing those to satisfy a signature would fabricate provenance for a substring.
- class AnchorEvaluation(**data)[source]¶
Bases:
BaseModelThe verdict on one candidate, before it becomes a provenance envelope.
Separated from
FieldProvenanceso the decision is inspectable on its own: a caller can ask WHY a value failed to ground without reading it back out of an outcome enum, and a test can assert on the reason rather than on the consequence.- Variables:
outcome – The grounding outcome this evaluation resolves to.
anchor_found – Whether the anchor occurs in the transcription.
parsed_anchor – The deterministic parse of the anchor, when the value under check is a decimal and the anchor parses at all.
parse_was_vacuous –
Truewhen the anchor and the rendered value are the same string, so the parse half of the check compared a value against itself and established nothing. The anchor half still ran.detail – Operator-facing explanation.
- Parameters:
data (Any)
- outcome: FieldGroundingOutcome¶
- anchor_found: bool¶
- parsed_anchor: Decimal | None¶
- parse_was_vacuous: bool¶
- detail: str¶
- refused_anchor_of(anchor, evaluation)[source]¶
Return the printed form the check looked for and did not find, or
None.The one place the refusal is derived, so every producer that clears the anchor on a miss records the same discrimination rather than each deciding for itself. Clearing alone loses the fact that a form was OFFERED, and an operator told “the reader offered nothing to point at” when the reader offered something the document does not carry is being sent to investigate the wrong thing.
A blank or absent anchor returns
Noneon purpose: there was nothing to refuse, and recording an empty refusal would manufacture exactly the false distinction this closes, in the other direction.- Parameters:
evaluation (
AnchorEvaluation) – The verdict the check reached on it.
- Return type:
- Returns:
The offered form, when a check ran and did not locate it.
- evaluate_anchor(*, value, anchor, transcription, currency_unit=None)[source]¶
Evaluate one candidate’s anchor against the transcription it came from.
- Parameters:
value (
Decimal|str) – The typed value the reader proposes.anchor (
str) – The verbatim printed form the reader claims to have read it from.transcription (
DocumentTranscription) – The acquisition-stage text, printed forms intact.currency_unit (
str|None) – The currency the document reported, so an amount cited beside its printed unit still parses.Nonefor a document that reported none.
- Return type:
- Returns:
The evaluation.
ANCHOREDrequires BOTH that the anchor occurs in the transcription AND that the value equals the deterministic parse of the anchor. A missing anchor isUNANCHORED; a present anchor that parses to a different value isCONTRADICTED, which is a stronger and more actionable statement than merely failing to ground.
- ground_structured_value(*, field, value, element_path, source_text, anchor=None, derive=None)[source]¶
Return the envelope for one value read from a document’s own record.
A path that constructs a
FieldProvenanceitself and hand-setsANCHOREDis asserting the check instead of running it. It is a separate dedicated entry point because the transcription this module normally checks against does not exist for a machine-readable document – there are no pages, no reading order and no transcriber – and aDocumentTranscriptionsynthesised to satisfy the signature would have to state a page count and a reader that never existed. This route runs the same two-part check as the text evaluator underneath.The anchor is the record’s own verbatim text, never the element path. The anchor field means the form the value was read from, and a downstream consumer reads it as evidence about what the document states. A schema path is a location: true, useful, and not evidence. It rides in the note instead, where an operator can still use it to find the value and nothing can mistake it for a printed form.
The check is real, not ceremonial. The parser produced the value; this looks for its verbatim form in the record’s own text independently, and re-derives a numeric value from the anchor. A projection that mangled a figure on the way through, or a reader that pointed at an element the document does not carry, fails it.
That second claim is enforced HERE and no longer only upstream. It was once true only because every structured country reader returns its own element’s text or
None– a guard in a different module entirely – while this check would happily locate a two-letter code inside a longer token, so a record stating no country at all anchoredESagainstESB12345674. The search is boundary-aware for word-shaped anchors as well as numeric ones, so the property this paragraph asserts is now a property of this function.What it still cannot do is prove the reader chose the RIGHT element – the same limit the transcription lane has, where an anchor found somewhere in the page does not prove it was found in the right place. A short code occurring as a genuine standalone token somewhere unrelated matches, and only the element path in the note distinguishes that.
- Parameters:
field (
str) – Name of theInvoiceDraftfield.value (
Decimal|str) – The typed value the parser produced.element_path (
str) – Where in the record it came from, for the operator’s note.source_text (
str) – The record’s own text, decoded from the source bytes.The record’s verbatim form of the value, where that DIFFERS from the value itself. Defaults to the value’s own rendering, which is correct for every field copied straight out of the record and was the only case until a value arrived normalised: a party’s country is stated
ESPby Facturae and carriedES, so grounding the carried form would look for a string the document never states.This is the same relation
FieldProvenance.anchoralready documents for the printed lanes –"1.234,56 €"anchoring the value1234.56. The shape is the same and the guarantee is not inherited: that relation is checked by a parse that resolves CONTRADICTED on disagreement, so an explicit anchor here must supply its own equivalent through derive. Requiring it is not ceremony – without it the anchor could be real while the value was arbitrary, and the envelope would assert the document evidences a value it never mentions.derive (
Callable[[str],str|None] |None) – How to re-derive value from anchor, REQUIRED whenever an explicit anchor is given for a textual value. Refused rather than defaulted, because a silent default would be this module choosing a normalisation on the caller’s behalf, which is the thing the anchor check exists to avoid.
- Return type:
- Returns:
The envelope, stamped
EXACT_STRUCTURED.
- ground_ambiguous_candidates(*, field, origin, candidates, note='')[source]¶
Return an
AMBIGUOUSenvelope carrying every competing candidate.The sanctioned way to record “several readings competed and none was decidable”. Deliberately takes the whole candidate set rather than a winner plus alternates: there is no winner, and a shape that had one would invite a caller to promote the first.
- Parameters:
field (
str) – Name of the draft field.origin (
FieldOrigin) – How the competing values were obtained.candidates (
tuple[FieldAmbiguityCandidate,...]) – Every competing reading. Two or more, enforced by the envelope itself.note (
str) – Operator-facing explanation of what made them competitors.
- Return type:
- Returns:
The
AMBIGUOUSenvelope.
- structured_provenance(*, parsed, evidence, derived, operation)[source]¶
Return one provenance envelope per value the structured record stated.
operationis retained for the derived country projection so its catalogue lookup shares the structured reader’s authority generation.- Return type:
- Parameters:
parsed (StructuredInvoiceRecord)
evidence (EvidenceInput)
operation (PinnedAuthorityOperation)
- resolved_country_code(stated, *, operation)[source]¶
Return a record country as
(resolved alpha-2, stated form).operationowns the country-vocabulary generation used for the structured record’s alpha-2/alpha-3 projection.