cadrumo.application.ledger.confirmation_gate module¶
The blocking half of the human review gate: what cannot be confirmed blind.
A draft is a proposal. Turning one into an Invoice is
the moment a machine reading becomes a filing-grade fact, and this module is the
only thing standing between the two. It answers one question — which of this
draft’s problems must a human answer before it may be confirmed at all — and
refuses the confirm until each named one carries an explicit, per-finding answer.
There is no bulk resolution and there will not be one. Not a
--confirm-all, not a --force, not an “accept remaining” that resolves the
set in one keystroke. The entire value of the gate is the per-document attention
it forces; a flag that clears every blocker at once re-creates exactly the rubber
stamp the gate exists to remove, and it would be asked for the first time someone
has a hundred documents waiting. Every resolution names ONE blocker by its id.
Completeness is by construction, not by diligence.
BLOCKING_REASON_BY_DISCREPANCY_KIND maps every member of
DraftDiscrepancyKind to a
ConfirmationBlockReason, and the module refuses to import if a
member is missing. A new deterministic check therefore cannot land as a finding
that quietly fails to block — the tree stops instead. That is the opposite of a
hand-listed set, which passes silently while covering half the axis.
The second blocker source is the grounding pass rather than the discrepancy
checks: a counterparty identifier whose reading was
AMBIGUOUS produced competing candidates and
no decision. That is not an arithmetic failure and raises no
DraftDiscrepancyFinding, but confirming it picks one
real taxpayer’s identifier over another’s by accident.
See also
InvoiceDraftThe proposal this gate guards.
closure_findings()Where the arithmetic blockers come from.
InvoiceConfirmationRecordWhat the resolutions are persisted into once the confirm succeeds.
- BLOCKING_REASON_BY_DISCREPANCY_KIND: Mapping[DraftDiscrepancyKind, ConfirmationBlockReason]¶
Which review-gate reason each deterministic check’s failure raises.
Total over
DraftDiscrepancyKindby construction — the assertion below fails the import when a member is unmapped, so a newly-landed check cannot produce a finding that silently does not block. Every member maps to a reason because every member names a real defect: an unmapped member would be a check whose failure the product decided not to care about, and that decision belongs in the check’s own existence rather than hidden in this table.A deterministic condition the product decides NOT to block on is therefore not represented here at all. It is reported through the non-blocking advisory channel instead –
country_vocabulary_advisory()andparty_attribution_advisory()are the two – which the review surface projects onto the envelope’s typed notices. An exemption row in this table would be the worse shape: the reader could no longer tell a blocking axis from an advisory one by looking at the axis.
- IDENTITY_FIELDS: frozenset[str]¶
Draft fields whose ambiguity is an ambiguous COUNTERPARTY, not a soft field.
Named rather than inlined so the exclusion is auditable. An ambiguous supplier name is a nuisance the operator retypes; an ambiguous tax identifier selects which real taxpayer the record names, feeds Modelo 347 per counterparty, and drives deductibility — so it blocks and the name does not.
- exception ConfirmationBlockedError(message=None, *, context=None, translated_message=None, precondition_verdict=None)[source]¶
Bases:
LedgerPreconditionErrorMixin,CadrumoErrorRaised when a confirm is attempted with an unresolved blocking finding.
- Parameters:
message (str | None)
translated_message (str | None)
precondition_verdict (PreconditionVerdict | None)
- Return type:
None
- code¶
- class ConfirmationBlocker(**data)[source]¶
Bases:
BaseModelOne problem a human must answer before this draft may be confirmed.
- Variables:
blocker_id – Clock-free, deterministic address for this blocker, derived from what the blocker IS. Re-reading the same document raises the same id, so an operator can resolve a blocker they saw in an earlier listing without the ids having shifted under them — and a resolution can never be pointed at a blocker by position.
reason – Which class of refusal this is.
field – The draft field the blocker is about, or
Nonewhen it is about a relationship between several.detail – Operator-facing explanation naming the figures or readings involved.
candidates – The competing readings, when the blocker is an ambiguity the operator resolves by choosing one. Empty otherwise.
- Parameters:
data (Any)
- blocker_id: str¶
- reason: ConfirmationBlockReason¶
- field: str | None¶
- detail: str¶
- candidates: tuple[FieldAmbiguityCandidate, ...]¶
- property candidate_values: tuple[str, ...]¶
Return the candidate values, for checking a choose-candidate resolution.
- property candidate_digests: tuple[str, ...]¶
Return each candidate’s value as the review surface renders it.
The operator-facing surfaces emit every payload through the redaction funnel, so an ambiguous IDENTITY reaches the operator as a digest. That is correct and stays: the value that could not be established is exactly the value that must not cross an output boundary.
It also means the value is not a token the operator can type back. This is the form they actually saw, so it is the form they can name.
- names_a_candidate(token)[source]¶
Return whether token identifies one of this blocker’s readings.
Accepts the value or the digest the surface rendered it as. Both are choices rather than assertions, which is the property the refusal below exists to enforce: a digest matches only a reading the document actually offered, and it cannot be constructed for a value that was never a candidate without already holding that value.
Accepting the digest does not weaken the check – it makes it reachable. A resolution can only ever have been expressed by an operator who had the value in hand, which excluded every operator adjudicating from the review surface, where the value is redacted by design.
- class FindingResolution(**data)[source]¶
Bases:
BaseModelThe operator’s explicit answer to exactly one named blocker.
One resolution, one blocker, always. The id is required and is matched against the blockers the draft actually raises, so a resolution naming nothing is a refusal rather than a silent no-op — the failure mode where an operator believes they answered a finding and the confirm proceeded on a different one entirely.
- Variables:
blocker_id – The blocker being answered.
action – How it was settled.
value – The chosen or supplied value, as printed. Required for
CHOOSE_CANDIDATEandSUPPLY_VALUE, refused forATTEST, which changes no figure.note – The operator’s reason, required for
ATTEST— an attestation with no stated basis is a waiver wearing a resolution’s shape.
- Parameters:
data (Any)
- blocker_id: str¶
- action: FindingResolutionAction¶
- value: str | None¶
- note: str¶
- blocker_id(*, reason, field, detail)[source]¶
Return the clock-free derived address for one blocker.
Folds only what the blocker IS. A timestamp would make a re-read of the same unchanged document raise different ids, so a resolution captured from one listing would refuse against the next — and the operator would learn to re-run and re-copy rather than to answer.
- Return type:
- Parameters:
reason (ConfirmationBlockReason)
field (str | None)
detail (str)
- confirmation_blockers(draft)[source]¶
Return every problem that must be answered before draft may be confirmed.
Deterministic and total: the same draft always yields the same blockers with the same ids. An empty result means nothing blocks — never that the document is correct, because a check whose terms the document did not state is not run at all.
- Parameters:
draft (
InvoiceDraft) – The reviewed draft, carrying the discrepancies its deterministic checks raised and the provenance envelopes its grounding pass wrote.- Returns:
the discrepancy findings in the order the checks produced them, then the ambiguous identity fields in draft-field declaration order.
- Return type:
Blockers in a stable order
- resolved_blockers(*, draft, resolutions=())[source]¶
Return draft’s blockers, having refused unless every one is answered.
The single gate every confirm path passes through. It returns the blockers rather than a bare
Noneso the caller persists exactly the set that was answered, and cannot record a different one.- Parameters:
draft (
InvoiceDraft) – The draft being confirmed.resolutions (
Sequence[FindingResolution]) – One explicit answer per blocker. Order is irrelevant; coverage is not.
- Return type:
- Returns:
The blockers the draft raised, all of them resolved.
- Raises:
ConfirmationBlockedError – When a blocker carries no resolution, when a resolution names no blocker this draft raises, when two resolutions answer one blocker, or when a
CHOOSE_CANDIDATEnames neither a recorded candidate’s value nor the digest the surface rendered it as.
A choose-candidate resolution may name its reading EITHER way, and the second form is what makes the ambiguity surface usable at all. An ambiguous identity reaches the operator through the redaction funnel, so both competing readings render as digests; requiring the value asked them to type a string the surface had deliberately withheld, and the review command’s whole purpose for identity fields is that the operator – who holds the document – decides between the readings. They could see which was right and had no way to say so.
The value is never consumed beyond this check: it attests WHICH reading was chosen and never becomes the record’s data, so nothing downstream needed it to have crossed the boundary.