# Exact records, revisions and evidence - data contract

Status: frozen 25 October 2026 before implementation; refinements below included. Applies to Exact Reference Match and the shared evidence input layer for later reconciliation topics.

## Sourced context and selected teaching profile

Official BIL camt.053.001.02 Implementation Guidelines v2.1, dated8August2023, describe entry amount/currency, credit/debit, reversal flag, booked status and distinct references/dates. Its page59 reversal description establishes the essential boundary: the entry's CRDT/DBIT already describes its reversal movement. Do not flip that sign again. The Bank of America camt reference guide distinguishes statement/account/entry references, booking/value dates and credit/debit direction. These are provider-specific guides, not our normalized JSON schema or a full ISO20022 parser.

Sources checked25October2026:
- https://www.bil.com/BIL-digital/assets/doc/BIL_CAMT053_V2_final.pdf — pages58–59; version2.1/2023-08-08.
- https://images.em.bankofamerica.com/GTS/ISO_20022/ReferenceGuideBanktoCustomerStatement(CAMT.053).pdf — pages1–2; no immutable publication version identified.
- https://www.iso20022.org/iso-20022-message-definitions — catalogue access to message definitions, not proof that this tutorial implements any complete message version.

Every example is synthetic. The normalizer is a trusted adapter obligation; this tutorial begins after extraction. It must map a declared actual source field into a bilaterally meaningful reference namespace, convert supported decimal amounts exactly to minor units and preserve provenance. It must explicitly route zero amounts, fractional minor units, pending entries, unsupported currency and ambiguous aggregate/detail extraction instead of silently discarding or rounding them. No XML parser, bank certification or universal reference-uniqueness claim is made.

## Financial perspective and scope

Entity CB-A/ledger CB-GENERAL is the **account owner**. direction=inflow means money increases the owner's bank-asset balance; outflow decreases it. Under the selected adapter, statement CRDT maps inflow and DBIT maps outflow. Internal counterpart is the movement of the owner's cash/nostro asset, not a customer's credit-normal deposit sign.

Amounts are positive canonical decimal strings,≤30 digits; currency USD/EUR, two minor decimals, no conversion. A multi-currency reconciliation routing scope may contain currency-separated records; amounts in different currencies never match or add. reconciliation_account_id is the trusted host's bilateral account-routing identity, not an inferred IBAN or ownership proof. Query/head scope binds ledger/entity/routing account; every movement inherits it.

A reversal is a separate booked active movement with its already directed sign. The original remains a booked movement. is_reversal is retained classification, not a command to remove the original, reverse its sign again, or authorize another journal. Native reversal lineage is an upstream adapter/posting obligation; this input does not fabricate target IDs when absent.

A statement retraction/revision is evidence replacement. It is distinct from an actual new financial reversal.

## Shared API and strict query

Python `exact_reference_match(query,internal_groups,external_groups,context)` / TypeScript `exactReferenceMatch`. Common helper selects source rows under the same query semantics for later topics; no sibling runtime import is required.

Query keys exactly:
- schema_version: d30-exact-reference-match-v1 (later algorithms use their own version with the same common scope/window/K fields).
- ledger_id, entity_id, reconciliation_account_id: strict Stage 1 ASCII identifiers.
- period_start, period_end: genuine YYYY-MM-DD,1–366 civil days, half-open booking-date window.
- knowledge_cutoff: strict genuine Gregorian UTC milliseconds; K≥period_end midnight.
- reference_policy: BYTE_EXACT_ASCII_V1.

Context keys exactly internal_head,external_head. Heads are trusted host evidence **independent of the supplied groups**, not inferred from their last event. JSON does not authenticate them.

Head keys exactly:
schema_version(d30-reconciliation-head-v1),source_id,side,ledger_id,entity_id,reconciliation_account_id,coverage_start,coverage_end,observed_at,head_sequence,receipt_ref.
- side is internal or external, matching its context position.
- coverage interval [coverage_start,coverage_end) contains the query window.
- observed_at≥K, else KNOWLEDGE_BEYOND_SOURCE.
- head_sequence canonical nonnegative string≤30 digits.
- source_id/receipt_ref/scope identifiers use the strict ASCII identifier grammar.
- coverage is completeness relative to the trusted observer/extraction log and declared window; not financial finality or proof that every real-world transaction was posted.

