docs(core): written specs, with sources, for the commands and reports

Max-testing pass, step 2: every domain decision in ledger.rs and
reports.rs gets a GOAL / SOURCE / INVARIANTS / SCENARIOS block in its
doc comment: post_entry, try_create_entry, close_period_to,
mark_cleared, reconciliation_report, account_balance, create_invoice,
create_bill, receive_payment, pay_bill, register_asset, dispose_asset,
run_payroll, remit_payroll_taxes, set_line_class, set_entry_vendor;
trial_balance(_as_of), profit_and_loss_for_period, balance_sheet_as_of,
general_ledger, members_equity, cash_flow, profit_and_loss_by_class,
snapshot. Sources trace to docs/spec/rules.md (and through it to
Pacioli, the IRS, FASB); what no document settles is marked CONVENTION,
DECISION, or ASSUMED for Stephen to confirm. The specs are the input to
spec-blind test writing (the next step): tests derived from the spec
alone, never from the code.

Also: rustdoc now runs in `just check` with -D warnings; fixed the seven
existing broken intra-doc links and one bare URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-13 14:21:44 -06:00
1 parent 925c3ba523
commit 0d4a5f8f53
7 files changed
+360 -7

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@
//! separate set of books with its own rules and stays with the CPA. This
//! module never pretends to do tax.
//!
//! The register is master data owned by [`Ledger`]: assets are journaled
//! The register is master data owned by [`Ledger`](crate::Ledger): assets are journaled
//! like accounts (see `ledger-store`), and ids are allocator-deterministic
//! for replay. Depreciation itself is pure math — [`depreciation_schedule`]
//! — and postings go through the normal entry machinery, so every
+212 -1
View File
@@ -26,7 +26,8 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// An account the engine posts to by code: invoices, bills, payroll, and
/// closes find (or create) these. See [`Ledger::check_controls`].
/// closes find (or create) these; a command refuses one of the wrong type
/// (`LedgerError::ControlAccountMismatch`).
#[derive(Clone, Copy, Debug)]
pub struct ControlAccount {
/// The code the engine looks for.
@@ -615,6 +616,17 @@ impl Ledger {
/// Start a draft journal entry, rejecting dates that fall in a closed
/// period. This is the entry point for new work; [`Self::create_entry`]
/// is for replay and internal use.
///
/// ```text
/// GOAL: start a draft entry for new work.
/// SOURCE: docs/spec/rules.md B-12; Lean invariant Inv.draftsOpen.
/// SCENARIOS:
/// 1. IF the date falls in a closed period:
/// - SHOULD refuse (ClosedPeriod)
/// - NEVER create a draft there
/// BECAUSE a closed period takes no new entries.
/// 2. Otherwise: SHOULD return a fresh id, one past the last.
/// ```
pub fn try_create_entry(
&mut self,
date: NaiveDate,
@@ -771,6 +783,26 @@ impl Ledger {
/// Validate and post a draft entry. One-way: a posted entry is immutable.
///
/// Rejects entries dated in a closed period.
///
/// ```text
/// GOAL: make a draft part of the books, once and for good.
/// SOURCE: docs/spec/rules.md B-1, B-2, B-3, B-8 (Pacioli ch. 36: write
/// only forward), B-12.
/// INVARIANTS:
/// 1. MUST post only an entry that passes JournalEntry::validate.
/// 2. MUST append it to the log; NEVER reorder, change, or remove an
/// earlier posted entry.
/// 3. A refusal MUST leave the books unchanged.
/// SCENARIOS:
/// 1. IF the entry is unknown: SHOULD refuse (UnknownEntry).
/// 2. IF it is already posted:
/// - SHOULD refuse (EntryAlreadyPosted)
/// - NEVER post it twice
/// 3. IF it is dated in a closed period: SHOULD refuse (ClosedPeriod).
/// 4. IF it doesn't balance, has fewer than two lines, a zero, negative,
/// or out-of-range amount, or an unknown or placeholder account:
/// SHOULD refuse with that reason.
/// ```
pub fn post_entry(&mut self, entry: EntryId) -> Result<(), LedgerError> {
let snapshot = self
.entries
@@ -944,6 +976,15 @@ impl Ledger {
/// salvage below cost, at least one month of life, base currency) and
/// the account links (asset/asset/expense types, three distinct
/// accounts).
///
/// ```text
/// GOAL: put a fixed asset on the register, to depreciate monthly.
/// SOURCE: docs/spec/rules.md B-15; B-22 (capitalize vs expense).
/// SCENARIOS:
/// 1. IF the description is blank, cost isn't positive, salvage is
/// negative or not below cost, the useful life is zero months, or the
/// accounts aren't the right types: SHOULD refuse.
/// ```
pub fn register_asset(&mut self, new: NewAsset) -> Result<AssetId, LedgerError> {
self.validate_new_asset(&new)?;
let id = self.next_asset_id;
@@ -981,6 +1022,18 @@ impl Ledger {
/// Mark an asset disposed. The disposal *entry* is posted through the
/// normal machinery by the caller; this records the lifecycle change.
///
/// ```text
/// GOAL: take an asset off the books when sold or scrapped.
/// SOURCE: docs/spec/rules.md B-15; CONVENTION (gain or loss on disposal).
/// INVARIANTS:
/// 1. The gain (or loss) MUST equal proceeds minus book value at the
/// disposal date.
/// 2. The asset's cost and accumulated depreciation MUST both leave the
/// books.
/// SCENARIOS:
/// 1. IF the asset is unknown or already disposed: SHOULD refuse.
/// ```
pub fn dispose_asset(
&mut self,
id: AssetId,
@@ -1121,6 +1174,32 @@ impl Ledger {
///
/// Returns the closing entry's id, or `None` if there was nothing to
/// close.
///
/// ```text
/// GOAL: close a period: zero every revenue and expense account's balance
/// as of its last day into equity, and lock it.
/// SOURCE: docs/spec/rules.md B-11 (Pacioli ch. 27, 34), B-12, B-13 (26
/// U.S.C. §704(a)); R-? Periods and close.
/// INVARIANTS:
/// 1. After a close, every revenue and expense account MUST have a zero
/// balance as of the period's last day.
/// 2. The closing entry MUST balance, be dated the period's last day, and
/// credit equity with exactly the net income through that day (split
/// among members by share, to the cent, by largest remainder).
/// 3. Entries dated after the period MUST stay untouched (their income
/// belongs to their own period).
/// 4. A sweep account (draws, distributions, dividends) MUST end at zero,
/// its balance moved into its target.
/// 5. NEVER post a closing entry when there is nothing to close.
/// SCENARIOS:
/// 1. IF the period is unknown or already closed: SHOULD refuse.
/// 2. IF a draft is dated inside it: SHOULD refuse (DraftsInPeriod).
/// 3. IF a later period is already closed: SHOULD refuse (CloseOutOfOrder).
/// 4. IF a target or sweep account isn't a distinct equity account, or
/// member shares aren't positive and summing to 1: SHOULD refuse.
/// 5. IF closing to retained earnings and 3999 exists but isn't a
/// postable equity account: SHOULD refuse (ControlAccountMismatch).
/// ```
pub fn close_period_to(
&mut self,
id: PeriodId,
@@ -1452,6 +1531,15 @@ impl Ledger {
/// Tag an entry (draft or posted) with the vendor it paid or was billed
/// by, or clear the tag. The tag is reporting metadata, like a receipt
/// link: it changes no amounts, so posted entries may be tagged.
///
/// ```text
/// GOAL: tag an entry with the vendor it paid, for 1099s.
/// SOURCE: docs/spec/rules.md B-19.
/// SCENARIOS:
/// 1. IF the entry or vendor is unknown: SHOULD refuse.
/// 2. Tagging MUST change no amount (it's metadata, allowed on posted
/// entries).
/// ```
pub fn set_entry_vendor(
&mut self,
entry: EntryId,
@@ -1559,6 +1647,22 @@ impl Ledger {
/// Create a sales invoice. Auto-posts: Dr `1200` Accounts Receivable,
/// Cr `4000` Revenue. The invoice date must not fall in a closed period.
///
/// ```text
/// GOAL: bill a customer and recognize the revenue (accrual basis).
/// SOURCE: IRS Pub. 538, accrual method (docs/spec/sources/irs-periods-and-methods.md:
/// "report income in the year it is earned"); ASC 606-10-25-23;
/// control accounts: Ledger::check_controls.
/// INVARIANTS:
/// 1. MUST post one balanced entry: Dr 1200 receivables, Cr 4000 revenue,
/// for the sum of the lines, dated the invoice date.
/// 2. The invoice MUST start open, with nothing paid.
/// 3. A refusal MUST leave the books unchanged.
/// SCENARIOS:
/// 1. IF the customer is unknown, a line amount isn't positive, the due
/// date is before the invoice date, or the date is in a closed
/// period: SHOULD refuse.
/// ```
pub fn create_invoice(
&mut self,
customer_id: CustomerId,
@@ -1604,6 +1708,21 @@ impl Ledger {
/// Create a vendor bill. Auto-posts: Dr `6999` General Expenses,
/// Cr `2000` Accounts Payable. The bill date must not fall in a closed
/// period.
///
/// ```text
/// GOAL: record a vendor's bill as an expense owed (accrual basis).
/// SOURCE: IRS Pub. 538, accrual method ("deduct ... expenses in the year
/// incurred"); control accounts: Ledger::check_controls.
/// INVARIANTS:
/// 1. MUST post one balanced entry: Dr 6999 general expenses, Cr 2000
/// payables, for the sum of the lines, dated the bill date, tagged
/// with the vendor (1099s).
/// 2. A refusal MUST leave the books unchanged.
/// SCENARIOS:
/// 1. IF the vendor is unknown, a line amount isn't positive, the due date
/// is before the bill date, or the date is in a closed period: SHOULD
/// refuse.
/// ```
pub fn create_bill(
&mut self,
vendor_id: VendorId,
@@ -1659,6 +1778,20 @@ impl Ledger {
/// Record a customer payment against an invoice. Posts: Dr Cash,
/// Cr A/R. Updates the invoice's paid amount and status.
///
/// ```text
/// GOAL: record a customer's payment against an invoice.
/// SOURCE: IRS Pub. 538 (accrual); CONVENTION (A/R workflow).
/// INVARIANTS:
/// 1. MUST post Dr cash, Cr 1200 receivables, for the amount.
/// 2. The invoice's paid amount MUST rise by the amount; it is Paid
/// exactly when fully paid, Partial when partly.
/// SCENARIOS:
/// 1. IF the amount isn't positive: SHOULD refuse.
/// 2. IF it exceeds what's still owed:
/// - SHOULD refuse (Overpayment)
/// - NEVER let paid exceed the total
/// ```
pub fn receive_payment(
&mut self,
invoice_id: InvoiceId,
@@ -1696,6 +1829,18 @@ impl Ledger {
/// Pay a vendor bill. Posts: Dr A/P, Cr Cash. Updates the bill's paid
/// amount and status.
///
/// ```text
/// GOAL: record paying a vendor's bill.
/// SOURCE: IRS Pub. 538 (accrual); CONVENTION (A/P workflow).
/// INVARIANTS:
/// 1. MUST post Dr 2000 payables, Cr cash, for the amount, tagged with
/// the vendor.
/// 2. The bill's paid amount MUST rise by the amount.
/// SCENARIOS:
/// 1. IF the amount isn't positive, or exceeds what's still owed: SHOULD
/// refuse.
/// ```
pub fn pay_bill(
&mut self,
bill_id: BillId,
@@ -1917,6 +2062,14 @@ impl Ledger {
/// Mark a posted entry's line as cleared (or uncleared). The cleared
/// flag is a workflow marker, not accounting data: posted entries stay
/// immutable, and the flag lives in a side table.
///
/// ```text
/// GOAL: mark a posted line as cleared (seen on the bank statement), or not.
/// SOURCE: CONVENTION (bank reconciliation); R-? Bank reconciliation.
/// SCENARIOS:
/// 1. IF the entry is unknown, a draft, or has no such line: SHOULD refuse.
/// 2. Marking MUST change no amount, no report but the reconciliation.
/// ```
pub fn mark_cleared(
&mut self,
entry_id: EntryId,
@@ -1953,6 +2106,19 @@ impl Ledger {
/// cleared balance (posted lines marked cleared, dated on or before
/// the statement date) to the statement balance, and lists every
/// uncleared line so the human can see what's outstanding.
///
/// ```text
/// GOAL: compare the books with a bank statement.
/// SOURCE: CONVENTION (bank reconciliation); R-? Bank reconciliation.
/// INVARIANTS:
/// 1. book_balance MUST be the account's net of all posted lines.
/// 2. cleared_balance MUST be the net of cleared lines dated on or
/// before the statement date.
/// 3. difference MUST equal cleared_balance - statement_balance; zero
/// means reconciled.
/// 4. uncleared_lines MUST list exactly the posted, uncleared lines on
/// the account dated on or before the statement date.
/// ```
pub fn reconciliation_report(
&self,
account: AccountId,
@@ -2052,6 +2218,17 @@ impl Ledger {
/// Balance expressed in the account's normal side: positive means a
/// normal balance, negative means a contra balance.
///
/// ```text
/// GOAL: an account's balance on its normal side.
/// SOURCE: docs/spec/rules.md B-5.
/// INVARIANTS:
/// 1. MUST equal the account's debits minus credits for debit-normal
/// types (asset, expense), and credits minus debits otherwise.
/// 2. NEVER return negative zero.
/// SCENARIOS:
/// 1. IF the account is unknown: SHOULD refuse.
/// ```
pub fn account_balance(&self, account: AccountId) -> Result<Decimal, LedgerError> {
let account = self
.chart
@@ -2116,6 +2293,21 @@ impl Ledger {
///
/// Accounting only: there are no tax tables here. Every withholding
/// number is supplied by the caller.
///
/// ```text
/// GOAL: record a payroll: wages, the employer's FICA, what is withheld
/// and owed, and net pay.
/// SOURCE: IRS Pub. 15 (docs/spec/sources/irs-payroll.md); R-? Payroll
/// (accounting only: the caller supplies every number).
/// INVARIANTS:
/// 1. MUST post one balanced entry: Dr 6100 gross wages, Dr 6110 employer
/// FICA, Cr 2110-2150 the withholdings and both shares of FICA, Cr cash
/// net pay.
/// 2. Net pay MUST equal gross minus the employee's withholdings.
/// SCENARIOS:
/// 1. IF any amount is negative, gross is zero, or withholdings exceed
/// gross: SHOULD refuse.
/// ```
pub fn run_payroll(
&mut self,
date: NaiveDate,
@@ -2168,6 +2360,15 @@ impl Ledger {
/// Posts one entry — Dr each payable for its balance, Cr cash for the
/// total. Fails with [`LedgerError::NothingToRemit`] when nothing is
/// owed, and rejects dates in closed periods.
///
/// ```text
/// GOAL: pay the payroll taxes and withholdings owed.
/// SOURCE: IRS Pub. 15 (deposits).
/// INVARIANTS:
/// 1. MUST zero every payroll payable (2110-2150) against cash.
/// SCENARIOS:
/// 1. IF nothing is owed: SHOULD refuse (NothingToRemit).
/// ```
pub fn remit_payroll_taxes(
&mut self,
date: NaiveDate,
@@ -2283,6 +2484,16 @@ impl Ledger {
/// Tag a draft entry's line with a class, or clear it with `None`.
/// Draft-only: posted entries are immutable, so their classes are
/// frozen at posting time.
///
/// ```text
/// GOAL: tag a draft line with a class (or clear it).
/// SOURCE: R-? Tracking classes.
/// SCENARIOS:
/// 1. IF the entry is posted:
/// - SHOULD refuse
/// - NEVER change a posted line (B-8)
/// 2. IF the line or class doesn't exist: SHOULD refuse.
/// ```
pub fn set_line_class(
&mut self,
entry: EntryId,
+1 -1
View File
@@ -12,7 +12,7 @@
//! * All mutations go through [`Ledger`] commands (`create_entry`, `add_line`,
//! `post_entry`, `reverse_entry`). Posted entries are immutable; corrections
//! are reversing entries.
//! * Reports ([`trial_balance`], [`balance_sheet`], [`profit_and_loss`]) are
//! * Reports ([`trial_balance`](reports::trial_balance), [`balance_sheet`](reports::balance_sheet), [`profit_and_loss`](reports::profit_and_loss)) are
//! pure functions of a ledger, which is what makes the semantic oracle
//! (`docs/oracle.md`) possible.
+1 -1
View File
@@ -9,7 +9,7 @@
//! difference lands in Retained Earnings (created as `3999` if missing).
//! 3. Marks the period closed.
//!
//! Once closed, [`Ledger::create_entry`] and [`Ledger::post_entry`] reject
//! Once closed, [`Ledger::try_create_entry`](crate::Ledger::try_create_entry) and [`Ledger::post_entry`](crate::Ledger::post_entry) reject
//! any entry dated inside the period. Corrections to closed periods go
//! through the *next* open period (or a reversal dated in an open period).
+142 -1
View File
@@ -1,4 +1,4 @@
//! Reports: pure functions of a [`Ledger`](crate::Ledger).
//! Reports: pure functions of a [`Ledger`].
//!
//! Reports never mutate and never guess. They are what the semantic oracle
//! (`docs/oracle.md`) diffs: the same fixture books through two engines must
@@ -44,6 +44,14 @@ impl TrialBalance {
}
/// Build the trial balance from all posted entries.
///
/// ```text
/// GOAL: the trial balance over every posted entry (see trial_balance_as_of).
/// SOURCE: docs/spec/rules.md B-6; R-7.
/// INVARIANTS:
/// 1. MUST equal trial_balance_as_of on or after the last entry's date.
/// 2. MUST balance (total debits == total credits).
/// ```
pub fn trial_balance(ledger: &Ledger) -> TrialBalance {
trial_balance_where(ledger, &|_| true)
}
@@ -51,6 +59,29 @@ pub fn trial_balance(ledger: &Ledger) -> TrialBalance {
/// The trial balance as of a date: every posted entry dated on or before
/// it (closing entries included, so after a close it is the post-closing
/// trial balance).
///
/// ```text
/// GOAL: every account's balance, in a debit or a credit column, from the
/// posted entries dated on or before `as_of`.
/// SOURCE: docs/spec/rules.md B-6 (Pacioli ch. 36; IRS Pub. 583); R-7, R-9b.
/// INVARIANTS:
/// 1. MUST have total debits equal total credits, for any books built
/// through the Ledger commands.
/// 2. MUST list each non-placeholder account with a nonzero balance
/// exactly once, in code order; NEVER list a placeholder or a zero
/// balance.
/// 3. MUST put each balance in exactly one column, never negative.
/// 4. NEVER count a draft, or an entry dated after `as_of`; closing
/// entries dated on or before it count (after a close this is the
/// post-closing trial balance).
/// SCENARIOS:
/// 1. IF no entries are posted by `as_of`:
/// - SHOULD be empty with zero totals.
/// 2. IF an asset was debited 100 and credited 30:
/// - SHOULD show it with debit 70, credit 0.
/// BECAUSE the trial balance shows each account's net balance on the
/// side it falls.
/// ```
pub fn trial_balance_as_of(ledger: &Ledger, as_of: NaiveDate) -> TrialBalance {
trial_balance_where(ledger, &|e| e.date() <= as_of)
}
@@ -151,6 +182,16 @@ pub struct ClassProfitAndLoss {
///
/// The rows partition the ledger: summing every row's revenue and expenses
/// reproduces [`profit_and_loss`].
///
/// ```text
/// GOAL: a profit and loss per class (department, location, job), plus the
/// unclassified lines.
/// SOURCE: docs/spec/rules.md B-14; R-? Tracking classes.
/// INVARIANTS:
/// 1. The per-class P&Ls (with unclassified) MUST sum to the whole P&L,
/// account by account (conservation).
/// 2. NEVER count a line in two classes.
/// ```
pub fn profit_and_loss_by_class(ledger: &Ledger) -> Result<Vec<ClassProfitAndLoss>, LedgerError> {
// Which classes actually have P&L lines, and whether any P&L line is
// unclassified. Balance-sheet-only classes get no row.
@@ -399,6 +440,26 @@ fn statement_lines(
/// counts every line). Closing entries are excluded: they move a period's
/// earnings into retained earnings, so counting them would report a closed
/// period as breaking even.
///
/// ```text
/// GOAL: revenue, expenses, and net income for the entries dated from
/// `start` through `end` (both inclusive), optionally one class's lines.
/// SOURCE: docs/spec/rules.md B-14, B-11; R-9.
/// INVARIANTS:
/// 1. MUST count only revenue and expense accounts, a line per account
/// with its normal-side amount, and net income = revenue - expenses.
/// 2. NEVER count a closing entry (a close moves earnings; it doesn't
/// earn or spend), a draft, or an entry outside the range.
/// 3. MUST partition: for start <= m < end, the period's net income equals
/// [start, m] plus [m+1, end].
/// 4. MUST equal the change in current earnings (balance sheet) across
/// the range when no close falls inside it.
/// 5. With a class: MUST count only lines tagged with that class.
/// SCENARIOS:
/// 1. IF a month was closed: SHOULD still report that month's earnings.
/// BECAUSE closing doesn't make a period break even (R-9).
/// 2. IF `start` is after `end`: SHOULD refuse. (ASSUMED)
/// ```
pub fn profit_and_loss_for_period(
ledger: &Ledger,
start: NaiveDate,
@@ -443,6 +504,23 @@ pub fn profit_and_loss_for_period(
/// Revenue and expense not yet closed to retained earnings appear as
/// current earnings inside equity, so the accounting equation holds on
/// every date, before and after closes.
///
/// ```text
/// GOAL: assets, liabilities, and equity as of a date, a line per account.
/// SOURCE: docs/spec/rules.md B-7 (CON 8 ¶E62), B-11; R-8.
/// INVARIANTS:
/// 1. MUST balance: assets = liabilities + equity + current earnings,
/// exactly.
/// 2. MUST count every posted entry dated on or before `as_of`, closing
/// entries included (after a close, earnings sit in retained earnings
/// or members' capital, and current earnings is zero for closed time).
/// 3. Current earnings MUST be revenue minus expenses not yet closed.
/// 4. NEVER list revenue or expense accounts as lines.
/// 5. Lines use each account's normal-side balance (a contra balance is
/// negative).
/// SCENARIOS:
/// 1. IF nothing is posted by `as_of`: SHOULD be empty and balanced.
/// ```
pub fn balance_sheet_as_of(
ledger: &Ledger,
as_of: NaiveDate,
@@ -538,6 +616,22 @@ pub struct GeneralLedger {
/// posting to its account, with the entry's other side as the split, and a
/// running balance from the account's balance before `start`. Closing
/// entries are postings like any other.
///
/// ```text
/// GOAL: every posting from `start` through `end`, by account in code
/// order, with running balances.
/// SOURCE: docs/spec/rules.md B-6; R-9a (the QuickBooks general ledger).
/// INVARIANTS:
/// 1. Each account MUST show its beginning balance (entries before
/// `start`), every posting in the range in date order, and an ending
/// balance equal to beginning plus the postings, on the account's
/// normal side.
/// 2. Each account's ending balance MUST equal its trial-balance balance
/// as of `end` (normal side).
/// 3. Each posting MUST name its split: the one account on the entry's
/// other side, or "-Split-" when the other side has several.
/// 4. NEVER include drafts.
/// ```
pub fn general_ledger(
ledger: &Ledger,
start: NaiveDate,
@@ -718,6 +812,23 @@ pub struct MembersEquityStatement {
/// earnings plus unclosed income), are allocated by share. Allocation is to
/// the cent with the last member absorbing rounding, so every row's total
/// is exact and the ending total equals balance-sheet equity.
///
/// ```text
/// GOAL: each member's capital roll-forward for a partnership over a
/// period: beginning, contributions, allocated net income, distributions,
/// ending; with a total.
/// SOURCE: docs/spec/rules.md B-13 (26 U.S.C. §704(a); Form 1065 Schedule
/// M-2), B-11; R-9c.
/// INVARIANTS:
/// 1. Each row MUST roll forward exactly: beginning + contributions +
/// income - distributions + other = ending.
/// 2. The total ending MUST equal balance-sheet equity at `end`.
/// 3. Unclosed earnings and the period's income MUST be split by share,
/// to the cent, by largest remainder (DECISION, Stephen 2026-10-13).
/// 4. NEVER hide an unexplained change: it goes in "other".
/// SCENARIOS:
/// 1. IF shares don't sum to 1, or are not positive: SHOULD refuse.
/// ```
pub fn members_equity(
ledger: &Ledger,
start: NaiveDate,
@@ -862,6 +973,17 @@ pub fn members_equity(
/// are sorted by account code, and object keys are fixed. Two engines fed the
/// same fixture books must produce byte-identical output — that is the whole
/// point (see `docs/oracle.md`).
///
/// ```text
/// GOAL: the trial balance, P&L, and balance sheet as deterministic JSON,
/// the interchange format for cross-checking against other
/// implementations.
/// SOURCE: R-10; docs/oracle.md.
/// INVARIANTS:
/// 1. MUST be identical for the same books, whatever order accounts were
/// added in (decimal strings, sorted lines, fixed keys).
/// 2. NEVER contain floats.
/// ```
pub fn snapshot(ledger: &Ledger) -> Result<serde_json::Value, LedgerError> {
let tb = trial_balance(ledger);
let pl = profit_and_loss(ledger)?;
@@ -971,6 +1093,25 @@ pub struct CashFlowStatement {
/// working-capital changes gives operating cash flow; fixed-asset purchases
/// and disposal proceeds give investing; equity and designated debt give
/// financing. The three sections sum to the net change in `cash_accounts`.
///
/// ```text
/// GOAL: the statement of cash flows by the indirect method: net income
/// adjusted to operating cash, plus investing and financing, for a
/// period.
/// SOURCE: docs/spec/rules.md B-14 (ASC 230-10-45-28 indirect method;
/// 230-10-45-16 operating inflows).
/// INVARIANTS:
/// 1. Operating + investing + financing MUST equal the net change in the
/// cash accounts over the period, exactly.
/// 2. Operating MUST start from the period's net income (closing entries
/// excluded) and adjust for changes in non-cash working-capital
/// accounts.
/// 3. Investing MUST hold changes in non-current assets (fixed assets);
/// a disposal's gain or loss moves out of operating into investing.
/// 4. Financing MUST hold changes in equity (other than earnings) and in
/// the liabilities named as financing.
/// 5. NEVER count closing entries.
/// ```
pub fn cash_flow(
ledger: &Ledger,
params: &CashFlowParams,