Ledgers that reconcile
Finance systems are tested on data that cannot be shown to the testers, or on random numbers that never balance. Neither exercises the code that matters: the close, the reconciliation, the report that ties one ledger to another. Here the books balance by construction and the balances you care about are declared.
Who: finance engineering, audit analytics, ERP and fintech teams. Time: under 2 seconds for 44,440 rows.
The declaration#
YAML
# misata.yaml
name: general_ledger
seed: 8
tables:
accounts:
rows: 40
columns:
account_id: {type: int, unique: true, min: 1000, max: 1039}
account_type: {type: categorical, choices: [asset, liability, equity, revenue, expense], probabilities: [0.3, 0.2, 0.05, 0.15, 0.3]}
journal_entries:
rows: 8000
columns:
entry_id: {type: int, unique: true, min: 1, max: 8000}
source: {type: categorical, choices: [billing, payroll, payables, manual], probabilities: [0.45, 0.15, 0.3, 0.1]}
posted_on: {type: date, start: "2024-01-01", end: "2025-12-31"}
journal_lines:
rows: 36000
columns:
line_id: {type: int, unique: true, min: 1, max: 36000}
entry_id: {type: foreign_key, sampling: uniform}
account_id: {type: foreign_key}
debit: {type: float, distribution: lognormal, mean: 2400, std: 5200, min: 0.01, max: 400000, decimals: 2}
credit: {type: float, distribution: lognormal, mean: 2400, std: 5200, min: 0.01, max: 400000, decimals: 2}
constraints:
- name: double_entry
type: balanced_ledger
group_by: [entry_id]
debit_column: debit
credit_column: credit
receivables_rollforward:
rows: 400
columns:
period: {type: categorical, choices: ["2024-Q1", "2024-Q2", "2024-Q3", "2024-Q4", "2025-Q1", "2025-Q2", "2025-Q3", "2025-Q4"]}
movement_type: {type: categorical, choices: [invoiced, collected, written_off]}
amount: {type: float, min: 1, max: 250000, decimals: 2}
relationships:
- {parent: journal_entries.entry_id, child: journal_lines.entry_id, min_children: 2}
- accounts.account_id -> journal_lines.account_id
waterfalls:
- table: receivables_rollforward
period_column: period
type_column: movement_type
amount_column: amount
starting_value: 1250000
inflow_shares: {invoiced: 1.0}
outflow_shares: {collected: 0.96, written_off: 0.04}
outflow_rate: 0.55
points:
- {period: "2024-Q1", ending_value: 1318400.00}
- {period: "2024-Q2", ending_value: 1402750.50}
- {period: "2024-Q3", ending_value: 1377210.25}
- {period: "2024-Q4", ending_value: 1461980.75}
- {period: "2025-Q1", ending_value: 1512006.10}
- {period: "2025-Q2", ending_value: 1590044.80}
- {period: "2025-Q3", ending_value: 1566310.35}
- {period: "2025-Q4", ending_value: 1648902.40}balanced_ledgermakes every line one-sided (a debit or a credit) and every journal entry's debits equal its credits to the cent.min_children: 2gives every entry at least two lines, because a one-line entry cannot balance.- The
waterfallsdeclaration is an accounts receivable roll-forward: opening balance, plus invoices, minus collections and write-offs, lands on the declared closing balance every quarter for two years, with collections 96% and write-offs 4% of what leaves.
Show the numbers
| Quarter | Closing balance |
|---|---|
| 2024-Q1 | 1,318,400.00 |
| 2024-Q2 | 1,402,750.50 |
| 2024-Q3 | 1,377,210.25 |
| 2024-Q4 | 1,461,980.75 |
| 2025-Q1 | 1,512,006.10 |
| 2025-Q2 | 1,590,044.80 |
| 2025-Q3 | 1,566,310.35 |
| 2025-Q4 | 1,648,902.40 |
Source: waterfalls in the misata.yaml above, opening at 1,250,000.00; every quarter closes exactly
Run it#
Shell
misata explain misata.yaml
misata generate -c misata.yaml -o books --attestThe check#
Shell
python books/verify.pyText
ok 4 file(s) match their attested sha256
ok 52 of 52 invariant(s) hold| Invariant | Count | What it proves |
|---|---|---|
ledger_entry_balance | 1 | no journal entry where debits differ from credits |
ledger_trial_balance | 1 | total debits equal total credits: 111,369,276.72 each |
min_children | 1 | every entry has at least two lines (here 2 to 15, median 4) |
waterfall_balance | 8 | each quarter closes on its declared balance |
waterfall_net | 8 | each quarter's movements net to the change in balance |
waterfall_component | 24 | invoiced, collected and written off, per quarter |
| keys, domains, uniques | 9 | every line points at a real entry and account |
What to test with it#
- The close. Every entry balances, so every slice of entries does: the trial balance is zero for 2024 (4,020 entries), for 2025 (3,980), for any month and any source. A report that does not tie out has a bug in the report.
- Reconciliations. The receivables roll-forward opens at 1,250,000.00 and closes 2025-Q4 at 1,648,902.40. A reconciliation job that computes a different closing balance from the movements is wrong, and you know the right answer before running it.
- Known-answer questions.
misata evalpack -c misata.yamlturns the same declarations into SQL questions with answers, for testing an agent or a BI tool on accounting data.
Related#
- Declaration reference:
waterfalls,balanced_ledgerand every other declaration - Attest a dataset: the ledger, waterfall and
min_childreninvariants - Certificate format: what each of the 52 checks records
- Build an evalpack: turn the same books into SQL questions with answers
Files#
The recipe folder runs as it is. Its files on GitHub:
Prefer generating data visually?
Misata Studio lets you turn natural language prompts into relational schemas with exact outcome invariants, live preview tables, and one-click database seeding without writing code.