## Atomic source groups and revision events

Each source input is a list of groups. Group keys exactly:
schema_version(d30-reconciliation-group-v1),group_id,source_id,available_at,events.

available_at is the trusted observer receipt of the **entire** normalized source batch. All events become knowledge-visible together at equality. It is neither source document creation time nor physical COMMIT proof.

Group events are 1–32 revision events;≤4096 groups and≤4096 total events per source in this portable profile. These are reference resource bounds, not bank limits. Empty source head 0/groups[] is valid.

Event keys exactly:
sequence,row_id,revision,supersedes_revision,source_version_ref,action,movement.
- sequence: canonical positive string≤30 digits, gap-free normalized observer-log position 1..head. It is not native statement numbering or a SQL sequence.
- row_id: stable strict identifier within one source/scope.
- revision: canonical positive string≤30 digits, starting 1.
- supersedes_revision: null at revision 1, otherwise exact prior revision string.
- source_version_ref: strict identifier for the adapter's retained document/extract version.
- action: upsert or withdraw.
- movement: complete object for upsert, null for withdraw.

First version is upsert/revision 1/null predecessor. An external row's next revision is previous+1 and points to that exact previous version; it must follow it in the observer log. External withdraw requires a prior currently-upsert row. Reinstatement may be a later upsert with the continued chain. Repeated withdraw without intervening upsert rejects INVALID_REVISION_ACTION.

Internal accepted movements are immutable: revision 1/upsert only. A journal correction creates additional movement rows (within one atomically exported group where appropriate), never changes the original booked movement's amount. External revisions change the reported evidence, not the internal ledger.

One row may appear at most once within one group. Duplicate identity classification is global and order-independent after structural validation. Bucket groups by group_id; changed variants win GROUP_ID_CONFLICT over exact duplicate copies. If no changed group exists, repeated identical groups reject DUPLICATE_GROUP. Then bucket all events by (row_id,revision): any changed event payload wins EVENT_ID_CONFLICT over exact duplicate copies, regardless of delivery order; if no changed event exists, repeats reject DUPLICATE_EVENT. A changed event includes any different sequence/predecessor/source-version/action/movement field. Distinct event identities sharing a sequence reject SEQUENCE_ID_CONFLICT. Check these identity classes before version chains or coverage, in the fixed group-conflict/group-duplicate/event-conflict/event-duplicate/sequence-conflict order. Within one group, different revisions of the same row reject DUPLICATE_ROW_IN_GROUP. No arbitrary first-copy winner is retained.

## Complete coverage and deterministic delivery order

Validate all supplied history through each independent head **before** K/window filtering. A missing unselected later revision still makes the supplied export incomplete relative to the head. Invalid structure/history rejects; missing coverage or insufficient source observation produces indeterminate with no matches.

Delivery arrays are deliberately order-independent in this profile: canonicalize groups by their declared minimum event sequence and events by numeric sequence only after strict schema/identity/duplicate checks. Require each group to occupy one consecutive interval with no overlapping/interleaved groups and require the entire union 1..head. Receipt clocks must be nondecreasing in that canonical log order and≤head.observed_at.

This is canonicalization of explicit source-log evidence, not an input-order or amount-based matching tie break. Results, mismatches, selected rows and ambiguity members are sorted by stable ASCII identities, independently of delivery permutation.

Apply K to whole groups inclusively (including all tied receipts). For each row choose its highest valid revision among visible groups. A selected withdraw contributes no active movement and is retained as an explicit withdrawal diagnostic. Older visible versions are superseded, not duplicate candidates.

Only after version selection, filter selected active rows by booking_date in [start,end). Value date remains evidence and is not substituted for booking date. Historical versions can have dates outside the current window; revision selection precedes window selection so a later date correction can move an item out of the queried window.

## Exact movement schema

