The close zeroed every revenue and expense account's all-time movement, so closing a period after later entries were posted swept those too: close January in March and February's and March's income landed in January's closing entry. The preview had the same bug and never showed where the net went. Now: - close_period_to(id, to) zeroes balances as of the period end; entries dated later stay open for their own period. Periods must close in order (LedgerError::CloseOutOfOrder): a later close has already swept the earlier period. - CloseTo::Members: for a partnership (multi-member LLC) the net goes to each member's capital account by share, to the cent, the last member absorbing rounding, which is what K-1 capital accounts track. CloseTo::RetainedEarnings (3999) stays the default; close_period is shorthand for it. books.close_period picks Members when wanderbooks.toml lists members. - The journal records the close target; replay re-derives the closing entry and now compares every line with the journal, not just the id, so a change to closing math can never silently rewrite history. - The tool's preview is the real close run on a clone of the ledger (Ledger now derives Clone), so preview and post can't disagree. - Members' equity: closing entries into a capital account are income, not contributions. Balance sheet for a partnership: capital accounts (with allocated earnings), then only unallocated earnings — "Undistributed earnings, prior years" if any, and this year's unclosed net income (loss). periods::allocate is shared by the close and the statement. Tests: January closed after February is posted sweeps exactly Dr 4000 2,500 / Cr 6100 800 / Cr 3999 1,700 (mutation-checked: the old code gives 3,500/120/800/2,580); out-of-order close refused; a partnership close splits 666.67 into 333.34/333.33 with no 3999, and the members' equity statement reads correctly before and after; store replay of a members close (reopen, rebuild, verify); partnership and corporate balance sheet layouts; stdio close preview equals the posted lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
8.3 KiB
8.3 KiB
Requirements — accounting semantics (Phase 1)
Numbered requirements for the engine. R-N is current scope; R-FUTURE-N
is explicitly out of scope for now but designed for. Agents: if code and a
requirement conflict, the requirement wins.
Core double-entry
- R-1 — Balanced entries. Every journal entry has at least two lines and
total debits exactly equal total credits (
Decimalequality, no epsilon). Unbalanced entries are rejected at post time with the offending totals. - R-2 — Exact arithmetic. All money is
Decimal. No binary floats in any money path. No silent rounding: amounts are exact end to end. (Presentation rounding, when the UI arrives, is half-up to the currency's minor unit and is a display concern only.) - R-3 — Immutability. Posted entries cannot be edited or deleted. The only
correction mechanism is a reversing entry (same lines, sides flipped)
linked via
reversal_of. Draft → Posted is one-way. - R-4 — Chart of accounts. Five closed types (Asset, Liability, Equity, Revenue, Expense) with fixed normal sides (debit-normal: Asset, Expense; credit-normal: Liability, Equity, Revenue). Codes are unique and non-empty. Accounts may have parents (hierarchy). Placeholder (header) accounts organize the chart and reject postings.
- R-5 — Single currency per ledger (day one). Every account and every journal line uses the ledger's base currency. Anything else is rejected, not converted. Multi-currency with translation is R-FUTURE-1.
- R-6 — Line hygiene. Zero-amount lines are rejected, not ignored. Unknown accounts are rejected. Single-line entries are rejected.
Reports
- R-7 — Trial balance. Every non-zero, non-placeholder account appears
with its debit or credit balance, sorted by code. Total debits must equal
total credits (the
balanced()check). - R-8 — Balance sheet. Assets, liabilities, equity grouped by account
type. The accounting equation holds exactly: assets = liabilities + equity.
Current earnings (revenue minus expenses not yet closed to retained
earnings) are folded into equity so the equation holds on a live book.
balance_sheet_as_ofgives the statement as of any date, a line per account, counting every entry dated on or before it (closing entries included: after a close the earnings sit in retained earnings). - R-9 — Profit and loss. Revenue and expenses with net income.
profit_and_loss_for_periodscopes it to a date range (optionally one class), a line per account, and excludes closing entries: a close moves earnings into retained earnings and must not make a closed period read as break-even. The cash flow statement excludes closing entries for the same reason. Period P&Ls partition: split a range anywhere and the halves sum to the whole, and a range's net income equals the change in current earnings across it (property-tested). - R-9a — General ledger.
general_ledger(start, end): by account in code order, each with its beginning balance, every posting in the period (date, entry, description, debit, credit, running balance on the account's normal side), and its ending balance, which ties to the trial balance as ofend. Each posting names its split: the one account on the entry's opposite side, or "-Split-" when that side has several (the QuickBooks convention). The entry-ordered listing is the general journal, a different report. - R-9b — Trial balance as of a date.
trial_balance_as_of(as_of). - R-9c — Statement of changes in members' equity. For a multi-member LLC (partnership): each member's capital roll-forward — beginning, contributions (credits to their capital account), allocated net income (loss), distributions (debits), ending — plus a total that equals balance-sheet equity. Earnings not yet distributed (retained earnings and unclosed income) and the period's income are allocated by ownership share to the cent, the last member absorbing rounding, so every row's total is exact. Any equity change the roll-forward doesn't explain is shown as "other", never hidden.
- R-10 — Canonical snapshots.
reports::snapshot()renders the three core reports as deterministic JSON (decimal strings, sorted lines, fixed keys). Two engines fed the same books must produce identical output. This is the oracle interchange format.
Auditability
- R-11 — Append-only log. Posted entries are kept in posting order. The log plus the chart fully determine every report.
- R-12 — Command surface. All mutations go through
Ledgermethods. There is no second way to change the books.
Receipt policy (MCP phase)
- R-13a — Receipt corrections. Posted entries are immutable (R-3); their evidence is not frozen but is append-only. A receipt is replaced by attaching the new one and unlinking the old with a reason; unwanted receipts and documents are withdrawn with a reason. Unlinking may never leave an entry the policy (R-13) requires a receipt for with none. Every step is a journal event; no blob is deleted.
- R-13 — Receipt enforcement. The MCP layer (not the engine — the engine
stays policy-free) enforces a configurable receipt policy: postings at or
above a threshold (
require_receipts_above, default"25.00"in base currency) must have at least one attached receipt, with per-account overrides.post_entrywithconfirmed: truerefuses with a typedreceipt_requirederror otherwise. The draft preview always includes the receipt checklist (attached: [...],required_but_missing: bool), so the human sees it before confirming. Full spec:docs/mcp-interface.md.
Explicitly future work
- R-FUTURE-6 — Budgets and scheduled transactions. (Budgets explicitly declined as bloat; scheduled transactions remain future.)
Promoted from future (done)
- R-? — Cash flow statement (indirect method). Was R-FUTURE-3.
ledger-core::reports::cash_flow+books.cash_flow. - R-? — QuickBooks-compatible import/export. Was R-FUTURE-4.
ledger-importcrate: CSV bank/chart import, IIF and XLSX export. - R-? — Multi-currency (foreign-currency lines). Was R-FUTURE-1.
JournalLinecarries optional foreign currency/amount;amountis the base-currency equivalent.Ledger::add_line_fx,Ledger::set_fx_rate. Reports work in base currency. (Full per-account currencies and translation remain future.) - R-? — Periods and close. Was R-FUTURE-2.
ledger-core::periods:create_period,close_period_to(id, to); closed periods reject new entries. The closing entry, dated at period end, zeroes each revenue and expense account's balance as of the period end (later-dated entries belong to later periods) and puts the net into:CloseTo::RetainedEarnings(3999, the corporate convention;close_periodis shorthand) orCloseTo::Members(each member's capital account by share, to the cent, the last absorbing rounding: the partnership convention). Periods close in order (CloseOutOfOrder). The journal records the target, and replay re-derives the closing entry and checks every line against the journal. - R-? — A/R and A/P workflows. Was R-FUTURE-5.
ledger-core::arap: customers, vendors, invoices, bills, payments, AR/AP aging. MCP tools for the full workflow. - R-? — Bank reconciliation. Cleared flags on posted lines,
reconciliation_reportwith uncleared-line detail. - R-? — Payroll (accounting only).
ledger-core::payroll:run_payrollposts one balanced entry (Dr 6100 gross, Dr 6110 employer FICA, Cr 2110–2150 payables, Cr cash net);remit_payroll_taxesclears the payables. No tax tables — the caller supplies every number. - R-? — Tracking classes (divisional P&L).
ledger-core::classes:JournalLine.class_id,add_class,set_line_class(drafts only),profit_and_loss_filtered,profit_and_loss_by_class. Shared costs are split explicitly across lines at entry time. - R-? — CPA handoff package.
books.export_cpa_package: zip with trial-balance XLSX, general-ledger XLSX (every posted line), P&L and balance-sheet PDFs (printpdf, MIT).
Non-goals (all phases of the engine)
The engine never does I/O, networking, wall-clock time sourcing, or UI. It is a pure domain library: data in, validated books out.