D30-F03-A01 / Released engineering topic

Exact Reference Matching: Preserve Evidence, Block Ambiguity, Explain Every Row

A production-minded guide to Exact Reference Match.

Exact Reference Matching: Preserve Evidence, Block Ambiguity, Explain Every RowD30 / D30-F03

Three pairs match perfectly. The reconciled bank asset still differs by 700 cents.

That is the useful starting point for reconciliation engineering: a match explains a relationship between records. A complete balance proof needs every movement and independent controls. This tutorial builds the first part carefully, retaining unmatched evidence so the second part remains possible.

You will implement a conservative one-to-one matcher in Python and TypeScript, select statement revisions as they were known at a cutoff, and explain why a duplicate reference cannot be rescued by a convenient amount. All records belong to fictional entity CB-A, ledger CB-GENERAL and routing account CEDAR-SETTLEMENT. They are synthetic teaching data.

Start from the account owner's asset

The internal side represents the owner's cash or nostro asset. An inflow increases that asset; an outflow decreases it. This differs from displaying a customer's credit-normal deposit liability.

Our selected statement adapter maps CRDT to inflow and DBIT to outflow. A reversal is a separate movement whose direction is already supplied. An inflow reversal increases the owner's asset; its flag does not ask us to negate the amount again. The original movement also remains in the booked history.

This boundary follows the native credit/debit and reversal fields in BIL's camt.053 guide. It is a provider/version-specific interpretation, not a universal file parser. Bank of America's guide also distinguishes booking and value dates and several reference fields. The adapter must map a declared shared field meaning deliberately.

Every normalized amount is a positive canonical decimal string in minor units, at most 30 digits. Supported USD and EUR use two minor decimals. Currency is retained, never converted or combined across currencies. A normalizer must route unsupported zero, fractional-minor, pending and ambiguous aggregate/detail entries explicitly.

Preserve the two histories and their independent heads

A source export contains atomic groups. Each group has a trusted observer receipt, available_at, and one or more revision events. The event records source sequence, stable row identity, revision, predecessor and source-version reference.

Internal accepted movements are immutable. A journal correction adds movements; it does not edit an old row. External evidence can be revised or withdrawn through a continuous version chain.

An independent context head states how far the trusted observer log is complete and when it was observed. We validate every supplied event through that head before applying a knowledge cutoff. Otherwise deleting a later revision could make the earlier evidence appear complete.

The query has a half-open booking-date window and a knowledge cutoff K. It selects a whole group when available_at ≤ K. A latest visible withdrawal is retained as a diagnostic and removes that row from active candidates. Latest revision selection happens before booking-date filtering, so a corrected date can move a row out of the window.

The receipt means the complete batch was available to the observer. It is not physical database COMMIT-time proof. JSON does not authenticate the source, routing scope or head. Those are trusted host obligations.

A receipt changes two facts atomically

Use the booking window [20 October, 21 October). The internal source contains:

RowMovementUSD centsStrong reference
I-FUNDInflow5000PAY-001
I-OUTOutflow1200PAY-002
I-REVInflow reversal1200REV-002
I-MISSINGOutflow700Missing

The first external receipt reports the corresponding 5000 inflow and 1200 reversal inflow, but PAY-002 is 1100. It also reports a 700 outflow with the declared missing-reference sentinel. Two pairs match. The two PAY-002 records disagree, and each missing-reference record remains unmatched: four unmatched records in total.

At 09:00 on 22 October, one atomic external group arrives. It revises PAY-002 to 1200 and withdraws the external missing-reference row. Both changes become visible at equality. Immediately before that receipt, neither change is selected; at the receipt, both are.

Both external evidence changes enter at the same observer receipt

Notice the different operations. Revising a statement amount replaces reported evidence. Withdrawing a row retracts that evidence. Neither operation creates a financial reversal. The booked reversal already present remains an ordinary active candidate with its reversal classification.

Exact identity must stay exact

The usable key is:

Plain text
(namespace, original printable ASCII reference value)

The policy preserves case, punctuation, leading zeros and outer spaces. Thus PAY-001, pay-001 and PAY-001 are three different identities. Namespace is part of the key: an instruction identifier is not automatically the same thing as an account-servicer reference.