Movement keys exactly:
booking_status,booking_date,value_date,currency,direction,amount_minor,reference,description,is_reversal.
- booking_status is booked only in this profile; pending/informational records route UNSUPPORTED_BOOKING_STATUS.
- booking_date,value_date are genuine YYYY-MM-DD; exact-reference matching does not require their equality across sources. Every upsert must have booking_date≤its group available_at calendar date, including historical/unselected versions; future booked movements reject FUTURE_BOOKING_DATE. This profile explicitly allows a future value_date as retained evidence: it does not imply present availability, finality or interest entitlement, and it is never substituted for booking_date in matching.
- currency USD/EUR; direction inflow/outflow.
- amount_minor positive canonical string≤30 digits. No negative/bool/numeric/zero/exponent/leading zero/rounding.
- description is a Unicode-scalar string≤240 scalars, no lone surrogates, C0 controls U+0000..U+001F, DEL U+007F or C1 controls U+0080..U+009F. This is evidence for later fuzzy scoring, not strong reference normalization.
- is_reversal exact bool.
- reference is null or exact object namespace,value.
- namespace: strict ASCII identifier declaring an actually shared reference meaning; raw EndToEndId, instruction and account-servicer fields are not assumed interchangeable.
- value: string of 0–140 printable ASCII characters U+0020..U+007E. Raw bytes are retained.

## Strong reference and missing policy

Usable identity is (namespace,original ASCII value), with **no** case folding, punctuation stripping, zero stripping, Unicode normalization or outer-space trimming.

Missing classification is separate: null, empty value, ASCII-space-only value or the exact declared sentinel NOTPROVIDED is MISSING_REFERENCE. The sentinel policy is a Cedar adapter/profile choice, not a claim that every bank field uses that value. Mixed outer spaces remain meaningful: " PAY-01" differs from "PAY-01";"PAY-01 " differs from both. Lowercase notprovided is not silently converted to the uppercase sentinel.

Wrong type/control/non-ASCII reference rejects INVALID_REFERENCE; missing reference remains an explicit unmatched record and never becomes the string "None"/"null"/"undefined" or an empty shared match key.

## Conservative exact matching

Bucket the selected active in-window records by usable namespace+exact reference **before** amount/currency/direction/reversal-kind gates. If either side has more than one row in a bucket, block the **entire bucket** as NON_UNIQUE_REFERENCE, even when amounts would select one plausible pair. No composite amount key rescue, greedy input-order choice or maximum-amount heuristic.

For a unique 1×1 bucket, match only if currency,direction,amount_minor,is_reversal all agree. Otherwise both rows are unmatched with FIELD_MISMATCH and the ordered mismatching field list. A unique 1×0 or 0×1 row is NO_COUNTERPART. Missing-reference rows are MISSING_REFERENCE. Different namespaces are different buckets even when values are equal.

Date equality is not an additional exact gate: both booking dates must be in the declared query window. Preserve a signed booking_date_gap_days diagnostic on a successful pair. A stricter date/composite-key variant is outside this initial tutorial.

Every selected row belongs to exactly one match, unmatched record or blocked ambiguity bucket. A pair consumes each side once within the run. Outputs are proposals; separate algorithms/runs need an explicit host-managed exclusion/resolution registry before durable acceptance. A changed source revision must trigger review of prior accepted reconciliation evidence, not automatic reassignment or a second suspense posting.

## Frozen helper and stable output

Always keys status,code,coverage,selected,matches,unmatched,ambiguous.

Common exported helper Python select_reconciliation_rows(query,internal_groups,external_groups,context,expected_schema="d30-exact-reference-match-v1") / TypeScript selectReconciliationRows(query,internalGroups,externalGroups,context,expectedSchema="d30-exact-reference-match-v1"). expected_schema is a developer-selected literal for a pinned local wrapper, not a query-provided bypass. The helper checks the exact common query fields above, with that one schema-version expectation. A later wrapper validates its own extra policy/context fields first, then constructs the exact common projection; unknown wire fields must not be silently discarded without that wrapper check.

Helper returns exactly status,code,coverage,selected. Success is ok/RECONCILIATION_ROWS_SELECTED. Main exact API adds matches/unmatched/ambiguous and changes success code to EXACT_MATCH_COMPLETE. Failure retains the helper status/code and null coverage/selected.

