A later correction can change an earlier day's balance. If you query only the economic date, you may reproduce today's corrected history while claiming to reproduce yesterday's report. Two cutoffs make that distinction explicit: what economic activity belongs in the balance, and what complete evidence had this observer received?
This tutorial builds an exact posted-balance reader in Python and TypeScript. It retains original journals, recognizes appended reversal/replacement groups, checks full history against independent head evidence and returns a visible audit trace. You will calculate a two-axis balance, spot a half-correction error and read the limits of its evidence.
The example is synthetic. Cedar Teaching Bank, its accounts, instruction IDs and every timestamp below were created for teaching. USD amounts are integer cents; customer deposits are credit-normal liabilities of the bank. A raw debit-positive balance of −97350 therefore means a normal deposit liability of 97350, or $973.50. No provider data or named bank incident is represented.
Give each clock one job
The prerequisite Double-Entry Posting Engine explains balanced legs; Correcting and Reversing Entry Algorithm explains retaining originals and appending their inverse plus a replacement. Event-Sourced Balance Fold reconstructs a balance from complete committed history. Here we add two independent temporal cutoffs to that reconstruction.
effective_at tells us when a journal contributes economically. source_available_at records when the underlying evidence became available to the posting process. recorded_at retains the journal's trusted recording instant. A complete group's available_at is the observer's receipt after successful commit. A trusted head's observed_at bounds what the supplied export can establish.
The economic cutoff E filters effective times. The knowledge cutoff K filters whole-group observer receipts. Source availability is preserved as evidence but does not replace K. An old instruction can arrive late, and source timestamps need not increase across the journal sequence.
Temporal database research distinguishes valid time from transaction/database history time. We choose an observer availability axis explicitly. This is a portable bitemporal query profile, not an assertion that an observer's receipt equals a database's native system time. Jensen et al.'s temporal glossary provides the vocabulary; the schema and receipt choice are ours.
Why recording timestamps cannot split a correction
Suppose a correction's reversal records at 08:01:00.100 and its replacement at 08:01:00.300. Both belong to one committed transaction, and the observer receives the whole correction group at 08:02:00.000. A cutoff at 08:01:00.200 must not return the reversal alone. Even 08:01:59.999 still precedes the observer receipt.
PostgreSQL documents complete transaction visibility after completion. A consumer export must preserve that group boundary. It cannot reconstruct atomic visibility by pretending the two original recording fields were equal. The reader trusts the supplied post-commit group receipt; it does not prove a SQL commit or collect that receipt itself. PostgreSQL18 transaction documentation
Also, a timestamp's name does not establish its clock semantics. In PostgreSQL, current_timestamp refers to transaction start, while clock_timestamp samples wall-clock evaluation time. Neither automatically means the physical commit instant or when a consumer learned about the transaction. PostgreSQL18 date/time functions
Follow one corrected transfer
The independently calculated Cedar fixture contains five journals in four groups:
| Group | Economic effect | Observer receipt UTC | Ada's normal balance after receipt, E = 23 Oct |
|---|---|---|---|
| Funding | Dr cash 100000 /Cr Ada 100000 on 20 Oct 09:00 | 20 Oct 09:02 | 100000 |
| Erroneous transfer | Dr Ada 2500 /Cr Ben 2500 on 20 Oct 10:00 | 20 Oct 10:02 | 97500 |
| Atomic correction | Exact inverse 2500, then Dr Ada 2400 /Cr Cora 2400, effective 20 Oct 10:00 | 22 Oct 08:02 | 97600 |
| Fee | Dr Ada 250 /Cr fee income 250 on 23 Oct 10:00 | 23 Oct 10:02 | 97350 |
Before the correction receipt, Ada has 100000−2500=97500 and Ben has 2500. At receipt equality, both correction members become visible. Ada has 100000−2500+2500−2400=97600, Ben has 0 and Cora has 2400. The original remains in history; the new signed contributions explain the change.
Now hold K at 24 October noon and change E. At 20 October end, Ada is 97600 because the 23 October fee is economically excluded. At 23 October end, Ada is 97600−250=97350. Both queries validate and know the complete five-journal history; they differ in which economic contributions are selected. These are author-derived arithmetic results, not observations from an actual bank.
Read the grid along either axis. Moving K across 22 October 08:02 reveals the complete correction. Moving E across 23 October 10:00 includes the fee only if its group is already known. Open the full-size diagram.
Write the two-cutoff sum
For one account a, let d(j,a) be journal j's debits minus credits for that account. Let e(j) be its effective time and k(g) its containing group's observer receipt. After validating the complete export:
| Symbol | Meaning | Unit |
|---|---|---|
| E | Economic cutoff | UTC instant |
| K | Observer knowledge cutoff | UTC instant |
| d(j,a) | Debit-minus-credit contribution | Integer minor units |
| e(j),k(g) | Journal economic /group receipt boundaries | UTC instant |
| Braw | Account raw balance | Integer minor units in one currency |
For a debit-normal asset, normal balance equals raw balance. For a credit-normal liability, normal balance equals−raw balance. Do not add currencies together. There is no floating-point rounding or approximate equality.
Each contribution occupies [effective_at,+∞)×[group.available_at,+∞). Both lower boundaries are inclusive. This geometric view prevents an economic timestamp from being mistaken for knowledge. Choosing E after K still includes only accepted contributions known at K; it produces no forecast of future activity.
Require evidence before returning a number
The input query has explicit ledger, entity, account, currency, E and K. A trusted context supplies a source head independently of the submitted groups. That head names its stream, sequence, observation time and receipt reference. An export cannot establish its own completeness by claiming its last supplied sequence is the head.
This portable profile accepts at most 4096 complete groups per validated history; that is a resource bound of the example reader, not a ledger capacity claim. The reader validates every group from sequence 1 through the independent head before filtering time. Missing a future-unselected tail therefore rejects INCOMPLETE_EXPORT. Missing an interior group rejects a gap; input is never silently sorted. Exact duplicate delivery rejects DUPLICATE_GROUP, while changed content under one group ID rejects GROUP_ID_CONFLICT. Rejecting both avoids silently making a consumer's delivery assumptions part of accounting arithmetic.
Strict validation also checks exact object fields, genuine Gregorian UTC millisecond timestamps, canonical monetary strings, balanced same-scope journals, retained account signs and currency. A correction has exactly two members, ordered reversal then replacement. Its link must reference an earlier original, and the reversal must preserve every original account, amount and position while flipping only debit/credit. One full reversal per original is supported; a replacement can itself be corrected later.
Current posting policy is a separate question. Closing an account today does not erase its historical journals. This reader validates retained account type, normal side, scope and currency; it does not rerun today's active-account check or current open-period list. Retained adjustment evidence is checked when booking and economic months differ. No legal or statutory restatement policy is inferred.
Implement one exact read model
The complete Python reference exposes query_balance; the TypeScript reference exposes queryBalance. Each is self-contained. Python int and TypeScript BigInt handle exact internal arithmetic; every monetary input and output is a decimal string so JSON transport never loses an integer above 2^53.
from bitemporal import query_balance
result = query_balance(query, groups, accounts, context)
if result['status'] == 'ok':
print(result['balance']['normal_balance_minor'])
else:
print(result['code'])
The complete data contract defines every field and rejection code; the shared fixture provides runnable inputs. This bitemporal reader deliberately requires checkpoint:null. Use Event-Sourced Balance Fold to study registered, independently checked checkpoint prefixes rather than accepting an unexplained opening scalar here.
Success returns exact balance, trusted coverage metadata, visible group trace and fixed-E knowledge intervals. A rejection has no balance or partial trace. The trace never contains future-unavailable group names or amounts. Source-head metadata remains evidence provenance, so do not treat a later observation receipt as a feature supposedly available at historical K.
The interval output begins with zero genesis and records each visible receipt. Tied receipts coalesce into one observable balance version. Intervals are half-open, and the last end is null: this means unsuperseded within the selected known prefix, not that no later correction exists. Even a receipt producing no account change remains an evidence version. A scalar balance alone would hide that distinction.
Try the two-cutoff lab
Open the interactive lab. Its ten guided observations move through funding, wrong transfer, the two correction recording instants, the receipt boundary, later knowledge and a changed economic cutoff. Choose the missing-tail comparison to see why an apparently unaffected historical number is withheld when evidence is incomplete. The tied-receipt scenario shows why receipt equality selects all groups at that boundary.
The lab uses the compiled TypeScript reference and complete source fixture rather than a separate simplified balance formula. Its table, normal balance, selected journal identities and interval count recompute together. Step, Back, Reset and reduced-motion Play preserve the same deterministic observation sequence.
Verify the boundaries, then read the limits
The local validation compares complete Python/TypeScript outputs over 91 independently specified cases. Tests cover equality, zero history, closed historical accounts, old source evidence, missing future groups, exact and changed duplicates, wrong inverses, repeated reversals, later correction of replacements, Unicode reason handling and exact integers exceeding 2^53. They also check input immutability, detached output arrays and absence of unavailable identities. Validation record and reproduction commands
The historical-example decision is not useful here. A named incident would need licensed full original and corrected instructions, consumer receipts, atomic group identities and independent source coverage to reproduce this question. Invented event dates attached to a real institution would add narrative without evidence. Cedar lets us test the clock boundary explicitly.
This portable model does not authenticate its host inputs, produce database persistence, prove commit timestamps or establish cryptographic integrity. It is suitable for explaining and testing reproducible query semantics once a trustworthy exporter supplies the agreed evidence. Native system-versioned tables require a separate adapter and clock contract. For example, MariaDB's bitemporal tables combine application-time and system-versioned periods; that is a related storage design, not a result demonstrated by this reader.
Continue with Event-Sourced Balance Fold for checkpoint reconstruction, or revisit Correcting and Reversing Entry Algorithm for correction writes. The next integrity tutorial, Hash-Chained Journal, asks how to detect altered history. It adds a different evidence mechanism; it does not turn an observer receipt into an exact physical commit timestamp.
The primary reference notes identify source roles and versions. The scene script carries the same synthetic inputs and exact balances into a future video; no rendered or published video is claimed.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
Accessed: 25 October 2026. Supports: the temporal concepts and platform semantics identified for each source below. Limitations: no jurisdictional accounting/legal rule is established by these temporal database sources; our chosen receipt model and schemas remain author-defined. No customer, market or provider observations are used. Paraphrases only; all financial examples are author-created.
| Primary source | Version, section and source role | Applicability boundary |
|---|---|---|
| Christian S. Jensen et al., A Consensus Glossary of Temporal Database Concepts | ACM SIGMOD Record 23(1), March1994, pp 52–64, DOI10.1145/181550.181560; §§ 3.1–3.2/3.8 valid, transaction and bitemporal vocabulary; author-hosted paper | Sourced conceptual terminology. Our observer receipt axis is a stated implementation choice, not an assertion of native transaction-time equivalence. PDF successfully inspected during initial Stage 2 research; later retry timed out. |
| Jensen and Snodgrass, Temporal Data Management | IEEE TKDE 11(1), Jan/Feb1999, pp 36–44, temporal dimensions discussion | Primary historical research; no1999 vendor/platform status presented as current. |
| PostgreSQL, Transactions | Version18, § 3.4 | Sourced atomic visibility basis. Portable input grouping does not prove those SQL properties or deployment durability. |
| PostgreSQL, Date/Time Functions | Version18, § 9.9.5 | current_timestamp is transaction-start time; clock_timestamp is evaluation wall-clock. Neither is automatically the commit instant or consumer receipt. |
| PostgreSQL, System Information Functions | Version18, § 9.27.9 commit timestamp facilities | Optional track_commit_timestamp data is not the same as this supplied observer receipt; retention/configuration apply. Native commit-time query implementation excluded. |
| MariaDB, Bitemporal Tables | Official rolling documentation as accessed; application-time and system-versioned combination | Comparison only. No MariaDB execution/version-compatibility result claimed. |
Implementation choices: strict schema, USD/EUR scope, canonical strings, observer receipt selection, fixed-E interval output, full-head coverage, immutable retained journal modeling, correction inverse checks and no checkpoint consumption. These are defined in the README and tested, not attributed to the papers. Author-derived arithmetic:100000−2500+2500−2400=97600; later−250=97350. Synthetic inputs: Cedar Teaching Bank/CB-A, account names, IDs, dates and receipts. Historical-example decision: not useful; no named bank case is implied.
Full dependency-light reference implementations in both supported languages.
/** Strict portable bitemporal read model. Trusted receipts are inputs, not commit proofs. */
type Obj = Record<string, any>;
const fields = (s: string) => s.split(' ');
const QUERY = fields('schema_version ledger_id entity_id account_id currency effective_cutoff knowledge_cutoff checkpoint');
const HEAD = fields('stream_id ledger_id entity_id observed_at head_sequence receipt_ref');
const GROUP = fields('schema_version group_id kind ledger_id entity_id available_at journals correction');
const JOURNAL = fields('command sequence recorded_at adjustment_reason');
const COMMAND = fields('schema_version ledger_id entity_id journal_id business_operation_id currency effective_at source_available_at booking_period legs');
const ACCOUNT = fields('account_id ledger_id entity_id currency account_type normal_side status');
const LINK = fields('correction_id original_journal_id reversal_journal_id replacement_journal_id reason');
const NORMAL: Obj = {asset:'debit', expense:'debit', liability:'credit', equity:'credit', income:'credit'};
const exact = (x: any, f: string[]) => x !== null && typeof x === 'object' && !Array.isArray(x) && Object.keys(x).length === f.length && f.every(k => Object.hasOwn(x,k));
const match = (x: any, r: RegExp) => typeof x === 'string' && r.exec(x)?.[0] === x;
const ident = (x: any) => match(x,/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/);
const pos = (x: any) => match(x,/^[1-9][0-9]{0,29}$/);
const nonneg = (x: any) => match(x,/^(0|[1-9][0-9]{0,29})$/);
// Python str.strip whitespace: deliberately excludes U+FEFF and includes U+0085/U+001C..1F.
const stripReason = (x: string) => x.replace(/^[\u0009-\u000D\u001C-\u0020\u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+|[\u0009-\u000D\u001C-\u0020\u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+$/gu,'');
const reasonOK = (x: any) => typeof x === 'string' && x === stripReason(x) && [...x].length >= 1 && [...x].length <= 240 && ![...x].some(c => c.codePointAt(0)! >= 0xD800 && c.codePointAt(0)! <= 0xDFFF);
function timestamp(x: any): boolean {
if (!match(x,/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/)) return false;
const y=Number(x.slice(0,4)),m=Number(x.slice(5,7)),d=Number(x.slice(8,10));
const days=[31, y%4===0 && (y%100!==0 || y%400===0)?29:28,31,30,31,30,31,31,30,31,30,31];
return y>=1 && m>=1 && m<=12 && d>=1 && d<=days[m-1]! && Number(x.slice(11,13))<24 && Number(x.slice(14,16))<60 && Number(x.slice(17,19))<60;
}
function signature(x: any): string {
if (Array.isArray(x)) return '['+x.map(signature).join(',')+']';
if (x!==null && typeof x==='object') return '{'+Object.keys(x).sort().map(k=>JSON.stringify(k)+':'+signature(x[k])).join(',')+'}';
return JSON.stringify(x);
}
const reject=(code: string): Obj=>({status:'rejected',code,balance:null,coverage:null,trace:[],versions:[]});
function accountMap(accounts: any): Map<string,Obj>|null {
if (!Array.isArray(accounts)) return null;
const map=new Map<string,Obj>();
for (const a of accounts) {
if (!exact(a,ACCOUNT) || !['account_id','ledger_id','entity_id'].every(k=>ident(a[k])) || map.has(a.account_id) || !['USD','EUR'].includes(a.currency) || typeof a.account_type!=='string' || !Object.hasOwn(NORMAL,a.account_type) || a.normal_side!==NORMAL[a.account_type] || !['active','closed'].includes(a.status)) return null;
map.set(a.account_id,a);
}
return map;
}
function validateJournal(j: any,lookup: Map<string,Obj>,ledger: string,entity: string): string|null {
if (!exact(j,JOURNAL) || !exact(j.command,COMMAND)) return 'INVALID_SCHEMA';
const c=j.command;
if (c.schema_version!=='d30-journal-v1') return 'UNSUPPORTED_SCHEMA';
if (!['ledger_id','entity_id','journal_id','business_operation_id'].every(k=>ident(c[k]))) return 'INVALID_IDENTIFIER';
if (c.ledger_id!==ledger || c.entity_id!==entity) return 'SCOPE_MISMATCH';
if (!['USD','EUR'].includes(c.currency)) return 'UNSUPPORTED_CURRENCY';
if (!pos(j.sequence)) return 'INVALID_SEQUENCE';
if (![c.effective_at,c.source_available_at,j.recorded_at].every(timestamp)) return 'INVALID_TIMESTAMP';
if (c.effective_at>j.recorded_at || c.source_available_at>j.recorded_at) return 'CLOCK_ORDER_VIOLATION';
if (!match(c.booking_period,/^[0-9]{4}-(0[1-9]|1[0-2])$/) || c.booking_period.slice(0,4)==='0000') return 'INVALID_SCHEMA';
const r=j.adjustment_reason;
if (typeof r!=='string') return 'INVALID_SCHEMA';
if (c.booking_period!==c.effective_at.slice(0,7) ? stripReason(r).length===0 : r!=='') return 'INVALID_ADJUSTMENT_EVIDENCE';
if (!Array.isArray(c.legs) || c.legs.length<2 || c.legs.length>32) return 'INVALID_LEGS';
let debit=0n,credit=0n; const seen=new Set<string>();
for (const leg of c.legs) {
if (!exact(leg,fields('account_id side amount_minor'))) return 'INVALID_SCHEMA';
if (!ident(leg.account_id)) return 'INVALID_IDENTIFIER';
if (!['debit','credit'].includes(leg.side)) return 'INVALID_SIDE';
if (!pos(leg.amount_minor)) return 'INVALID_AMOUNT';
const a=lookup.get(leg.account_id);
if (!a) return 'UNKNOWN_ACCOUNT';
if (a.ledger_id!==ledger || a.entity_id!==entity) return 'SCOPE_MISMATCH';
if (a.currency!==c.currency) return 'CURRENCY_MISMATCH';
seen.add(leg.account_id);
if (leg.side==='debit') debit+=BigInt(leg.amount_minor); else credit+=BigInt(leg.amount_minor);
}
if (seen.size<2) return 'DISTINCT_ACCOUNTS_REQUIRED';
return debit===credit?null:'UNBALANCED_JOURNAL';
}
/** Pure genesis-only query. Validate full head coverage before selecting E/K. */
export function queryBalance(query: any,groups: any,accounts: any,context: any): Obj {
if (!exact(query,QUERY)) return reject('INVALID_SCHEMA');
if (query.schema_version!=='d30-balance-fold-v1') return reject('UNSUPPORTED_SCHEMA');
if (!['ledger_id','entity_id','account_id'].every(k=>ident(query[k]))) return reject('INVALID_IDENTIFIER');
if (!['USD','EUR'].includes(query.currency)) return reject('UNSUPPORTED_CURRENCY');
if (!timestamp(query.effective_cutoff) || !timestamp(query.knowledge_cutoff)) return reject('INVALID_TIMESTAMP');
if (query.checkpoint!==null) return reject('CHECKPOINT_NOT_SUPPORTED');
const lookup=accountMap(accounts);
if (!lookup) return reject('INVALID_ACCOUNTS');
if (!exact(context,fields('head checkpoint_registry')) || !exact(context.head,HEAD) || !Array.isArray(context.checkpoint_registry) || context.checkpoint_registry.length!==0) return reject('INVALID_CONTEXT');
const h=context.head;
if (!['stream_id','ledger_id','entity_id','receipt_ref'].every(k=>ident(h[k])) || !timestamp(h.observed_at) || !nonneg(h.head_sequence)) return reject('INVALID_CONTEXT');
if (query.ledger_id!==h.ledger_id || query.entity_id!==h.entity_id) return reject('SCOPE_MISMATCH');
const account=lookup.get(query.account_id);
if (!account) return reject('UNKNOWN_ACCOUNT');
if (account.ledger_id!==h.ledger_id || account.entity_id!==h.entity_id) return reject('SCOPE_MISMATCH');
if (account.currency!==query.currency) return reject('CURRENCY_MISMATCH');
if (query.knowledge_cutoff>h.observed_at) return reject('KNOWLEDGE_BEYOND_EXPORT');
if (!Array.isArray(groups) || groups.length>4096) return reject('INVALID_GROUP');
const groupIds=new Map<string,string>(), journalIds=new Map<string,Obj>(), businessIds=new Set<string>(), correctionIds=new Set<string>(), reversedIds=new Set<string>(), inverseIds=new Set<string>();
let expected=1n, previousRecorded:string|null=null,previousAvailable:string|null=null;
for (const g of groups) {
if (!exact(g,GROUP)) return reject('INVALID_GROUP');
if (g.schema_version!=='d30-committed-group-v1') return reject('UNSUPPORTED_SCHEMA');
if (!['group_id','ledger_id','entity_id'].every(k=>ident(g[k]))) return reject('INVALID_IDENTIFIER');
if (groupIds.has(g.group_id)) return reject(signature(g)===groupIds.get(g.group_id)?'DUPLICATE_GROUP':'GROUP_ID_CONFLICT');
groupIds.set(g.group_id,signature(g));
if (g.ledger_id!==h.ledger_id || g.entity_id!==h.entity_id) return reject('SCOPE_MISMATCH');
if (!['posting','correction'].includes(g.kind) || !Array.isArray(g.journals) || g.journals.length!==(g.kind==='posting'?1:2)) return reject('INVALID_GROUP');
if (!timestamp(g.available_at)) return reject('INVALID_TIMESTAMP');
if (g.available_at>h.observed_at || (previousAvailable!==null && g.available_at<previousAvailable)) return reject('CLOCK_ORDER_VIOLATION');
previousAvailable=g.available_at; const localIds=new Set<string>(),localBusiness=new Set<string>();
for (const j of g.journals) {
const code=validateJournal(j,lookup,h.ledger_id,h.entity_id);if(code) return reject(code);
const n=BigInt(j.sequence),c=j.command;
if (n<expected) return reject('OUT_OF_ORDER_GROUP');
if (n>expected) return reject('SEQUENCE_GAP');
if (n>BigInt(h.head_sequence)) return reject('INCOMPLETE_EXPORT');
expected++;
if (j.recorded_at>g.available_at || (previousRecorded!==null && j.recorded_at<previousRecorded)) return reject('CLOCK_ORDER_VIOLATION');
previousRecorded=j.recorded_at;
if (journalIds.has(c.journal_id) || localIds.has(c.journal_id)) return reject('DUPLICATE_JOURNAL');
if (businessIds.has(c.business_operation_id) || localBusiness.has(c.business_operation_id)) return reject('DUPLICATE_BUSINESS_OPERATION');
localIds.add(c.journal_id);localBusiness.add(c.business_operation_id);
}
const link=g.correction;
if (g.kind==='posting') { if (link!==null) return reject('INVALID_CORRECTION_LINK'); }
else {
if (!exact(link,LINK) || !LINK.filter(k=>k!=='reason').every(k=>ident(link[k])) || !reasonOK(link.reason)) return reject('INVALID_CORRECTION_LINK');
const rev=g.journals[0].command,repl=g.journals[1].command;
if (correctionIds.has(link.correction_id) || link.reversal_journal_id!==rev.journal_id || link.replacement_journal_id!==repl.journal_id || ['ledger_id','entity_id','currency','effective_at','source_available_at','booking_period'].some(k=>rev[k]!==repl[k])) return reject('INVALID_CORRECTION_LINK');
const original=journalIds.get(link.original_journal_id);
if (!original) return reject('INVALID_CORRECTION_LINK');
if (reversedIds.has(link.original_journal_id)) return reject('ALREADY_REVERSED');
if (inverseIds.has(link.original_journal_id)) return reject('REVERSAL_OF_REVERSAL');
const inverse=original.legs.map((leg:Obj)=>({...leg,side:leg.side==='debit'?'credit':'debit'}));
if (original.currency!==rev.currency) return reject('CURRENCY_MISMATCH');
if (signature(rev.legs)!==signature(inverse)) return reject('INVALID_REVERSAL');
correctionIds.add(link.correction_id);reversedIds.add(link.original_journal_id);inverseIds.add(rev.journal_id);
}
for (const j of g.journals) {journalIds.set(j.command.journal_id,j.command);businessIds.add(j.command.business_operation_id);}
}
if (expected-1n!==BigInt(h.head_sequence)) return reject('INCOMPLETE_EXPORT');
let raw=0n,visible='0'; const trace:Obj[]=[],versions:Obj[]=[{knowledge_from:null,knowledge_to:null,raw_balance_minor:'0',normal_balance_minor:'0'}];
for (const g of groups) {
if (g.available_at>query.knowledge_cutoff) break;
let delta=0n; const selected:string[]=[];
for (const j of g.journals) {
if (j.command.effective_at>query.effective_cutoff) continue;
selected.push(j.command.journal_id);
for (const leg of j.command.legs) if (leg.account_id===query.account_id) delta+=BigInt(leg.amount_minor)*(leg.side==='debit'?1n:-1n);
}
raw+=delta;visible=g.journals[g.journals.length-1].sequence;
const normal=account.normal_side==='debit'?raw:-raw;
trace.push({group_id:g.group_id,first_sequence:g.journals[0].sequence,last_sequence:visible,available_at:g.available_at,selected_journal_ids:selected,raw_delta_minor:String(delta),raw_balance_minor:String(raw),normal_balance_minor:String(normal)});
const version={knowledge_from:g.available_at,knowledge_to:null,raw_balance_minor:String(raw),normal_balance_minor:String(normal)};
if (versions[versions.length-1]!.knowledge_from===g.available_at) versions[versions.length-1]=version;
else {versions[versions.length-1]!.knowledge_to=g.available_at;versions.push(version);}
}
const normal=account.normal_side==='debit'?raw:-raw;
return {status:'ok',code:'BITEMPORAL_VIEW',balance:{account_id:query.account_id,currency:query.currency,normal_side:account.normal_side,raw_balance_minor:String(raw),normal_balance_minor:String(normal)},coverage:{stream_id:h.stream_id,receipt_ref:h.receipt_ref,head_sequence:h.head_sequence,validated_through_sequence:h.head_sequence,visible_through_sequence:visible,observed_at:h.observed_at},trace,versions};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.