Missing classification is separate. Null, empty, ASCII-space-only and exact uppercase NOTPROVIDED are missing under this declared Cedar profile. They produce explicit unmatched rows. They do not become a shared string such as "null" or "undefined". Lowercase notprovided remains usable bytes.

Why preserve outer spaces? This tutorial chooses conservative evidence identity. A production adapter might define a documented field-specific normalization policy, but such a policy changes the contract and its collision cases. Silently trimming inside the matcher would hide that choice.

Block reference ambiguity before inspecting the amount

Group selected active rows by usable reference key. If either side has more than one row, mark the entire bucket ambiguous.

Suppose the internal side contains PAY-002 outflows of 1200 and 1300 while the external side has one PAY-002 outflow of 1200. Matching the equal amounts looks attractive. It also conceals that the claimed strong reference identifies two internal rows. Under this conservative variant, all three members remain blocked.

A 2 by 1 reference bucket is blocked even when one amount agrees

This is a unique-reference-bucket algorithm. A composite reference-plus-amount variant could make a different decision; it must be named, specified and tested separately. Input order never resolves a collision.

For a unique one-to-one bucket, require exact equality of:

GateWhy it matters
CurrencyEqual digits in different units do not match
DirectionInflow and outflow have opposite asset effects
Amount in minor unitsStrong identity does not excuse a financial discrepancy
Reversal classificationPreserve the reported movement kind

Date equality is not an additional gate here. Both booking dates must be within the declared window; successful pairs retain external-minus-internal day gap. Future value dates are permitted as evidence and never substitute for booking dates. A future booked date relative to its observer receipt is rejected, including when that historical version would not be selected at K.

Implement selection, then matching

The selection helper is shared conceptually with later reconciliation topics, while each package contains its own pinned local code. It validates strict objects, source coverage and revision history before selecting rows.

Rendered selection and exact matching workflow

Open the full-size workflow. The ordering is the lesson: completeness and causal evidence selection precede any match decision.

The main Python function receives four wire arguments:

Python
result = exact_reference_match(
    case["query"],
    case["internal_groups"],
    case["external_groups"],
    case["context"],
)

The TypeScript equivalent is exactReferenceMatch. Its integers remain BigInt internally and canonical strings on the wire. Python uses arbitrary-precision integers. Monetary values never pass through floating point.

Successful output contains coverage, selected evidence, matches, unmatched rows and ambiguous buckets. Each selected row occurs in exactly one output class. Rejected or indeterminate input returns no partial selection and empty proposal arrays. A source gap or a cutoff beyond observation is indeterminate; an invalid schema, conflicting identity or broken revision chain is rejected.

Delivery arrays may be permuted. Explicit sequence evidence is canonicalized after structural/identity checks; duplicate or conflicting versions are rejected, never arbitrarily chosen. Results use stable ASCII ordering.

Three pairs still leave a difference

At the corrected receipt, PAY-001, PAY-002 and REV-002 match. The internal missing-reference outflow remains.

Three valid pairs coexist with a 700-cent difference

Compute all active movements, separately from matching:

Plain text
Internal = 5000 − 1200 + 1200 − 700 = 4300 cents
External = 5000 − 1200 + 1200       = 5000 cents
Difference: internal − external    = −700 cents

No extra pair or balancing plug is invented. If a separately supplied common opening were 10000, the movement-derived closes would be 14300 and 15000. The matcher has no independent closing controls, so it cannot declare a balance proof.

A completed result code means the proposal computation completed against the declared source heads. It does not mean reconciliation is operationally finished.

Explore receipts, ambiguity and strict failures

Open the guided exact-reference lab. Its initial preview is the corrected canonical result. Choose Begin to replay the observer receipts, then Step across the atomic revision boundary.

The scenario selector includes duplicate reference, currency mismatch, reversal-kind mismatch, raw case and space differences, missing future history, invalid future booked evidence, strict description controls, empty sources and huge exact amounts. A copied hypothetical reference comparison shows how byte identity changes proposals without editing any accepted source.

The lab preserves every supplied event. It reports validated versus visible source positions and lists selected row/version identities, so the graph cannot imply an unseen partial revision.

Check the result against an independent oracle

