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

FieldProvenance

The envelope this module produces, carrying the anchor it verified.

transcribe_text_layer()

The acquisition stage whose verbatim output this checks against.

FieldGroundingOutcome

The 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” cites 21% as the anchor for the value 21 – which is the correct, more literal reading. Passing that string straight to the decimal authority returns None, 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%% or 2%1 still 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 EUR cited as the anchor for the value 1200.00 – and for the same reason, so the currency rule is without_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 why currency_unit is threaded down from the draft.

Parameters:
  • anchor (str) – The verbatim printed form.

  • currency_unit (str | None) – The ISO-4217 code the document reported for itself, or None when it reported none.

Return type:

str

Returns:

The anchor with one unit marker removed, or unchanged.

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.

Parameters:

text (str) – Raw text from a transcription or a proposed anchor.

Return type:

str

Returns:

The regularised form, for anchor comparison only. Never stored as the anchor itself: the envelope keeps what the document actually printed.

printed_excerpt_occurs(excerpt, *, transcription)[source]

Return whether excerpt occurs 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:

True when the excerpt occurs. A blank excerpt is False

printed_excerpt_occurs_in_text(excerpt, *, text)[source]

Return whether excerpt occurs in text, 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.

Parameters:
  • excerpt (str) – The printed form to look for.

  • text (str) – The text to look in, printed forms intact.

Return type:

bool

Returns:

True when the excerpt occurs. A blank excerpt is False, on the same terms as the whole-document search: it evidences nothing.

class AnchorEvaluation(**data)[source]

Bases: BaseModel

The verdict on one candidate, before it becomes a provenance envelope.

Separated from FieldProvenance so 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 – True when 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 None on purpose: there was nothing to refuse, and recording an empty refusal would manufacture exactly the false distinction this closes, in the other direction.

Parameters:
  • anchor (str | None) – The form the reader offered, if any.

  • evaluation (AnchorEvaluation) – The verdict the check reached on it.

Return type:

str | None

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. None for a document that reported none.

Return type:

AnchorEvaluation

Returns:

The evaluation. ANCHORED requires BOTH that the anchor occurs in the transcription AND that the value equals the deterministic parse of the anchor. A missing anchor is UNANCHORED; a present anchor that parses to a different value is CONTRADICTED, 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 FieldProvenance itself and hand-sets ANCHORED is 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 a DocumentTranscription synthesised 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 anchored ES against ESB12345674. 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 the InvoiceDraft field.

  • 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.

  • anchor (str | None) –

    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 ESP by Facturae and carried ES, so grounding the carried form would look for a string the document never states.

    This is the same relation FieldProvenance.anchor already documents for the printed lanes – "1.234,56 €" anchoring the value 1234.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:

FieldProvenance

Returns:

The envelope, stamped EXACT_STRUCTURED.

ground_ambiguous_candidates(*, field, origin, candidates, note='')[source]

Return an AMBIGUOUS envelope 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:

FieldProvenance

Returns:

The AMBIGUOUS envelope.

structured_provenance(*, parsed, evidence, derived, operation)[source]

Return one provenance envelope per value the structured record stated.

operation is retained for the derived country projection so its catalogue lookup shares the structured reader’s authority generation.

Return type:

tuple[FieldProvenance, ...]

Parameters:
resolved_country_code(stated, *, operation)[source]

Return a record country as (resolved alpha-2, stated form).

operation owns the country-vocabulary generation used for the structured record’s alpha-2/alpha-3 projection.

Return type:

tuple[str, str] | None

Parameters:
country_code_value(pair)[source]

Return the resolved half of a country-code pair, or nothing.

Return type:

str | None

Parameters:

pair (tuple[str, str] | None)

stated_country_code(stated)[source]

Return the record’s non-blank country token verbatim.

Return type:

str | None

Parameters:

stated (str | None)