Each coverage side has exactly source_id,receipt_ref,observed_at,coverage_start,coverage_end,head_sequence,validated_through_sequence,visible_through_sequence. coverage has exactly internal,external. visible position is a complete group end selected by inclusive K.

Row reference keys exactly source_id,row_id,revision,sequence,source_version_ref. Selected active row keys exactly row_ref,group_id,available_at,movement. Withdrawal diagnostic keys exactly row_ref,group_id,available_at. No invented concatenated ID is mistaken for a Stage 1 business identity.

selected keys exactly internal,external,withdrawn,out_of_window_counts. internal/external are active in-window selected-row arrays. withdrawn has exactly internal/external arrays of visible latest withdrawal diagnostics. out_of_window_counts has exactly internal/external canonical nonnegative string counts for visible latest active rows excluded by booking window. Future-unavailable row/group identities do not appear in selected.

matches entry keys exactly reference,internal_row_ref,external_row_ref,currency,direction,amount_minor,is_reversal,booking_date_gap_days. The gap is external booking date minus internal booking date, a bounded JSON integer.
unmatched entry keys exactly side,row_ref,reason,mismatches. Reason is MISSING_REFERENCE/NO_COUNTERPART/FIELD_MISMATCH; mismatches is [] except the ordered list currency,direction,amount_minor,is_reversal containing only fields that differ.
ambiguous entry keys exactly reference,internal_row_refs,external_row_refs,reason. Reason NON_UNIQUE_REFERENCE. Every selected row is allocated exactly once across these three result classes.

Output order is fixed: selected/withdrawn rows by row_id then numeric revision; matches/ambiguities by namespace then raw ASCII reference, then internal/external row_id; unmatched by side (internal before external),row_id,numeric revision. Identity-bucket error precedence above is independent of arbitrary delivery permutation.

Rejected or indeterminate returns typed code, coverage:null,selected:null,matches:[],unmatched:[],ambiguous:[]. EXACT_MATCH_COMPLETE means the proposal computation completed against the declared heads, not that every row matched or closing balances agree.

Shared errors include INVALID_SCHEMA/INVALID_IDENTIFIER/INVALID_DATE/INVALID_TIMESTAMP/INVALID_AMOUNT/INVALID_REFERENCE/INVALID_DESCRIPTION/INVALID_CONTEXT/SCOPE_MISMATCH/SOURCE_MISMATCH/INVALID_GROUP/INVALID_SEQUENCE/INVALID_REVISION/INVALID_REVISION_ACTION/INTERNAL_ROW_IMMUTABLE/DUPLICATE_GROUP/GROUP_ID_CONFLICT/DUPLICATE_EVENT/EVENT_ID_CONFLICT/SEQUENCE_ID_CONFLICT/DUPLICATE_ROW_IN_GROUP/FUTURE_BOOKING_DATE/CLOCK_ORDER_VIOLATION. Coverage routes SOURCE_SEQUENCE_GAP/INCOMPLETE_SOURCE/KNOWLEDGE_BEYOND_SOURCE/COVERAGE_WINDOW_MISMATCH as indeterminate. Fixed validation phases and independent complete-output fixtures record exact precedence.

## Downstream boundaries

Fuzzy shares active revision/currency/direction/scope/amount facts and evaluates all selected active rows under its separately frozen score/date/tie contract. Pure APIs enforce one-row-once only within a run; the downstream host acceptance registry must veto or review previously accepted or revised evidence. Never omit source history to manufacture exclusions. Many-to-many shares the same active records and exact units, with separate grouping/fee/search bounds. Suspense must bind evidence row/version identities and authority; a match proposal is never posting permission. Balance proof needs independently versioned opening/closing controls at K and sums **all** selected signed movements, not only matched pairs; that control envelope is a later explicit extension.

Historical-case decision: not useful without a complete reusable statement and matched internal ledger/source chronology. The companion independent oracle is synthetic and intentionally contains unmatched/mismatched/ambiguous evidence.