The dataset has 105 complete expected outputs. The first 23 histories and manually declared pairings preceded implementation. Additional independent edge expectations extend the strict contract; they do not import the matcher.

Tests compare complete outputs in Python and TypeScript, exercise seeded delivery permutations, verify alias isolation and independently count row allocation. Browser QA runs the embedded kernel against all expected outputs at 390, 768 and 1440 pixels. This establishes consistency with the specified reference cases, not external bank correctness or universal reconciliation performance. See validation evidence, data contract, independent calculations and references.

Carry the right evidence forward

Exact matching proposes deterministic row relationships. A durable host must authenticate scope and source coverage and separately prevent or review reuse of previously accepted or revised row evidence across runs. This pure API has no accepted-match registry and grants no posting authority.

Continue with fuzzy transaction matching for explicit candidate scores, many-to-many reconciliation for bounded grouped evidence, suspense-account resolution for controlled posting, and balance proof for independent controls and all movements.

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

All sources accessed 2026-10-25. Examples are synthetic and contain no customer information.

BIL camt.053 implementation guide

Source: BIL camt.053.001.02 Implementation Guidelines v2.1.
Version: v2.1, dated 2023-08-08; pages 58–59.
Accessed: 2026-10-25.
Supports: native entry amount/currency, credit/debit direction, reversal indicator and booked status. A reversal entry's credit/debit indicator already describes its movement.
Limitations: provider-specific and version-specific guide. It neither defines our normalized JSON nor certifies a parser, universal uniqueness policy or final settlement.

Bank of America bank-to-customer statement reference

Source: Bank-to-Customer Statement camt.053 reference guide.
Version: public guide checked on access date; no immutable publication version identified; pages 1–2.
Accessed: 2026-10-25.
Supports: separate statement/account/entry reference fields, booking/value dates and credit/debit fields. Field meanings must be mapped deliberately.
Limitations: a provider profile. EndToEndId, instruction identifiers and account-servicer references are not universally interchangeable reconciliation keys.

ISO 20022 catalogue

Source: ISO 20022 message definitions.
Version: live official catalogue, access-date snapshot; current versioning is independent of the BIL profile.
Accessed: 2026-10-25.
Supports: official access to message definitions and versioned message families.
Limitations: catalogue access does not make this teaching normalizer a complete camt implementation or licensed bank adapter.

Local specification and independent evidence

Source: Data contract, claim ledger, independent calculations, and complete fixture dataset.
Version: d30-exact-reference-match-v1, frozen 2026-10-25.
Accessed: 2026-10-25.
Supports: explicit Cedar scope, units, strict schemas, clocks, revision policy, conservative matching rule, complete expected outputs and independently worked arithmetic.
Limitations: synthetic pedagogical specification. Parity alone is not independent financial evidence; author-declared pairings and calculations precede implementation.

