cadrumo.application.ledger.party_colocation module

Attribute a party’s address values by the block of the document they sit in.

The structural answer to party attribution, for every document whose layout can carry it. The model’s job does not change and the prompt does not grow: it copies values and quotes role evidence for the two identity fields, exactly as before. CODE attributes, which is where every other attribution in this design already lives.

Measured, no real document’s layout carries it, and that is stated here because this is where a reader meets the resolver. Scored against authored anchors rather than against the reader whose output would otherwise be being graded: of the documents carrying a hand-written reference transcription and a hand-written identity for both parties, NONE could be partitioned by any authored anchor pair, and every one failed for the single cause described under _regions() – a two-column header arriving as one line. The zero is a ceiling rather than a rate, since a perfect reader bounds every real one, so no prompt, model or second pass raises it. Segmenting such a header needs spatial information the extractor discards before this module is reached.

Two consequences, and neither is that this module is unwired. It attributes correctly on a stacked header and its gates prove it, so the finding is about which documents exist, not about the code. And on a prose document the unverified-attribution stamp and its review-gate advisory are the operating control rather than a placeholder – see stamp_unverified_party_attribution(), whose own docstring carries the same correction. The failure direction stays safe: unresolved keeps the warning.

The mechanism is containment, never proximity. A party’s role evidence is a printed heading the reader copied – FACTURAR A, Verkaufer, the label a NIF sits under – and it is already checked to occur in the document. Those headings partition the transcription into regions, and a value belongs to the party whose region contains it. A nearest-neighbour or reading-order-distance rule would be inference dressed as determinism: it would answer confidently on a two-column layout where the nearest heading belongs to the other party.

Segmentation is by line, not by blank line. Measured against the real evidence corpus, not assumed: every text-layer transcription in it carries zero blank lines, because a PDF text extractor emits reading-order lines and the visual gap between two address blocks leaves no character behind. A blank-line-delimited implementation would have been correct-looking and dead – never firing on any real document, which is the same failure as evidence no resolver consumes, in mirror image.

Three outcomes, and the third is the honest one. A value found in exactly its own party’s region is ATTRIBUTED. A value found only in the OTHER party’s region is CONTRADICTED – the transposition this whole apparatus exists for, and the one case where refusing is right. Everything else is UNRESOLVED: no role evidence to segment on, a value in both regions, a value in neither. Unresolved keeps the unverified-attribution stamp and the operator advisory rather than inventing a verdict, because a document that cannot answer the question has not answered it.

The zugferd specimen in the corpus is why the both-regions case is not theoretical: it prints the supplier’s postal code a second time in a remarks block below the customer heading, so that value genuinely occurs on both sides and no containment rule can honestly resolve it.

See also

stamp_unverified_party_attribution()

The unverified-attribution stamp this resolution clears or leaves standing, and which on a real prose document it leaves standing.

printed_excerpt_occurs_in_text()

The one authority for whether a region prints a given form.

class PartyAttributionOutcome(*values)[source]

Bases: StrEnum

What co-location settled about one address value’s party.

Variables:
  • ATTRIBUTED – The value occurs inside its own party’s region and not the other’s. The reader’s assignment is confirmed by the document’s own layout, so the unverified-attribution stamp is cleared.

  • CONTRADICTED – The value occurs only inside the OTHER party’s region. The reader filed it under the wrong party – the transposition the territory axis is silently wrong on – and it is refused rather than corrected, because swapping it here would substitute one unverified assignment for another.

  • UNRESOLVED – The document did not answer. No role evidence to segment on, the value printed on both sides, or printed on neither. The stamp stays and the operator keeps the advisory.

ATTRIBUTED
CONTRADICTED
UNRESOLVED
class PartyColocationResolution(**data)[source]

Bases: BaseModel

What co-location settled for every address value on one draft.

Variables:

outcomes – Per draft field name. Only the per-party address fields the establishment ladder consumes appear; a field the draft did not carry a value for is absent rather than UNRESOLVED, because there was no attribution to make.

Parameters:

outcomes (dict[str, PartyAttributionOutcome])

outcomes: dict[str, PartyAttributionOutcome]
property contradicted_fields: tuple[str, ...]

Return the fields the document filed under the wrong party.

resolve_party_attribution_by_colocation(*, draft, transcription)[source]

Return what the document’s own layout settles about each address value’s party.

Deterministic and side-effect free. Reads the draft and the transcription and returns a verdict per enrolled address field; it never rewrites a draft value and never re-assigns one to the other party.

It does not correct a transposition, it reports one. Moving a value to the region that contains it would replace the reader’s unverified assignment with this function’s, and the two rest on the same single reading of one document. A contradiction is surfaced for a person to settle.

Parameters:
  • draft (InvoiceDraft) – The read draft, carrying each party’s identity role evidence on its provenance envelopes.

  • transcription (DocumentTranscription) – The independently produced document text, line structure intact – which is why it is taken rather than a normalised string.

Returns:

an outcome per enrolled address field the draft carried a value for. Empty when the document states no usable party heading, which leaves every value on the unverified-attribution stamp. Measured, that is the outcome on every real document scored.

Return type:

PartyColocationResolution

party_attribution_findings(resolution)[source]

Return one blocking finding per value the document filed under the wrong party.

The consuming half, without which the resolver would detect a transposition and drop it – computed evidence nothing acts on, which is the failure this whole design keeps arriving back at. A contradiction that reaches no operator is indistinguishable from a check that never ran.

Only CONTRADICTED yields a finding. An unresolved value stays on the advisory stamp: firing a blocker on every document whose layout cannot be separated would refuse the ordinary case, which is the alert fatigue that makes an alert worthless on the case that matters.

Parameters:

resolution (PartyColocationResolution) – What co-location settled for the draft.

Return type:

tuple[DraftDiscrepancyFinding, ...]

Returns:

The findings, in field order. Empty when nothing was contradicted.

party_regions(*, draft, transcription)[source]

Return each party’s region text, keyed by role, or an empty mapping.

The partition itself, exposed rather than kept private because a gate asserting only the outcomes cannot distinguish “the partition was right and the values landed in it” from “the partition collapsed and everything read unresolved”. Both are green on an outcome assertion alone, and only one is the behaviour under test.

Both sides are required. One heading partitions nothing: with a single anchor every value on the page falls inside its region by construction, which would attribute the other party’s values to it. Fewer than two usable regions yields an empty mapping, and that is the commonest honest unresolved case rather than an error.

Parameters:
  • draft (InvoiceDraft) – The read draft, carrying identity role evidence on its envelopes.

  • transcription (DocumentTranscription) – The document text with its line structure intact.

Return type:

dict[str, str]

Returns:

Region text per party role, or an empty mapping when the document states no usable partition.