exact-match.ts
/** Conservative exact proposals; no parser, source authentication or posting. */
type Obj=Record<string,any>;
const QUERY=['schema_version','ledger_id','entity_id','reconciliation_account_id','period_start','period_end','knowledge_cutoff','reference_policy'];
const HEAD=['schema_version','source_id','side','ledger_id','entity_id','reconciliation_account_id','coverage_start','coverage_end','observed_at','head_sequence','receipt_ref'];
const GROUP=['schema_version','group_id','source_id','available_at','events'];
const EVENT=['sequence','row_id','revision','supersedes_revision','source_version_ref','action','movement'];
const MOVEMENT=['booking_status','booking_date','value_date','currency','direction','amount_minor','reference','description','is_reversal'];
const IDENT=/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
const POSITIVE=/^[1-9][0-9]{0,29}$/;
const NONNEGATIVE=/^(0|[1-9][0-9]{0,29})$/;
const DATE=/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
const STAMP=/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/;
const INDETERMINATE=new Set(['SOURCE_SEQUENCE_GAP','INCOMPLETE_SOURCE','KNOWLEDGE_BEYOND_SOURCE','COVERAGE_WINDOW_MISMATCH']);
export class ReconciliationError extends Error {}
function need(condition:any,code:string):asserts condition {if(!condition)throw new ReconciliationError(code)}
const object=(v:any):v is Obj=>v!==null&&typeof v==='object'&&!Array.isArray(v)&&[Object.prototype,null].includes(Object.getPrototypeOf(v));
const exact=(v:any,keys:string[]):v is Obj=>object(v)&&Object.keys(v).length===keys.length&&keys.every(k=>Object.hasOwn(v,k));
const full=(r:RegExp,s:string)=>r.exec(s)?.[0]===s;
const identifier=(v:any)=>typeof v==='string'&&full(IDENT,v);
const positive=(v:any)=>typeof v==='string'&&full(POSITIVE,v);
const nonnegative=(v:any)=>typeof v==='string'&&full(NONNEGATIVE,v);
const compare=(a:string,b:string)=>a<b?-1:a>b?1:0;
const compareInteger=(a:string,b:string)=>BigInt(a)<BigInt(b)?-1:BigInt(a)>BigInt(b)?1:0;
function civil(v:any):number|null {
 if(typeof v!=='string'||!full(DATE,v)||v.slice(0,4)==='0000')return null;
 const d=new Date(v+'T00:00:00.000Z');
 return Number.isFinite(d.getTime())&&d.toISOString().slice(0,10)===v?d.getTime():null;
}
function timestamp(v:any):boolean {
 if(typeof v!=='string'||!full(STAMP,v)||v.slice(0,4)==='0000')return false;
 const d=new Date(v);return Number.isFinite(d.getTime())&&d.toISOString()===v;
}
function same(a:any,b:any):boolean {
 if(Array.isArray(a))return Array.isArray(b)&&a.length===b.length&&a.every((v,i)=>same(v,b[i]));
 if(object(a))return object(b)&&Object.keys(a).length===Object.keys(b).length&&Object.keys(a).every(k=>Object.hasOwn(b,k)&&same(a[k],b[k]));
 return a===b;
}
function stable(v:any):string {
 if(Array.isArray(v))return '['+v.map(stable).join(',')+']';
 if(object(v))return '{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+stable(v[k])).join(',')+'}';
 return JSON.stringify(v);
}
function validateMovement(m:any,available:string){
 need(exact(m,MOVEMENT),'INVALID_SCHEMA');
 need(m.booking_status==='booked','UNSUPPORTED_BOOKING_STATUS');
 need(civil(m.booking_date)!==null&&civil(m.value_date)!==null,'INVALID_DATE');
 need(m.booking_date<=available.slice(0,10),'FUTURE_BOOKING_DATE');
 need(typeof m.currency==='string'&&['USD','EUR'].includes(m.currency),'UNSUPPORTED_CURRENCY');
 need(typeof m.direction==='string'&&['inflow','outflow'].includes(m.direction),'INVALID_DIRECTION');
 need(positive(m.amount_minor),'INVALID_AMOUNT');need(typeof m.is_reversal==='boolean','INVALID_REVERSAL_FLAG');
 need(typeof m.description==='string'&&[...m.description].length<=240&&[...m.description].every(c=>{const n=c.codePointAt(0)!;return !(n<32||(n>=127&&n<=159)||(n>=0xd800&&n<=0xdfff))}),'INVALID_DESCRIPTION');
 if(m.reference!==null){const r=m.reference;need(exact(r,['namespace','value']),'INVALID_REFERENCE');need(identifier(r.namespace),'INVALID_REFERENCE');need(typeof r.value==='string'&&r.value.length<=140&&[...r.value].every(c=>c.codePointAt(0)!>=32&&c.codePointAt(0)!<=126),'INVALID_REFERENCE')}
}
function validateEvent(e:any,available:string){
 need(exact(e,EVENT),'INVALID_SCHEMA');need(positive(e.sequence),'INVALID_SEQUENCE');
 need(identifier(e.row_id)&&identifier(e.source_version_ref),'INVALID_IDENTIFIER');
 need(positive(e.revision),'INVALID_REVISION');
 need(e.supersedes_revision===null||positive(e.supersedes_revision),'INVALID_REVISION');
 need(typeof e.action==='string'&&['upsert','withdraw'].includes(e.action),'INVALID_REVISION_ACTION');
 if(e.action==='upsert')validateMovement(e.movement,available);else need(e.movement===null,'INVALID_REVISION_ACTION');
}
function groupErrors(groups:any[],head:Obj){
 const errors:string[]=[];
 for(const g of groups){try{
  need(exact(g,GROUP),'INVALID_GROUP');need(identifier(g.group_id)&&identifier(g.source_id),'INVALID_IDENTIFIER');
  need(g.schema_version==='d30-reconciliation-group-v1','UNSUPPORTED_SCHEMA');need(g.source_id===head.source_id,'SOURCE_MISMATCH');
  need(timestamp(g.available_at),'INVALID_TIMESTAMP');need(g.available_at<=head.observed_at,'CLOCK_ORDER_VIOLATION');
  need(Array.isArray(g.events)&&g.events.length>=1&&g.events.length<=32,'INVALID_GROUP');
 }catch(error){if(error instanceof ReconciliationError)errors.push(error.message);else throw error}}
 if(errors.length)throw new ReconciliationError(errors.sort()[0]);
}
function rowReference(head:Obj,event:Obj):Obj {
 return {source_id:head.source_id,row_id:event.row_id,revision:event.revision,sequence:event.sequence,source_version_ref:event.source_version_ref};
}
function selectedRow(head:Obj,group:Obj,event:Obj):Obj {
 return {row_ref:rowReference(head,event),group_id:group.group_id,available_at:group.available_at,movement:structuredClone(event.movement)};
}
function selectSource(query:Obj,groups:any,head:Obj):[Obj[],Obj[],string,Obj]{
 need(Array.isArray(groups)&&groups.length<=4096,'INVALID_GROUP');groupErrors(groups,head);
 need(groups.reduce((n,g)=>n+g.events.length,0)<=4096,'INVALID_GROUP');
 const errors:string[]=[];
 for(const g of groups)for(const event of g.events){try{validateEvent(event,g.available_at)}catch(error){if(error instanceof ReconciliationError)errors.push(error.message);else throw error}}
 if(errors.length)throw new ReconciliationError(errors.sort()[0]);
 const grouped=new Map<string,Obj[]>();
 for(const g of groups){const canonical={...g,events:[...g.events].sort((a,b)=>compareInteger(a.sequence,b.sequence)||compare(stable(a),stable(b)))};const list=grouped.get(g.group_id)??[];list.push(canonical);grouped.set(g.group_id,list)}
 need(![...grouped.values()].some(items=>items.some(g=>!same(g,items[0]))),'GROUP_ID_CONFLICT');
 need(![...grouped.values()].some(items=>items.length>1),'DUPLICATE_GROUP');
 const identities=new Map<string,Obj[]>();
 for(const g of groups)for(const e of g.events){const key=JSON.stringify([e.row_id,e.revision]),list=identities.get(key)??[];list.push(e);identities.set(key,list)}
 need(![...identities.values()].some(items=>items.some(e=>!same(e,items[0]))),'EVENT_ID_CONFLICT');
 need(![...identities.values()].some(items=>items.length>1),'DUPLICATE_EVENT');
 const events:Obj[]=groups.flatMap(g=>g.events);
 need(new Set(events.map(e=>e.sequence)).size===events.length,'SEQUENCE_ID_CONFLICT');
 for(const g of groups)need(new Set(g.events.map((e:Obj)=>e.row_id)).size===g.events.length,'DUPLICATE_ROW_IN_GROUP');
 const sequences=events.map(e=>BigInt(e.sequence)).sort((a,b)=>a<b?-1:a>b?1:0),headSequence=BigInt(head.head_sequence);
 need(sequences.length===0||sequences.at(-1)!<=headSequence,'HEAD_SEQUENCE_MISMATCH');
 need(sequences.every((n,i)=>n===BigInt(i+1)),'SOURCE_SEQUENCE_GAP');
 need(BigInt(sequences.length)===headSequence,'INCOMPLETE_SOURCE');
 const canonical=[...grouped.values()].map(items=>items[0]).sort((a,b)=>compareInteger(a.events[0].sequence,b.events[0].sequence));
 for(const g of canonical)need(BigInt(g.events.at(-1).sequence)-BigInt(g.events[0].sequence)+1n===BigInt(g.events.length),'GROUP_INTERVAL_CONFLICT');
 let lastAvailable:string|null=null,visibleThrough='0';
 const latestAll=new Map<string,[Obj,Obj]>(),latestVisible=new Map<string,[Obj,Obj]>();
 for(const g of canonical){
  need(lastAvailable===null||g.available_at>=lastAvailable,'CLOCK_ORDER_VIOLATION');lastAvailable=g.available_at;
  for(const e of g.events){
   const previous=latestAll.get(e.row_id);
   if(head.side==='internal')need(e.revision==='1'&&e.supersedes_revision===null&&e.action==='upsert'&&previous===undefined,'INTERNAL_ROW_IMMUTABLE');
   else if(previous===undefined){need(e.revision==='1'&&e.supersedes_revision===null,'INVALID_REVISION');need(e.action==='upsert','INVALID_REVISION_ACTION')}
   else{const prior=previous[1];need(BigInt(e.revision)===BigInt(prior.revision)+1n&&e.supersedes_revision===prior.revision,'INVALID_REVISION');if(e.action==='withdraw')need(prior.action==='upsert','INVALID_REVISION_ACTION')}
   latestAll.set(e.row_id,[g,e]);if(g.available_at<=query.knowledge_cutoff)latestVisible.set(e.row_id,[g,e]);
  }
  if(g.available_at<=query.knowledge_cutoff)visibleThrough=g.events.at(-1).sequence;
 }
 const active:Obj[]=[],withdrawn:Obj[]=[];let outside=0;
 for(const [rowId,[g,e]]of [...latestVisible.entries()].sort((a,b)=>compare(a[0],b[0]))){
  const row=selectedRow(head,g,e);
  if(e.action==='withdraw'){delete row.movement;withdrawn.push(row)}
  else if(e.movement.booking_date>=query.period_start&&e.movement.booking_date<query.period_end)active.push(row);
  else outside++;
 }
 const coverage:Obj={};for(const k of ['source_id','receipt_ref','observed_at','coverage_start','coverage_end','head_sequence'])coverage[k]=head[k];
 coverage.validated_through_sequence=head.head_sequence;coverage.visible_through_sequence=visibleThrough;
 return[active,withdrawn,String(outside),coverage];
}
export function selectReconciliationRows(query:any,internalGroups:any,externalGroups:any,context:any,expectedSchema='d30-exact-reference-match-v1'):Obj {
 try{
  need(exact(query,QUERY),'INVALID_SCHEMA');need(query.schema_version===expectedSchema,'UNSUPPORTED_SCHEMA');
  need(['ledger_id','entity_id','reconciliation_account_id'].every(k=>identifier(query[k])),'INVALID_IDENTIFIER');
  const start=civil(query.period_start),end=civil(query.period_end);need(start!==null&&end!==null,'INVALID_DATE');
  need((end-start)/86400000>=1&&(end-start)/86400000<=366,'INVALID_PERIOD');
  need(timestamp(query.knowledge_cutoff),'INVALID_TIMESTAMP');
  need(query.knowledge_cutoff>=query.period_end+'T00:00:00.000Z','KNOWLEDGE_BEFORE_PERIOD_END');
  need(query.reference_policy==='BYTE_EXACT_ASCII_V1','UNSUPPORTED_REFERENCE_POLICY');
  need(exact(context,['internal_head','external_head']),'INVALID_CONTEXT');
  for(const side of ['internal','external']){
   const h=context[side+'_head'];need(exact(h,HEAD),'INVALID_CONTEXT');
   need(h.schema_version==='d30-reconciliation-head-v1'&&h.side===side,'INVALID_CONTEXT');
   need(['source_id','ledger_id','entity_id','reconciliation_account_id','receipt_ref'].every(k=>identifier(h[k])),'INVALID_CONTEXT');
   need(['ledger_id','entity_id','reconciliation_account_id'].every(k=>h[k]===query[k]),'SCOPE_MISMATCH');
   const a=civil(h.coverage_start),b=civil(h.coverage_end);need(a!==null&&b!==null&&a<b,'INVALID_CONTEXT');
   need(timestamp(h.observed_at)&&nonnegative(h.head_sequence),'INVALID_CONTEXT');
   need(h.coverage_start<=query.period_start&&h.coverage_end>=query.period_end,'COVERAGE_WINDOW_MISMATCH');
   need(query.knowledge_cutoff<=h.observed_at,'KNOWLEDGE_BEYOND_SOURCE');
  }
  need(context.internal_head.source_id!==context.external_head.source_id,'INVALID_CONTEXT');
  const selected:Obj={internal:[],external:[],withdrawn:{internal:[],external:[]},out_of_window_counts:{internal:'0',external:'0'}},coverage:Obj={};
  for(const [side,groups]of [['internal',internalGroups],['external',externalGroups]]as const){const [active,withdrawn,count,cov]=selectSource(query,groups,context[side+'_head']);selected[side]=active;selected.withdrawn[side]=withdrawn;selected.out_of_window_counts[side]=count;coverage[side]=cov}
  return{status:'ok',code:'RECONCILIATION_ROWS_SELECTED',coverage,selected};
 }catch(error){if(error instanceof ReconciliationError)return{status:INDETERMINATE.has(error.message)?'indeterminate':'rejected',code:error.message,coverage:null,selected:null};throw error}
}
export function referenceKey(reference:any):[string,string]|null {
 if(reference===null||reference.value.replace(/ /g,'')===''||reference.value==='NOTPROVIDED')return null;
 return[reference.namespace,reference.value];
}
export function exactReferenceMatch(query:any,internalGroups:any,externalGroups:any,context:any):Obj {
 const result=selectReconciliationRows(query,internalGroups,externalGroups,context);
 if(result.status!=='ok')return{...result,matches:[],unmatched:[],ambiguous:[]};
 const buckets=new Map<string,{key:[string,string];internal:Obj[];external:Obj[]}>(),unmatched:Obj[]=[];
 for(const side of ['internal','external']as const)for(const row of result.selected[side]){
  const key=referenceKey(row.movement.reference);
  if(key===null)unmatched.push({side,row_ref:structuredClone(row.row_ref),reason:'MISSING_REFERENCE',mismatches:[]});
  else{const encoded=JSON.stringify(key),bucket=buckets.get(encoded)??{key,internal:[],external:[]};bucket[side].push(row);buckets.set(encoded,bucket)}
 }
 const matches:Obj[]=[],ambiguous:Obj[]=[];
 for(const bucket of [...buckets.values()].sort((a,b)=>compare(a.key[0],b.key[0])||compare(a.key[1],b.key[1]))){
  const [namespace,value]=bucket.key,left=bucket.internal,right=bucket.external;
  if(left.length>1||right.length>1)ambiguous.push({reference:{namespace,value},internal_row_refs:left.map(r=>structuredClone(r.row_ref)),external_row_refs:right.map(r=>structuredClone(r.row_ref)),reason:'NON_UNIQUE_REFERENCE'});
  else if(left.length===0||right.length===0){const side=left.length?'internal':'external',row=(left.length?left:right)[0];unmatched.push({side,row_ref:structuredClone(row.row_ref),reason:'NO_COUNTERPART',mismatches:[]})}
  else{
   const [l,r]=[left[0],right[0]],[a,b]=[l.movement,r.movement];
   const fields=['currency','direction','amount_minor','is_reversal'].filter(k=>a[k]!==b[k]);
   if(fields.length){for(const [side,row]of [['internal',l],['external',r]]as const)unmatched.push({side,row_ref:structuredClone(row.row_ref),reason:'FIELD_MISMATCH',mismatches:[...fields]})}
   else matches.push({reference:{namespace,value},internal_row_ref:structuredClone(l.row_ref),external_row_ref:structuredClone(r.row_ref),currency:a.currency,direction:a.direction,amount_minor:a.amount_minor,is_reversal:a.is_reversal,booking_date_gap_days:(civil(b.booking_date)!-civil(a.booking_date)!)/86400000});
  }
 }
 unmatched.sort((a,b)=>(a.side==='internal'?0:1)-(b.side==='internal'?0:1)||compare(a.row_ref.row_id,b.row_ref.row_id)||compareInteger(a.row_ref.revision,b.row_ref.revision));
 return{status:'ok',code:'EXACT_MATCH_COMPLETE',coverage:result.coverage,selected:result.selected,matches,unmatched,ambiguous};
}

Full-height labexact reference labOpen full screen
Written by

Fintech engineer building market-data and financial systems, and the author of every article, glossary record, and reference implementation on The Fintech Builder.