P3 review: the surface, docs and proofs
check / check (push) Canceled after 0s

Split kernel::entry_flows into entry_movements and flows_of so the
reversal-mirror proof fits CI; prove runs the fast harnesses, prove-slow
the 8 slow ones (own job in scheduled CI), prove-all both. Laws in
verification.md renumbered 1-31 with a table of all 55 harnesses.

MCP docs checked against the 115 tools (about 60 fixes) with a test that
every tool is documented; rules.md cross-references fixed; bookspec
statuses and loader tightened; viewer notes wrap and accounts sort by
code; STATUS.md says P3 is built, cutover pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-22 02:13:08 -06:00
1 parent 2c5b5a7171
commit 0495726001
49 files changed
+963 -292

No files matched your search

+185 -82
View File
@@ -39,9 +39,11 @@ the books), `.formation_state`, `.formed`, `.state_entity_number`, `.ein`
(`books.accounting_basis`, `.cash_accounts`, `.financing_accounts` (loans,
and money the owners advanced: ASC 230-10-45-14(b)), `.investing_accounts`
(ASC 230-10-20), `.close_into` (single owner only), `.close_sweep`,
`.asset_tag_prefix`); and policy (`policy.evidence.threshold` (receipts
`.asset_tag_prefix`, `.schedule_convention` (daily, the default, or
monthly_even)); backups (`backup.git_remote`, `backup.folder`); and policy (`policy.evidence.threshold` (receipts
strictly above it; "0.00": every entry), `.required_accounts`,
`.exempt_accounts`, `policy.parties` (off, suggest, require),
`.exempt_accounts`, `policy.parties` (off, suggest, require), `policy.require_tax_category`
(warn, the default, or refuse),
`policy.approval_rules` (off, the default, or enforce)). A value is checked
by its key and kept in one spelling (a time zone as the database spells it,
an amount without leading zeros, a return as `schedule_c`, `1065`, `1120s`
@@ -129,7 +131,7 @@ settings-and-conventions section says how to use them.
state ("needs_onboarding" or "active"), settings_source, rules: {
evidence, parties, approval_rules }, settings_summary: [{ key, value,
set_by, seq }], conventions: [{ id, kind, title, summary }], open_items:
[{ id, title, due }], truncated, handbook_sections, format_upgrade?: {
[{ id, title, due }], truncated?, handbook_sections, format_upgrade?: {
guard, format, adds, how }, next_steps: [{ tool, args?, why }],
isolation }`. The connect text names it as the first call.
`format_upgrade` is there only for books waiting for a format upgrade
@@ -139,8 +141,16 @@ settings-and-conventions section says how to use them.
`isolation` is `same_user (advisory)` (D-32): the books are hidden from
agents in the owner's own account, not locked against them. Lists cut
to fit say so in `truncated` ("conventions: 4 more (books.conventions)").
`focus` adds steps: `onboarding` the onboarding step, `close` the
company's close steps, `audit` `books.verify`. Not built yet (P5):
`next_steps`, in order: the format upgrade's (when waiting);
`books.import_settings` (books that still read a settings file); the
onboarding step (`books.handbook {section: "contract"}`, when the chart
is empty or `focus` is `onboarding`); `books.open_items` (an item due
within 30 days or overdue); `books.backup` (none set up, or one stale,
failing, paused or failing its drill); the focus's step (`close`:
`books.conventions {kind: "close_step"}`; `audit`: `books.verify`); then
always `books.handbook {section: "contract"}` and `books.digest`.
`focus` is `daily` (the default), `onboarding`, `close` or `audit`;
anything else is refused. Not built yet (P5):
`book.id`, `book.url`, `head.last_handoff`, the `agent` input, and
loading it up front under tool search. Also `books://start`.
**The format upgrade** (plan 3.9, D-39, rules.md B-27). Books an older
@@ -304,9 +314,8 @@ where there is one; under "off" nothing); corrections by reversal; the receipt r
never rounded, the accounts that always need a receipt and those exempt,
and that reversals need none); the exemptions (depreciation and
amortization, whose schedules are the evidence, and closes, derived from
the books); that `books.create_invoice`, `books.create_bill`,
`books.receive_payment`, `books.pay_bill`, `books.run_payroll` and
`books.remit_payroll_taxes` post at once without a receipt check; TINs
the books); that the invoice, bill, payment and payroll tools post at
once, unchecked (in those words, naming no tool); TINs
only into the vault; show the numbers, and after posting check
`books.digest` and offer it to the human; and where settings live: in the
books, changed only through the tools, or, for books that still read a
@@ -336,8 +345,8 @@ The long guidance is the **handbook**, compiled in from
contract, entries, drafts-and-reversals, evidence-and-receipts,
attachments, files-in-out, reconciliation, fixed-assets,
prepaid-and-recurring, revenue-and-payouts, parties-and-tins, 1099,
cpa-handoff, settings-and-conventions, viewer, rules-that-change,
control-roles, tracing-figures.
cpa-handoff, settings-and-conventions, viewer, backups, format-upgrade,
rules-that-change, control-roles, tracing-figures.
- The same as resources: `books://handbook` (the index, JSON) and
`books://handbook/{section}` (Markdown, `text/markdown`), one listed per
section.
@@ -370,10 +379,12 @@ that changes anything also has a `destructiveHint`, true for these 29:
`books.set_tin` (both overwrite a vault record), `books.purge_tin`,
`books.create_invoice`, `books.create_bill`, `books.receive_payment`,
`books.pay_bill`, `books.run_payroll`, `books.remit_payroll_taxes`; and an
`idempotentHint` on the seven that a repeat with the same arguments leaves
`idempotentHint` on the ten that a repeat with the same arguments leaves
the journal and the vault as they were: `books.verify`,
`books.run_depreciation`, `books.run_schedules`, `books.run_recurring`,
`books.set_party_roles`, `books.set_setting` and `books.import_settings` (a setter such as `books.update_party`,
`books.set_party_roles`, `books.set_setting`, `books.import_settings`,
`books.set_vendor_1099`, `books.set_entry_instrument` and
`books.back_up_now` (a setter such as `books.update_party`,
`books.set_fx_rate` or `books.set_tin` journals a line, and `books.set_tin`
seals a new vault record, every time, so none carries it;
`idempotent_tools_change_nothing_when_repeated` calls each candidate twice
@@ -434,8 +445,9 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
## Tools — books
- `books.handbook { section? }`: the guide (see "Server instructions").
- `books.chart` → `{ accounts: [{ id, code, name, type, parent, placeholder,
balance }] }`. Balances included: one call answers "what do we have."
- `books.chart` → `{ accounts: [{ id, code, name, account_type, parent,
placeholder, balance, tax_category, deductible_percent }], base_currency
}`. Balances included: one call answers "what do we have."
- `books.add_account { code, name, account_type, parent?, placeholder?,
tax_category?, deductible_percent? }` → `{ account_id, warnings? }`.
Codes are unique; a name another account under the same parent has
@@ -459,11 +471,13 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
- `books.add_line { entry_id, account: "1000" | <id>, side: "debit" | "credit",
amount: "123.45" }` → `{ ok: true }`. A refused line (unknown account or
class, bad amount) adds nothing.
- `books.post_entry { entry_id, confirmed }` → `{ status, entry_id, date,
- `books.post_entry { entry_id, confirmed?, expected_total? }` → `{ status, entry_id, date,
description, total, lines (with resulting balances), receipts,
receipt_required, receipt_missing, split_detected, party_policy,
party_id?, party?, parties, parties_error?, no_party_reason?,
party_missing, party_suggestions }`. Without `confirmed: true`, `status`
party_missing, party_suggestions, approvals_missing? }`. `expected_total`,
when given, must be the entry's total debits, else nothing posts.
Without `confirmed: true`, `status`
is `"preview"` and nothing changes; with it, the receipt policy is
enforced and `status` is `"posted"`. `parties` lists every party the
entry names with its share resolved to the cent (`{ party_id, party,
@@ -478,7 +492,9 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
and a reversal whose reversal would post a depreciation period or a
schedule month a second time (naming the entry that posts it now), or
re-instate a run a later run caught up (carrying the books past the
schedule).
schedule); and a reversal of a depreciation run or a schedule month's
posting (or of a reversal in its chain) dated before that entry's own
date (a correction comes after what it corrects, B-9).
- `books.get_entry { entry_id }` → full entry with lines, receipts, and
its `vendor`, `party_id`, and `party` (the party's label, when it names
exactly one) tags, `parties` / `parties_error` as in the `post_entry`
@@ -487,8 +503,11 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
why, for a draft an earlier version journaled whose lines exact
arithmetic can't sum; a draft line on an account the chart doesn't have
(older books hold such drafts) shows an empty code and "(no account N)".
- `books.search_entries { account?, date_from?, date_to?, text?, limit?, offset? }`
→ entries with lines, newest first. `limit` caps at 100.
- `books.search_entries { account_id?, date_from?, date_to?, text?, limit?,
offset? }` → `{ entries: [{ entry_id, date, description, total,
reversal_of, receipt_count }] }`, posted entries, newest first (no
lines: `books.get_entry` has them). `limit` defaults to 50 and caps at
500.
- **Omitted dates (rules.md B-30).** One rule everywhere
(`ledger_core::calendar::PeriodDefaults`), stated in every report
tool's description and date parameters in the same words: a period
@@ -563,11 +582,13 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
- Changed in P3 (breaking): `net_income`, `depreciation` and
`working_capital` are gone (net income is in `operating_reconciliation`).
- `books.digest { since: "2026-10-01" }`
→ `{ entries_posted: [...], totals_by_account: [...], stale_rules: [...] }`.
→ `{ since, entries: [...], totals_by_account: [...], stale_rules: [...],
journal_repairs: [...], settings_problems: [...], unreconciled_accounts:
[...], uncategorized_accounts: [...] }`.
The human review surface: "what did the AI do with my books lately." The
digest is the answer to "trust, but verify." `stale_rules` lists legal
amounts wanderbooks may no longer know as of today (`{ name,
known_through, catalog, source, why }`): research the current value or
known_through, source, why }`): research the current value or
ask the human, then update it upstream (pull request or issue at
github.com/swaits/wanderbooks). `unreconciled_accounts` lists the cash
and card accounts due for reconciliation (`{ code, name,
@@ -606,23 +627,29 @@ life's last day is 9999-12-31 at the latest.
day it is ready and available for its use) starts depreciation (P3,
defect 13; B-15); `purchase_date` is its alias for one release (both
given and meaning different days: refused; the alias noted in `notes`). The
`acquisition_entry` (posted, debiting the cost account, dated on or
before the in-service date) gives `acquired_date`, which puts the asset
`acquisition_entry` (posted, debiting the cost account, neither a
reversal nor reversed, dated on or before the in-service date) gives
`acquired_date`, which puts the asset
on the register; with none, the acquired date is the in-service date.
The basis facts are `books.set_asset_tax_basis`'s (`tax_basis_rule` and
`contributor_basis`, with the old rule names, are aliases for one
release); a bad one registers nothing.
- `books.label_asset { asset, tag?, serial?, clear_serial? }` → `{ asset_id,
tag, serial }`: set or change the tag (unique ignoring case, never reused
by another asset even after a retag or disposal) or the manufacturer's
serial. `asset` is the id or current tag. Journaled as `asset.tagged`,
release); a bad one registers nothing, and so do basis facts given
without `basis_rule`.
- `books.label_asset { asset, tag?, serial?, clear_serial?, description?,
acquisition_entry? }` → `{ asset_id, tag, serial, acquisition_entry,
description }`: set or change the tag (unique ignoring case, never reused
by another asset even after a retag or disposal), the manufacturer's
serial, the description, or the linked acquisition entry (posted,
debiting the cost account, not reversed). Refused with nothing to
change, or with both `serial` and `clear_serial`. `asset` is the id or current tag. Journaled as `asset.tagged`,
`asset.serial_set`.
- `books.asset_labels { assets?, skip? }` → `{ file_name, content_base64,
tags }`: an Avery 5160/8160 label sheet (PDF): company, tag, QR code of
the tag, description, serial. Default: every active tagged asset; `skip`
reuses a partly used sheet.
- `books.asset_schedule { asset_id | tag, through? }`
→ `{ status, schedule: [{ period, amount, accumulated, book_value }] }`;
→ `{ asset_id, description, cost, salvage_value, useful_life_months,
status, schedule: [{ period, amount, accumulated, book_value }] }`;
a disposed asset's ends on its disposal day. A schedule an earlier
version's asset can't compute is a tool error saying why, never a
panic.
@@ -678,8 +705,9 @@ life's last day is 9999-12-31 at the latest.
in_service_date?, reason }` → `{ asset_id, tag, cost, salvage_value,
useful_life_months, in_service_date, acquired_date, basis, notes? }`.
Refused while the books carry depreciation for it (a run posted and not
reversed), and for an in-service date before its acquisition entry's
date; `in_service` is the alias of `in_service_date` for one release.
reversed), for a disposed asset, for a blank `reason`, and for an
in-service date before its acquisition entry's date; `in_service` is the
alias of `in_service_date` for one release.
A recorded value at contribution, and so the built-in gain, never
moves; a basis recorded before P3 has none and reads it as the book
cost, so a cost correction moves its built-in gain, and `notes` says so.
@@ -701,7 +729,8 @@ life's last day is 9999-12-31 at the latest.
- `books.dispose_asset { asset_id | tag, date, proceeds, proceeds_account,
gain_loss_account, buyer?, confirmed?, receipt_attachment_ids? }` (the
date never before the acquired date) →
without confirmation: `{ status: "preview", accumulated, book_value,
without confirmation: `{ status: "preview", asset_id, entry_id (0 in a
preview), accumulated, book_value,
proceeds, gain_loss, final_depreciation, excess_depreciation,
catch_up_depreciation, lines, receipt_required }` and posts nothing;
with `confirmed: true`: auto-posts the final depreciation (through the
@@ -774,8 +803,10 @@ through the policy.
the next month end to post (null once every month has). Refused: a
convention but those two (naming them); a `monthly_even` schedule not
starting on the 1st (naming the 1st, and `daily`); one whose months
aren't whole minor units (yen); a daily one whose last day is after
9999-12-31. `plan_error` is null, except for a schedule an earlier
aren't whole minor units (yen); `months` outside 1 to 600; one whose
last day is after 9999-12-31; and, in books an older version wrote, a
daily one (it needs the journal feature `schedule_convention`:
`format_upgrade_pending` until the owner's format upgrade). `plan_error` is null, except for a schedule an earlier
version accepted whose months run past the last date the books can
compute: then it says why, `remaining` and `next` are null and `months`
empty, and `books.run_schedules` refuses until it is dealt with.
@@ -796,7 +827,10 @@ through the policy.
month-end start stays on month ends (Jan 31, Feb 28, Mar 31).
- `books.list_recurring {}` → `{ recurring: [...] }`.
- `books.run_recurring { through? }` → `{ drafts: [{ recurring_id, date,
entry_id }] }`: one draft per occurrence on or before `through`, once.
entry_id }], skipped_closed_period: [{ recurring_id, date }] }`: one
draft per occurrence on or before `through`, once; an occurrence dated
in a closed period is not generated and is listed in
`skipped_closed_period` for the human to decide.
## Tools — the viewer
@@ -850,7 +884,8 @@ through the policy.
- `books.verify { blobs? }` → `{ status: "ok" | "healed", events, head,
healed?, blobs_verified?, database, other_databases?, checked }`
(`database`: the projection file verified, one for each schema,
`wanderbooks.v{N}.db`; `other_databases`: other schemas' files beside it,
`wanderbooks.v{N}.db`, or the older `wanderbooks.db` when it holds this
version's schema; `other_databases`: other schemas' files beside it,
left for the versions that use them). Verifies the journal's hash chain,
replays it against the live ledger, and compares every table of the
SQLite database row for row against a fresh projection of the journal.
@@ -859,6 +894,22 @@ through the policy.
every stored file. The server runs the same check, and heals the same
way, every time it starts.
## Tools — access
- `books.access_log { limit?, before?, token?, action?, outcome? }`
(books:admin; read-only) → `{ records, total, more, head: { seq, hash }
}`: who reached these books through the server (plan I-9), newest
first, each record its token, client, tool or resource, a digest of
its arguments and whether it ran (`ok`), failed (`error`) or was
refused. `limit` is 1 to 500 (default 50); `before` pages back (records
before that seq); `token` narrows to a token's name, `action` to `tool`,
`resource` or `connect` (refused at the door), `outcome` to `ok`,
`error` or `refused` (anything else is refused). `total` counts the
whole log; `more` says older records match too. The log is read whole
and its chain checked first: a damaged log is reported, never shown in
part. Books served over stdio keep no access log: the tool is not
offered there (and is refused if called).
## Tools — backups
Books served from the data root back themselves up (plan 3.7, D-30,
@@ -1005,9 +1056,13 @@ first entry's).
incorporates is a new vendor. A dated decision with nothing decided
before it is refused; one the same as the decision before its date
takes out any decision recorded for that date ("removed"); recorded
again exactly so, nothing is journaled ("unchanged"). Without
`form_1099`, `tin_on_file` alone marks the W-9, keeping the decision and
who made it; `tin_on_file` omitted keeps the flag. `warnings` names each
again exactly so (who decided and when included), nothing is journaled
but a changed W-9 flag ("unchanged"). Without `form_1099`, `tin_on_file`
alone marks the W-9, keeping the decision and who made it (`changed` is
then "unchanged", with no warnings); `reason_kind`, `reason` or
`effective` without `form_1099` are refused, and so is a call with
neither `form_1099` nor `tin_on_file`; `tin_on_file` omitted keeps the
flag. `warnings` names each
year (and its entries) whose payments the change reaches in a closed
period or after that year's 1099s were due (January 31 of the next
year): tell the human, a filed 1099 may need correcting. Journaled as
@@ -1035,7 +1090,10 @@ first entry's).
`entry.instrument_set`.
- `books.set_entry_vendor { entry_id, vendor? }` → the entry (as
`books.get_entry`, which shows `vendor`, and `instrument` and
`instrument_id`). Omitting `vendor` clears the tag. Metadata only:
`instrument_id`). Omitting `vendor` clears the tag. On an entry naming
no party or one, the vendor's party becomes its party (clearing a
no-party mark), and clearing the tag clears the party too when it was
the vendor's. Metadata only:
posted entries may be tagged. `books.draft_entry` takes `vendor` and
`instrument` too; bills and their payments are tagged automatically.
`books.set_entry_party` tags the vendor too when the party has a vendor
@@ -1155,9 +1213,9 @@ texts are at most 500 characters, no control characters.
kept), so `books.vendor_1099`'s `missing_w9` follows.
`tin` and `tin_type` put the full TIN in the TIN vault, and the signed
form (`w9_upload_id`, or `w9_content_base64` with `w9_sha256`, as in
"Tools — receipts & documents") goes there too (below); with them
`tin_last4` comes from the TIN (a given one must agree) and `kept_at`
defaults to `"wanderbooks vault"`.
"Tools — receipts & documents") goes there too (below); with a TIN,
`tin_last4` comes from it (a given one must agree); with a TIN or the
form, `kept_at` defaults to `"wanderbooks vault"`.
- `books.set_w4 { party, received_on, state, kept_at }` or `{ party, clear:
true }` → the party. Journaled as `party.w4_set`.
- `books.set_entry_party { entry_id, party?, parties?, no_party_reason? }`
@@ -1174,7 +1232,11 @@ texts are at most 500 characters, no control characters.
place of `party`, names every counterparty with its share; `[]` clears.
Journaled as `entry.parties_set { entry_id, parties: [{ party_id, share
}] }`, `share` being `{"amount": "30.00"}`, `{"percent": "60"}`, or
`"derived"`. Each share, of what the entry moved (its `entry_flow` net;
`"derived"`. Once the entry is posted (at `post_entry`, or tagging a
posted entry), a derived split among several parties is journaled again
as the exact amounts it resolves to (a second `entry.parties_set`), so a
later change to `[[company.members]]` never re-splits posted history; it
stays derived when the shares don't resolve or a part is zero. Each share, of what the entry moved (its `entry_flow` net;
for an entry moving no money, like property contributed, its total):
- `amount`: exactly that, positive, in cents; the amounts must sum to
what it moved.
@@ -1188,8 +1250,9 @@ texts are at most 500 characters, no control characters.
`[[company.members]]` shares, rescaled to the members named, rounded as
percents are. A derived share for a non-member among several is
refused.
Every party gives the same kind of share. Refused, journaling nothing: a
party named twice, an unknown party, mixed kinds, percents not summing
Every party gives the same kind of share. Refused, journaling nothing:
`party` with `parties`, or either with `no_party_reason`; a share giving
both `amount` and `percent`; a party named twice, an unknown party, mixed kinds, percents not summing
to 100, an amount finer than a cent, two parties with vendor records (a
1099 counts a payment toward one vendor; the one vendor party's record
is tagged and counts the whole payment), and, once the entry has
@@ -1272,7 +1335,7 @@ rules.md B-35):
cash_accounts` (every asset account but receivables when none are set),
the cards (the business-card payment methods' liability accounts,
`books.instruments`; while the books record none, the liability
accounts whose names say "card", not "gift card"; never a member's
accounts whose names say "card" and not "gift"; never a member's
card or a platform's account: the one rule the reconciliation reminders
and the viewer read), and the `[books]
financing_accounts` (what the members advanced, loans). Any other
@@ -1495,10 +1558,11 @@ and no control or invisible (Unicode format) characters; a MIME type
the upload's; inline they are required. The upload is taken only after
the entry and every `link_to` subject check out. `link_to` as in "Tools
— attachments".
- `books.link_receipt { receipt_id, entry_id }` → link a previously filed
- `books.link_receipt { attachment_id, entry_id }` → `{ ok }`. Links a previously filed
(unlinked, not withdrawn) receipt to its entry. A receipt linked elsewhere
must be unlinked first; one already linked to that entry
(`books.link_attachment`) is refused, as filing it there twice.
Journaled as `receipt.linked`.
- `books.unlink_receipt { attachment_id, reason }` → `{ ok, entry_id,
remaining_receipts }`. Detaches a receipt from its entry; it stays filed,
unlinked. Journaled as `receipt.unlinked` with the reason. Subject to the
@@ -1506,7 +1570,9 @@ and no control or invisible (Unicode format) characters; a MIME type
(`receipt_required` error, remedy `books.attach_receipt`), so replacing a
receipt is attach-new, then unlink-old.
- `books.withdraw_attachment { attachment_id, reason }` → `{ ok }`. Retires
an unlinked receipt or a filed document: hidden from listings, never
a receipt or document that is neither filed on an entry nor linked to
any subject (take those off first: `books.unlink_receipt`,
`books.unlink_attachment`); the reason must not be blank. Hidden from listings, never
linkable again. Journaled as `attachment.withdrawn`; the bytes stay in the
blob store. Nothing in the evidence trail is ever erased — receipts change
by new events, the same way entries change by reversals.
@@ -1516,7 +1582,8 @@ and no control or invisible (Unicode format) characters; a MIME type
are omitted unless `include_withdrawn` is true. `entry_id` lists the
files filed on the entry and linked to it.
- `books.get_receipt { attachment_id, view?: "meta" | "bytes" }` → `{
metadata, description?, links, content_base64?, download? }`. `meta`
attachment_id, sha256, entry_id?, category, file_name, mime_type,
size_bytes, description?, links, content_base64?, download? }`. `meta`
(the default) returns no bytes. `bytes` adds `content_base64`
(hash-verified) for a file of at most 256 KiB; a larger one gives
`download`, the command `wanderbooks download --book B <id> --to <a
@@ -1540,11 +1607,14 @@ engine stays policy-free (it knows nothing of receipts) — enforcement lives
in the MCP layer, which reads the `attachments` table before confirming a
posting.
- **Policy config** (server config file, not per-call): `require_receipts_above`
(default `"25.00"` in base currency), plus `receipt_required_accounts`
(account codes that always require receipts, e.g. meals and travel) and
`receipt_exempt_accounts` (e.g. automated bank fees). Human-edited and
versioned with the deployment.
- **Policy** (the books' settings, "Company settings"; not per-call):
`policy.evidence.threshold` (receipts strictly above it; unset
`"25.00"`; `"0.00"`: every entry), `policy.evidence.required_accounts`
(account codes whose entries always need one, e.g. meals and travel)
and `policy.evidence.exempt_accounts` (e.g. automated bank fees; an
entry is exempt only when every line is in one). Changed with
`books.set_setting`; books from before P1 read `config.json`'s
`receipt_policy`.
- **Preview.** Every `post_entry` preview includes the receipt checklist
(`receipts: [...]`, `receipt_required: bool`, `receipt_missing: bool`,
`split_detected: [...]`), so the human sees it before confirming. A
@@ -1560,6 +1630,12 @@ posting.
— not to shrink the amount or split the entry to dodge the threshold. The
tool description says this explicitly; gaming the policy is a correctness
violation, and the append-only audit log would show it anyway.
(`asset_id` instead of `entry_id` for a disposal; `split_group: [ids]`
when the split check triggered.)
- **Not checked yet:** `books.create_invoice`, `books.create_bill`,
`books.receive_payment`, `books.pay_bill`, `books.run_payroll` and
`books.remit_payroll_taxes` post at once with no preview and no receipt
check: attach their evidence right after, to the entry they return.
- **Anti-splitting.** Posting an entry triggers a split check: posted
entries on the same date with the same normalized description and debit
accounts are grouped, and if the group total exceeds the threshold while
@@ -1585,17 +1661,21 @@ posting.
and pdf are the printed statement, operating by class and investing
and financing by kind with each kind's transactions under it, with its
two schedules), `trial_balance`, `members_equity` (statement of
changes in members' equity; members from `wanderbooks.toml`; earnings
changes in members' equity; members from the settings (`company.members`); earnings
not yet closed are split the way the next members close will split
them, so a member's capital on a day never changes when its month is
closed: rules.md B-13, D-38),
`general_ledger` (by account: beginning balance, postings with running
balance and split, ending balance), `general_journal` (alias
`journal`: each entry posted in the period, line by line; `iif`:
every posted entry from the first through `as_of`, never a period).
- Formats: `pdf` and `xlsx` (base64) for the five statements; `xlsx`,
`csv`, `json` for every report; `iif` for `general_journal` (alias
`journal`), for QuickBooks Desktop.
every posted entry from the first through `as_of`, never a period),
`tax_summary` (see "Tools — tax categories"), `fixed_asset_register`
(as of a day; see "Tools — fixed assets").
- Formats: `pdf` and `xlsx` (base64) for `profit_and_loss`,
`balance_sheet`, `cash_flow`, `trial_balance`, `members_equity`,
`tax_summary` and `fixed_asset_register`; `xlsx`, `csv`, `json` for
every report (`general_ledger` and `general_journal` have no pdf);
`iif` for `general_journal` (alias `journal`), for QuickBooks Desktop.
- Dates (rules.md B-30): the period reports (`profit_and_loss`,
`cash_flow`, `members_equity`, `tax_summary`, `general_ledger`, and
`general_journal` as csv, json and xlsx) take `start` and `end`
@@ -1612,7 +1692,8 @@ posting.
character past ASCII is transliterated ("René" "Rene", "北京" "Bei
Jing", "—" "--"), a double quote is a single one (QuickBooks reads it
as a text delimiter), a tab or line break is a space, a control
character '?'. Within QuickBooks Desktop's limits (Intuit): an account
character, or a character with no transliteration, '?'; a colon in an
account's name or number '-' (a colon separates levels). Within QuickBooks Desktop's limits (Intuit): an account
name level past 31 characters is cut there; accounts whose names would
then match get " (code)" (QuickBooks would merge them); a subaccount
whose QuickBooks type differs from its parent's is written at the top
@@ -1648,8 +1729,9 @@ posting.
capital and draws; per-partner capital and distributions plus guaranteed
payments; officer compensation and shareholder distributions; dividends
and income tax expense), common income and expense accounts with tax
categories, and the `wanderbooks.toml` settings (tax form, members, close
target). A proposal: review with the human, then `books.import_chart_csv`.
categories, and `settings_toml`, the settings that go with it (tax form,
members, where closes go) as settings-file TOML for
`books.import_settings`. A proposal: review with the human, then `books.import_chart_csv`.
## Tools — tax categories
@@ -1662,10 +1744,14 @@ posting.
- `books.renumber_account { account, code }` → `{ id, previous_code,
code, config_mentions }`: give an account a new code. Only the code
changes (entries name accounts by id), so balances and reports don't
move. Refuses a code in use, or an unassigned control role's default
code for an account of the wrong type. `config_mentions` lists the
wanderbooks.toml settings still naming the old code (edit them; the
server reloads the file). Journaled as `account.renumbered`.
move. Refuses an empty code, a code in use, an unassigned control
role's default code for an account of the wrong type or a header
(placeholder), and any account an approval rule names (revise or retire
the rule first). `config_mentions` lists each setting still naming the
old code (as `books.set_setting` keys, or settings-file labels for books
whose settings are in a file) and each convention whose template or
approval rule names it: change each with `books.set_setting` /
`books.set_convention`. Journaled as `account.renumbered`.
- `books.rename_account { account, name, reason? }` → `{ id, code,
previous_name, name, notes }` (scope `books:post`, destructive: a
rename changes the label every past report prints): give an account a
@@ -1718,8 +1804,13 @@ posting.
- `books.import_bank_csv { csv_text? | upload_id?, bank_account_code,
default_offset_code, rules?, date_order?, source_attachment_id? }` →
creates one draft per transaction (QBO upload layout). `rules` maps
description substrings to account codes. `source_attachment_id` (the export as filed, category
`{ imported: [{ entry_id, date, description, amount, offset_account }],
count, source_attachment_id, source_linked }`: one draft per
transaction, all or none, from the QuickBooks bank-upload layout
(`Date,Description,Amount`, payments negative, or
`Date,Description,<money out>,<money in>`). `rules` maps description
substrings (ignoring case; the longest match wins) to account codes,
else `default_offset_code`. `source_attachment_id` (the export as filed, category
`import_source`) is checked before any draft and linked to the bank
account (`source_linked`; false when it was already), never to the
drafts: a bank export is not a receipt.
@@ -1748,7 +1839,7 @@ Hand-rolled on `ServerHandler` (rmcp 3.x has no resource macros):
- `books://receipts/{id}` — receipt metadata (as `books.get_receipt`
view `meta`; no bytes)
Resources exist so read-only clients (dashboards, the future read-only GUI,
Resources exist so read-only clients (dashboards, the viewer,
auditors) never need tool credentials. Tested end-to-end over stdio.
## The agent operating contract
@@ -1775,9 +1866,8 @@ in the server's tool descriptions, so the model reads it at use time):
## Out of scope for this phase
Bank feeds, payment processing, payroll, tax filing, multi-user authN/Z, the
GUI (it consumes the event log in a later phase). Cash flow reporting is
R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
Bank feeds, payment processing, payroll tax tables and filing (payroll is
accounting only: the caller supplies every figure), tax filing.
## Tools — periods
@@ -1789,9 +1879,11 @@ R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
ledger), posting nothing; with `confirmed: true`: `{ status: "closed",
period_id, closing_entry, lines, members }`, posting it and marking the
period closed. The entry zeroes revenue and expense balances as of the
period end; the net goes to `3999` Retained Earnings, or, with
`[[company.members]]` in the settings, to each member's capital account
by share, cumulatively (rules.md B-13; D-10): each close gives a member
period end; the net goes to `3999` Retained Earnings (created if
missing), or to `books.close_into` (a single owner's capital), zeroing
the `books.close_sweep` accounts (draws, dividends) into it too; or,
with `company.members` in the settings, to each member's capital
account by share (each member's distribution account zeroed into it), cumulatively (rules.md B-13; D-10): each close gives a member
their share of everything closed to these members and shares so far, to
the currency's minor unit (odd units by largest remainder, ties to the
member listed first when the count began), less what earlier closes gave
@@ -1875,16 +1967,19 @@ R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
amount }] }` → `{ id, total, entry_id, warnings? }`. Auto-posts Dr
`1200` Accounts Receivable / Cr `4000` Revenue. `warnings` names a
revenue account it created with no tax category (rules.md B-32).
`party_id` is the customer's `id` from `books.add_customer`, not its
`party_id`.
- `books.create_bill { party_id, date, due_date, lines }` → `{ id, total,
entry_id, warnings? }`. Auto-posts Dr `6999` General Expenses / Cr
`2000` Accounts Payable; `warnings` as for invoices.
`2000` Accounts Payable; `warnings` as for invoices. `party_id` is the
vendor's `id` from `books.add_vendor`, not its `party_id`.
- `books.receive_payment { document_id, date, amount, cash_account }` →
`{ entry_id, remaining, status }`. Posts Dr Cash / Cr A/R.
- `books.pay_bill { document_id, date, amount, cash_account }` →
`{ entry_id, remaining, status }`. Posts Dr A/P / Cr Cash.
- `books.ar_aging { as_of? }` → `{ as_of, rows: [{ name, current, days_30,
days_60, days_90, days_90_plus, total }] }` (`as_of` omitted: today).
- `books.ap_aging { as_of }` → same, per vendor.
- `books.ap_aging { as_of? }` → same, per vendor (`as_of` omitted: today).
## Tools — foreign exchange
@@ -1956,7 +2051,7 @@ entries.
file keeps the books' text), the README lists the same lines (and
names the entries with a line on accounts receivable or payable, to
which QuickBooks Desktop may ask the CPA to add a customer or vendor),
and `note` ends "N account name(s), number(s), place(s) or memo(s)
and, when the file writes anything differently, `note` ends "N account name(s), number(s), place(s) or memo(s)
are written differently in general-journal.iif to fit QuickBooks
Desktop (warnings says how; so does the README): tell the human.", N
counting each name, number, place and memo.
@@ -1978,7 +2073,11 @@ entries.
last fours only.", or "The package, with full TINs, for the CPA.",
followed, with gaps, by "N TIN(s) are missing (tin_gaps): collect them
with books.set_tin or books.set_w9 and export again." **K-1 worksheet** (`k1-members.csv`, with
`[[company.members]]`): one row per member, in order: `member,
`[[company.members]]`): one row per member, in order (with more than one
member, a last row totals them, "Total, all members (not a K-1)", to tie
to Schedule L; a worksheet that can't be made is replaced by
`<name>-NOT-INCLUDED.txt` saying why, the K-1, tax-basis and 1099
worksheets alike): `member,
party_id, share, capital_account, tin, tin_type, tin_note` (without
TINs: `tin_last4, tin_type`), then `beginning_capital,
contributions_cash, contributions_property, net_income, distributions,
@@ -2023,6 +2122,10 @@ entries.
by class, investing and financing by kind with each member named, and
its two schedules; reconciled to the package's cash accounts),
`members-equity`
(multi-member LLCs), `trial-balance`; plus `general-ledger.xlsx` and
`general-journal.xlsx`. Period reports cover the fiscal year to date.
(multi-member LLCs), `tax-summary`, `trial-balance`;
`fixed-asset-register` (PDF and XLSX) when an asset was acquired by
`as_of`; plus `general-ledger.xlsx` and `general-journal.xlsx`; and
`README.txt` first (the year, the basis, the journal's verified event
count and what each file is). Period reports cover the fiscal year
holding `as_of`, through it.
What the CPA needs; they never log into the system.
+7 -2
View File
@@ -15,7 +15,7 @@ layers differ in what they cover and in whether we may copy them:
| **Pacioli (1494), Geijsbeek translation (1914)** | The mechanics of double entry: debits equal credits, the trial balance, one money, writing only forward, correcting by opposite entry, closing into capital. No modern standard restates these; they predate every regulator. | Public domain | Vendored verbatim: `sources/pacioli-1494.md` |
| **IRS: Internal Revenue Code, Treasury Regulations, publications, form instructions** | Records and supporting documents, retention, accounting periods and methods, information returns (1099), capitalization (de minimis, 12-month rule), partners' capital, tax basis of purchased and contributed property (`sources/irs-partnership-basis.md`), payroll taxes, rounding on returns, return line items for tax categories (`sources/irs-tax-lines.md`) | Public domain (17 U.S.C. §105; IRS.gov's content policy) | Vendored verbatim: `sources/irs-*.md`, `sources/us-public-domain-status.md` |
| **File formats: Intuit (QuickBooks Online bank CSV, QuickBooks Desktop IIF), ECMA-376 Office Open XML, Microsoft Excel limits, Avery label sheets** | What `ledger-import` reads and writes: bank/card CSV layouts and signs, IIF journal entries, how XLSX stores numbers and Excel's 15-digit precision, asset-label sheet geometry (Avery 5160) | © Intuit Inc.; © Ecma International; © Microsoft; © Avery Products Corporation | Cited with short quotes, URLs, and hashes: `sources/intuit-bank-csv.md`, `sources/intuit-iif.md`, `sources/xlsx-numbers.md`, `sources/avery-5160.md` |
| **FASB Accounting Standards Codification (ASC)** | Authoritative US GAAP: cash flows (ASC 230), revenue and principal vs agent (ASC 606), foreign currency (ASC 830), credit losses and aging (ASC 326), property and depreciation (ASC 360) | © Financial Accounting Foundation | Cited by paragraph with short quotes only (in `rules.md`) |
| **FASB Accounting Standards Codification (ASC)** | Authoritative US GAAP: cash flows (ASC 230), revenue and principal vs agent (ASC 606), foreign currency (ASC 830), credit losses and aging (ASC 326), property and depreciation (ASC 360) | © Financial Accounting Foundation | Cited by paragraph with short quotes only (in `rules.md`; the paragraphs read, with where and when, in `sources/fasb-asc.md` and below) |
| **FASB Concepts Statement No. 8, Chapter 4 (2021)** | Definitions of the five elements (it replaced Concepts Statement 6) | © Financial Accounting Foundation; non-authoritative | Cited with short quotes only |
| **Textbooks (OpenStax *Principles of Accounting, Vol. 1*)** | Conventions no standard states: normal balances, closing-entry mechanics, straight-line depreciation arithmetic | CC BY-NC-SA 4.0 (incompatible with this repo's license) | Cited with short quotes and attribution only |
| **Convention or decision** | Where no authority speaks: aging bucket edges, cent rounding among members, closed-period locks | — | Stated as CONVENTION or DECISION, with who decided and when |
@@ -72,4 +72,9 @@ Deloitte's public DART Roadmap): ASC 230-10-10-1, 45-1, 45-4, 45-5,
each checked again against the same sources in the P3 review (numbers,
pages and wording hold). Open: 105-10-05-1 to 05-3, read only in their
2009 text (ASU 2009-01, and as Wiley GAAP 2020 quotes them); their
current wording is unconfirmed.
current wording is unconfirmed. Also open (P3 surface review, 2026-10-21): ASC 230-10-45-15(f) (debt
issue costs, cited by B-14 rule 11) is in neither list above; ASC
606-10-25-31, 25-32 and 55-20 (B-16's over-time method) are not yet read;
B-15 rule 6 cites ASC 250 with no paragraph; B-17's 606-10-55-39 is in
neither list. Each is to be read in the ASC basic view and listed in
`sources/fasb-asc.md` before it is relied on further.
+4 -3
View File
@@ -78,8 +78,9 @@ schedules round to it (B-15, B-16); assets an earlier version took finer
replay as journaled. **Checked:**
`an_asset_cost_finer_than_a_cent_is_refused`,
`a_foreign_line_is_booked_in_whole_base_units`,
`no_entry_posts_a_fraction_of_the_minor_unit`; the GnuCash oracle.
**Checked:** `foreign_currency_line_is_rejected`, `fx_overflow_is_an_error`.
`no_entry_posts_a_fraction_of_the_minor_unit`,
`foreign_currency_line_is_rejected`, `fx_overflow_is_an_error`; the
GnuCash oracle.
## B-4 Five elements, closed list
@@ -1997,7 +1998,7 @@ books declare before their first rename, B-27); `books.rename_account` (scope
`ledger-core` `tests/spec_ledger.rs`
(`a_new_name_is_one_trimmed_line_of_bounded_length`), `ledger-store`
`tests/store.rs` (`a_new_accounts_name_is_journaled_trimmed`); Lean
`Step.record` (a name is outside the model: `docs/verification.md` 13a);
`Step.record` (a name is outside the model: `docs/verification.md` law 15);
bookspec `examples/rename-changes-no-amount.toml`.
## B-32 Every revenue and expense account maps to a line of the return
+5 -2
View File
@@ -4,7 +4,7 @@ Governs what a member's party totals count (rules.md B-35): a member's capital a
## Treas. Reg. §1.704-1(b)(2)(iv)(b): basic rules of capital accounts
Source: <https://www.ecfr.gov/api/renderer/v1/content/enhanced/current/title-26?part=1&section=1.704-1>
Retrieved: 2026-10-20
SHA-256 of the retrieved file (`ecfr7041.html`): `0eafa6f27d7df3a3cc93ebad7cbdc0c37a49b055c53a43d5e5e8efa29f80beeb`
SHA-256 of the retrieved file (`ecfr17041.html`): `0eafa6f27d7df3a3cc93ebad7cbdc0c37a49b055c53a43d5e5e8efa29f80beeb`
> (b) Basic rules. Except as otherwise provided in this paragraph (b)(2)(iv), the partners' capital accounts will be considered to be determined and maintained in accordance with the rules of this paragraph (b)(2)(iv) if, and only if, each partner's capital account is increased by
@@ -30,6 +30,9 @@ Source: as above (the same retrieved file).
## Partner's Instructions for Schedule K-1 (Form 1065), Item L
Source: <https://www.irs.gov/instructions/i1065sk1>
Retrieved: 2026-10-20
SHA-256 of the retrieved file (`i1065sk1.html`): `87d46c0e6c07eb0b642e9479f72aed7c35ed005cab0b1c644af5572a9df22d7c`
SHA-256 of the retrieved file (`i1065sk1.html`): `87d46c0e6c07eb0b642e9479f72aed7c35ed005cab0b1c644af5572a9df22d7c`
(This page's HTML differs from fetch to fetch, so each retrieval has its
own hash: `irs-partnerships.md` quotes another retrieval of the same day. A re-fetch on
2026-10-21 hashed differently again; the quoted text was checked present.)
> The partnership must report your beginning capital account and ending capital account for the year using the tax-basis method, including the amount of capital you contributed to the partnership during the year, your share of the partnership’s current-year net income or loss as computed for tax purposes, any withdrawals and distributions made to you by the partnership, and any other increases or decreases to your capital account determined in a manner generally consistent with figuring the partner’s adjusted tax basis in its partnership interest (without regard to partnership liabilities), taking into account the rules and principles of sections 705, 722, 733, and 742.
+5 -2
View File
@@ -1,5 +1,5 @@
# Partnerships: shares of income and capital accounts
Governs `CloseTo::Members`, the members' equity statement, and the K-1 worksheet's capital accounts on the tax basis (`ledger_core::tax_capital`, rules.md B-36). Partners' shares come from the partnership agreement (IRC §704(a)); a partner's capital account is one running balance of what they contributed, their allocations of income and loss, and what was distributed to them (Treas. Reg. §1.704-1(b)(2)(iv)(b)); when a partner's interest changes during the year, the distributive shares take the varying interests into account, period by period (IRC §706(d)(1), Treas. Reg. §1.706-4(a)); Form 1065 reports each partner's capital account on the tax basis (Schedule K-1 item L) and the changes in them (Schedule M-2). No authority prescribes how to round a share to the cent: that rule is the owner's decision (largest remainder, 2026-10-13), counted from the company's first close (D-10, 2026-10-16).
Governs `CloseTo::Members`, the members' equity statement, and the K-1 worksheet's capital accounts on the tax basis (rules.md B-13; `ledger_core::tax_capital`, rules.md B-36). Partners' shares come from the partnership agreement (IRC §704(a)); a partner's capital account is one running balance of what they contributed, their allocations of income and loss, and what was distributed to them (Treas. Reg. §1.704-1(b)(2)(iv)(b)); when a partner's interest changes during the year, the distributive shares take the varying interests into account, period by period (IRC §706(d)(1), Treas. Reg. §1.706-4(a)); Form 1065 reports each partner's capital account on the tax basis (Schedule K-1 item L) and the changes in them (Schedule M-2). No authority prescribes how to round a share to the cent: that rule is the owner's decision (largest remainder, 2026-10-13), counted from the company's first close (D-10, 2026-10-16).
## 26 U.S.C. §704(a)-(b)
Source: <https://www.law.cornell.edu/uscode/text/26/704>
@@ -59,7 +59,10 @@ SHA-256 of the retrieved file (`ecfr17064.html`): `e7b07463190db6a610dee37660efe
## Partner's Instructions for Schedule K-1 (Form 1065) (2025)
Source: <https://www.irs.gov/instructions/i1065sk1>
Retrieved: 2026-10-20
SHA-256 of the retrieved file (`i1065sk1.html`): `d7d6298286601fd95ab8e445c10fd62499ea1e37f8e5c3531f8e1783862d8d03`
SHA-256 of the retrieved file (`i1065sk1.html`): `d7d6298286601fd95ab8e445c10fd62499ea1e37f8e5c3531f8e1783862d8d03`
(This page's HTML differs from fetch to fetch, so each retrieval has its
own hash: `irs-capital-accounts.md` quotes another retrieval of the same day. A re-fetch on
2026-10-21 hashed differently again; the quoted text was checked present.)
**Item L, its first paragraph** (the partner's capital account, on the tax basis; the K-1 worksheet carries the book capital account and, beside it, the tax-basis one as far as the books know it, rules.md B-36)
+70 -5
View File
@@ -23,7 +23,7 @@ promise.
| Model-based stateful | the Lean state machine, transcribed (3) | `tests/model.rs` | `just check` | ~3s | every change |
| Differential: Decimal vs i64 | the kernel at two types (3) | `kernel_bridge.rs` | `just check` | <1s | every change |
| Differential: GnuCash | another implementation, adjudicated (3) | `tests/oracle_gnucash.rs`, `oracle/gnucash/` | `just oracle` | ~2 min | weekly |
| Formal proofs | Kani (bounded) and Lean (unbounded) | `src/proofs.rs`, `verify/lean/` | `just prove` | ~15-20 min (bounded by `entry_flows_of_a_reversal_mirror`, ~14 min; all 29 harnesses ~43 min of CPU, run in parallel) | weekly |
| Formal proofs | Kani (bounded) and Lean (unbounded) | `src/proofs.rs`, `verify/lean/` | `just prove` (47 harnesses and Lean), `just prove-slow` (the 8 in the justfile's `kani_slow`, each under its own 30-minute cap) | prove ~5 min and prove-slow ~8 min at four jobs on a quiet laptop (each ~25 min under heavy load; docs/verification.md, "Running it") | weekly (CI runs both) |
| Fuzzing | crash, hang, broken books (4); for `fuzz_store_replay` (validly chained journals with fuzz-chosen events): books that open must verify, and the viewer must accept exactly what the writer does | `fuzz/fuzz_targets/`, corpora in `fuzz/corpus/` | `just fuzz <target> <secs>` | 10 min each | weekly |
| Miri | undefined behavior in our use of dependencies (4) | the core's unit, bridge, invariant tests (the exhaustive calendar walk aside) | `just miri` | ~4 min | on demand |
| Golden | `tests/golden/fixture_company.json` (5) | `invariants.rs` | `just check` | <1s | every change |
@@ -984,8 +984,10 @@ the end.
`constructive_agrees`.
- **Proofs:** Kani `entry_flows_sum_to_cash`,
`entry_flows_pure_are_movements`, `entry_flows_are_bounded`,
`entry_flows_of_a_reversal_mirror`, `no_cash_no_flow` (entries of up to
three lines), `take_is_odd_and_bounded`, `apply_cash_mirrors`,
`no_cash_no_flow` (entries of up to three lines), the reversal's mirror
(`entry_movements_of_a_reversal_mirror` and `flows_of_mirrors` at three
lines, `entry_flows_of_a_reversal_mirror` end to end at two, since the
P3 surface review), `take_is_odd_and_bounded`, `apply_cash_mirrors`,
`apply_cash_spends_exactly`, `exchanged_is_bounded`,
`constructive_nets_to_zero`, `constructive_mirrors`; Lean
`cashflow_sections_sum_to_cash_change`, `entry_flows_sum`,
@@ -1015,7 +1017,8 @@ the end.
`on_behalf`, the kernel's received sign, `max` for `min`, the uses'
order reversed): 16 fail a test; one is equivalent today (pairing that
ignores `on_behalf`: a mirrored reversal's real and constructive parts
are each proved to mirror, Kani `entry_flows_of_a_reversal_mirror` and
are each proved to mirror, Kani `entry_movements_of_a_reversal_mirror`,
`flows_of_mirrors`, `entry_flows_of_a_reversal_mirror` and
`constructive_mirrors`, so the check is defensive, for P4's drafts).
The first round's 18 breaks, re-run on the reworked code (four
adapted where the code moved), and one more for the taxes class: 18
@@ -1436,7 +1439,7 @@ bound, the minor-unit plan check), `first_day`, `last_day`, `next`, `due`'s
zero months, `ScheduleConvention::parse`/`key`; `settings.rs` the new key
and `schedule_convention()`'s default; `store.rs` `run_depreciation`
(nothing due, the feature on a re-run), `live_runs`,
`depreciation_runs_ever`, `dispose_preview` (true-up split, `in_month`),
`Ledger::depreciation_runs` (was `depreciation_runs_ever`), `dispose_preview` (true-up split, `in_month`),
`check_nothing_after_disposal`, `disposal_lines`, `check_reverse_entry`
(the re-instatement room),
`register_schedule` (the feature only for daily), `ensure_feature`,
@@ -1690,6 +1693,68 @@ rules.md says why not. Mutation runs wait for P12 (D-33).
tax-basis K-1's book depreciation left it out (B-36); `books.list_assets`
as of a day said "disposed" for a later disposal (B-15 rule 8).
## P3: the surface review (2026-10-21)
The agent's surface, the docs and the whole proof run, on `main` with all
of P3 landed (first `2f999c6`, then rebased onto `2c5b5a7` with the
seams, asset basis and data safety reviews, and checked again). No
mutation run (D-33). What was run, in this
order, each alone (under `taskset -c 4-15`, four build jobs, on a laptop
other work kept at a load average near 30): `just check` (clean, before
and after the fixes); `just prove` as it stood (it ran past its
30-minute cap at four jobs, 52 of 53 harnesses verified: see
docs/verification.md, "Running it"); `just prove` and `just prove-slow`
after the split, until both passed; the viewer's screenshots on the demo
books; the privacy sweep below.
| What | Where | Oracle |
|---|---|---|
| Every tool the server has is documented in `docs/mcp-interface.md`, and every tool the doc documents exists (115; `books.access_log` had no entry) | `ledger-mcp` `server.rs` `every_tool_is_documented_and_every_documented_tool_exists` (with a check of its own reader) | the doc, read tool by tool against the code |
| The reversal's cash flow mirror at three lines, in two halves: a reversal's movements are its original's negated, and `kernel::flows_of` is odd; end to end at two lines | Kani `entry_movements_of_a_reversal_mirror`, `flows_of_mirrors`, `entry_flows_of_a_reversal_mirror` (`kernel::entry_flows` split into `entry_movements` and `flows_of`, behavior unchanged: every cash flow test, bridge and property passes) | docs/verification.md law 10; Lean `flows_reversal` |
| A closed-list value in a bookspec example (a 1099 form, a payment method's kind, a reason kind, a party totals basis) is spelled exactly as the README spells it; a K-1 query needs `cash_accounts` | `ledger-core` `tests/bookspec.rs` `a_form_in_another_case_is_refused`, `a_payment_method_kind_in_another_case_is_refused`, `a_reason_kind_in_another_case_is_refused`; `check_k1` | bookspec/README.md |
| Every Kani harness in `src/proofs.rs` runs in exactly one of `just prove` and `just prove-slow`, each run's count is checked, and a slow-list name that isn't a harness fails | `justfile` `_kani` | docs/verification.md, "Running it" |
**Docs corrected** (each against the code): `docs/mcp-interface.md`, about
60 fixes (parameter names: `search_entries`' `account_id`, `link_receipt`'s
`attachment_id`; outputs: `books.chart`, `books.digest`, `get_receipt`,
`run_recurring`, `label_asset`, `asset_schedule`, `dispose_asset`,
`import_bank_csv`; refusals: `reverse_entry`'s date rule,
`create_schedule`, `renumber_account`, `register_asset`, `correct_asset`,
`set_vendor_1099`, `set_entry_party`, `withdraw_attachment`; the
idempotent list (ten, not seven), the handbook's section list,
`books.start`'s next steps, the receipt policy's keys, the export reports
and formats, the CPA package's files, the close's targets, stale "out of
scope" text); tool and schema text in `server.rs` and `types.rs` where it
said the same wrong things; rules.md cross-references (eight wrong B-rule
numbers in code, tests and one example); bookspec's README (what the
loader takes, the statuses defined) and thirteen examples' statuses (an
example whose figures follow an owner decision is CHOICE and cites it);
docs/verification.md (laws numbered 1 to 31, no gaps; every harness listed
with its law, recipe and time); `README.md`'s tool count.
**Privacy sweep** (after the rebase onto `2c5b5a7`). `scrubber.originals_left`
over every tracked file (626, binaries decoded too): none in any text
file or fixture (the P2 served-books fixture's journal, database and
vault included); the six Inter font files match only a descending digit
run in their TrueType bytecode, a placeholder number the scrub's map also
holds, not a TIN from the books. The first company's people, places and bank, by name
(case-insensitive, LICENSE files aside): none. The eighteen figures from
its books, with and without thousands separators: none.
**The viewer** (the demo books, `cargo test -p ledger-gui --test screens`,
every page; PNGs kept outside the repo): the figures tie across pages.
Fixed: a report's notes were drawn on one line and cut (the fixed asset
register's explanation of its tax columns ended mid-word); they wrap over
the table's width now. The Accounts page listed each type's accounts in
the order they were added; it lists them by code, as the reports do.
**For P12 (mutation targets, none run):** `kernel::entry_movements` and
`kernel::flows_of` (the split of `entry_flows`; the same mutants as
before); the bookspec loader's exact-spelling filters and `check_k1`'s
`cash_accounts` check; `documented_tools` (its own sample checks it).
The earlier P3 sections' lists stand (one name corrected:
`Ledger::depreciation_runs`, which was `depreciation_runs_ever`).
## Benchmarks (baseline 2026-10-13)
No budget, by decision (the owner, 2026-10-13: the books do little work
+183 -82
View File
@@ -103,12 +103,19 @@ Each law, and what proves it. Kani harnesses are in
same identities at runtime and refuses (`CashFlowDoesNotFoot`) rather
than show a statement that doesn't foot.
- Kani `entry_flows_sum_to_cash`, `entry_flows_pure_are_movements`,
`entry_flows_are_bounded`, `entry_flows_of_a_reversal_mirror`,
`no_cash_no_flow` (end to end on entries of up to three lines, each
in any of the four places: four lines ran past half an hour per
harness; at three, the reversal's mirror is the slowest harness in
the suite, about 14 minutes on the development laptop, the others
seconds to 6 minutes), `take_is_odd_and_bounded`, `apply_cash_mirrors` (the
`entry_flows_are_bounded`, `no_cash_no_flow` (end to end on entries
of up to three lines, each in any of the four places: four lines
ran past half an hour per harness); the reversal's mirror at three
lines in two halves, `entry_movements_of_a_reversal_mirror` (a
reversal's cash and section movements are its original's negated)
and `flows_of_mirrors` (`kernel::flows_of` is odd, for any
movements three lines can make), `kernel::entry_flows` being the
one then the other, and end to end on up to two lines,
`entry_flows_of_a_reversal_mirror` (at three lines end to end it
took about 14 minutes alone, 2026-10-20, and past 30 minutes at
four jobs on a busy machine, 2026-10-21; split then, nothing
weakened: the halves cover every three-line entry the end-to-end
harness did), `take_is_odd_and_bounded`, `apply_cash_mirrors` (the
parts, for any amounts), `apply_cash_spends_exactly` (how a
section's cash splits among its kinds, operating's classes
included), `exchanged_is_bounded` (the
@@ -162,7 +169,7 @@ sequence of commands, of any length**:
commands adds, removes, or changes an entry dated in it, and it stays
closed.
- Lean `Step.frozen`, `Steps.frozen`
14a. **How the books are kept changes no entry**: in the model, a record
15. **How the books are kept changes no entry**: in the model, a record
(a setting, convention, decision, open item or approval; a vendor's
1099 decision or W-9 flag, a payment method, an entry's payment-method
tag) leaves the
@@ -193,7 +200,7 @@ sequence of commands, of any length**:
### Closing
15. **A closing line zeroes its account**, and **a closing entry balances
16. **A closing line zeroes its account**, and **a closing entry balances
and zeroes every revenue and expense account** through the period's
last day, whether the net goes to one account or is split among
members.
@@ -201,14 +208,22 @@ sequence of commands, of any length**:
`closing_entry_balances`
- Lean `closingEntry_net`, `closingEntry_zeroes`,
`closingEntry_accepted`, `close_zeroes`
16. **A close credits equity with exactly the net income**: revenue minus
17. **A close credits equity with exactly the net income**: revenue minus
expenses through the period's last day.
- Lean `net_closings_is_net_income`, `close_credits_net_income`
17. **An allocation's parts sum to the whole, to the unit**, however the
other shares were rounded, and only the last share changes.
- Kani `plug_last_sums_exactly` · Lean `plugLast_total`,
`plugLast_keeps`
18. **A run of member closes telescopes**: each member's total from a run
18. **An allocation's parts sum to the whole, to the unit**, however the
other shares were rounded, and only the last share changes; and the
largest-remainder apportionment (`kernel::apportion`, rules.md B-13)
succeeds exactly when the rounded-down parts are short of the whole by
less than a unit each, then sums to the whole, gives each part nothing
or one unit (one part may also take the sub-unit fraction), and never
gives a unit to a part while one with a strictly larger remainder goes
without (no member favored by position).
- Kani `plug_last_sums_exactly`, `apportion_is_exact`,
`apportion_is_fair` · Lean `plugLast_total`, `plugLast_keeps`,
`Apportion.ranks_perm`, `Apportion.apportion_sum`,
`Apportion.apportion_sum_of_shortfall` (any number of parts)
19. **A run of member closes telescopes**: each member's total from a run
of cumulative closes is their allocation of everything the run closed
(so its rounding is one allocation's, never a sum of them), and each
close hands out exactly its net, for any allocation whose parts sum to
@@ -217,56 +232,56 @@ sequence of commands, of any length**:
- Lean `runPart_telescopes`, `closePart_total` · property
`each_member_stays_within_a_cent_of_their_share`
The P3 odd cent (defect 17; rules.md B-13; D-10, D-38) adds three laws;
their numbers are assigned when P3 lands:
The P3 odd cent (defect 17; rules.md B-13; D-10, D-38) adds three laws:
- **A cumulative members close hands out exactly its net, and leaves each
member's running total one allocation of everything the count closed**,
whatever rule the count's earlier closes were made under, as long as
they handed out what they closed. The run rule is the special case where
they gave exactly the allocation. A refusal leaves the parts as they
were.
- Kani `apportion_cumulative_is_exact`,
`cumulative_closing_entry_balances` (the closing entry nets to zero
whatever the true-up)
- Lean `trueUp_total`, `trueUp_running`, `closePart_eq_trueUp`,
`Apportion.allocW_sum`, `Apportion.allocW_cumulative_total`
- properties `apportion_cumulative_agrees`,
`allocate_cumulative_agrees_with_the_integer_model`
- **Bounded drift: no member's running total is ever a minor unit from
their exact share.** With shares w_i / W (exact decimals) and every
amount a whole number of minor units (B-3), after every cumulative
close |(given_i + part_i)·W − T·w_i| < W, whatever earlier closes gave.
The precondition is the code's: `periods::allocate_cumulative` refuses a
product Decimal would round, so the remainders it ranks are exact, and
they sum to whole units (the Lean hypothesis of `rem_pos_of_rank_lt`).
- Kani `cumulative_close_never_drifts_a_unit_at_halves`, `_at_tenths`
and `_at_near_thirds` (two members at 50/50, three at 20/30/50%, three
at 33.33/33.33/33.34%; every running total, net and earlier split to
2^12 units)
- Lean `Apportion.rem_pos_of_rank_lt` (a member given a unit had a
positive remainder), `Apportion.apportion_drift` (one apportionment
is within a unit), `Apportion.allocW_drift` (the model of
`allocate_cumulative`, gain or loss), `cumulative_drift` (any
allocation within a unit stays so under the true-up),
`Apportion.allocW_cumulative_drift` (the two together): any number of
members, any amounts
- property `members_capital_never_drifts_a_cent_from_their_share`
- **A member's capital on a day never moves when its period is closed.**
The statement of members' equity gives each member, of the earnings not
yet closed, what the count's next close would give them; closing moves
those earnings into capital without changing the sum. When the count's
earlier closes gave something else (closes under an earlier rule), the
figure moves by exactly the true-up the close posts, never before.
- Lean `statement_after_close`, `statement_unmoved_by_close`
- property `each_members_capital_chains_and_holds_through_closes`;
`three_members_at_a_third_each_over_twelve_months` (every day of a
year, closed and never closed), the viewer's
`a_members_page_shows_the_same_capital_before_and_after_a_close`
20. **A cumulative members close hands out exactly its net, and leaves each
member's running total one allocation of everything the count closed**,
whatever rule the count's earlier closes were made under, as long as
they handed out what they closed. The run rule is the special case where
they gave exactly the allocation. A refusal leaves the parts as they
were.
- Kani `apportion_cumulative_is_exact`,
`cumulative_closing_entry_balances` (the closing entry nets to zero
whatever the true-up)
- Lean `trueUp_total`, `trueUp_running`, `closePart_eq_trueUp`,
`Apportion.allocW_sum`, `Apportion.allocW_cumulative_total`
- properties `apportion_cumulative_agrees`,
`allocate_cumulative_agrees_with_the_integer_model`
21. **Bounded drift: no member's running total is ever a minor unit from
their exact share.** With shares w_i / W (exact decimals) and every
amount a whole number of minor units (B-3), after every cumulative
close |(given_i + part_i)·W − T·w_i| < W, whatever earlier closes gave.
The precondition is the code's: `periods::allocate_cumulative` refuses a
product Decimal would round, so the remainders it ranks are exact, and
they sum to whole units (the Lean hypothesis of `rem_pos_of_rank_lt`).
- Kani `cumulative_close_never_drifts_a_unit_at_halves`,
`cumulative_close_never_drifts_a_unit_at_tenths` and
`cumulative_close_never_drifts_a_unit_at_near_thirds` (two members at 50/50, three at 20/30/50%, three
at 33.33/33.33/33.34%; every running total, net and earlier split to
2^12 units)
- Lean `Apportion.rem_pos_of_rank_lt` (a member given a unit had a
positive remainder), `Apportion.apportion_drift` (one apportionment
is within a unit), `Apportion.allocW_drift` (the model of
`allocate_cumulative`, gain or loss), `cumulative_drift` (any
allocation within a unit stays so under the true-up),
`Apportion.allocW_cumulative_drift` (the two together): any number of
members, any amounts
- property `members_capital_never_drifts_a_cent_from_their_share`
22. **A member's capital on a day never moves when its period is closed.**
The statement of members' equity gives each member, of the earnings not
yet closed, what the count's next close would give them; closing moves
those earnings into capital without changing the sum. When the count's
earlier closes gave something else (closes under an earlier rule), the
figure moves by exactly the true-up the close posts, never before.
- Lean `statement_after_close`, `statement_unmoved_by_close`
- property `each_members_capital_chains_and_holds_through_closes`;
`three_members_at_a_third_each_over_twelve_months` (every day of a
year, closed and never closed), the viewer's
`a_members_page_shows_the_same_capital_before_and_after_a_close`
### Parties
19. **A party's part of an entry is signed as its own account would be,
23. **A party's part of an entry is signed as its own account would be,
with a direction that is its sign**: paid exactly when what counts is
positive, received when negative, neither exactly when nothing counts;
the money and property in it each carry the whole's sign and never
@@ -275,7 +290,7 @@ their numbers are assigned when P3 lands:
property than it gave, the rest still owed (rules.md B-35).
- Kani `party_target_direction_is_its_sign` · bridge
`party_target_agrees`
20. **A reversal is the exact opposite of what it reverses**: the target
24. **A reversal is the exact opposite of what it reverses**: the target
is odd (negating the lines' capital movement, money and own-account
movement gives the original's reverse, but for the debits fallback and
a member's entry whose cash isn't exactly its money: an expense booked
@@ -292,7 +307,7 @@ their numbers are assigned when P3 lands:
### 1099 payments
21. **A payment to a vendor is split, never lost or counted twice**:
25. **A payment to a vendor is split, never lost or counted twice**:
each payment line goes in the bucket rules.md B-19's table names (its
16 cases: with no payment method naming its account, a financing line
never counts and a card account's line is always a card's); the
@@ -317,7 +332,7 @@ function, `kernel::prorate_by_days` (each month's share through
unit, `MONTH_L` = lcm(28, 29, 30, 31) = 377,580 parts of a month, so a day
of any month is a whole number of them.
22. **The parts of a span sum to its total exactly, none is negative, and
26. **The parts of a span sum to its total exactly, none is negative, and
the last is the total less the others** (plan/served.md P3 defect 9:
"parts sum to the total; the last part absorbs the remainder"). Every
split whose parts but the last fit in the span is computed; only parts
@@ -329,7 +344,7 @@ of any month is a whole number of them.
(a total in whole units is charged, so split, in whole units), from
`Prorate.prorate_total`, `Prorate.prorate_nonneg`,
`Prorate.parts_telescope` · bridge `prorate_agrees_at_i64_and_i128`
23. **Each part but the last is its days times the monthly rate within one
27. **Each part but the last is its days times the monthly rate within one
minor unit** (P3 defect 9: "each part equals days × rate within 0.01",
stated for any currency's minor unit). Each cumulative charge is its
exact share rounded half up to a multiple of the unit (a tie goes up;
@@ -344,7 +359,7 @@ of any month is a whole number of them.
`the_last_months_charge_in_tenths_is_within_half_a_cent` · Lean
`Prorate.roundUnit_near`, `Prorate.cum_near`, `Prorate.part_near`,
`Prorate.part_within_a_unit` · bridge `prorated_through_agrees_at_i32_and_i128`
24. **The charge through a day never falls, never exceeds the total, is
28. **The charge through a day never falls, never exceeds the total, is
continuous where one month ends and the next begins, and is the total
on the last day**; malformed input (a negative total, no months, a
unit that isn't positive, a part no month has, a day past the part, a
@@ -361,7 +376,7 @@ of any month is a whole number of them.
above the total, the total on the last day),
`prorated_through_refuses_malformed_input` · property
`prop_depreciation_is_monotone_and_bounded` (the engine)
25. **A catch-up run never carries the books past the schedule** at a
29. **A catch-up run never carries the books past the schedule** at a
month end already posted, and posts everything due through its month
when no run comes after it (plan/served.md P3, settled in design: a
month skipped is caught up in the month the run is made); every
@@ -377,7 +392,7 @@ of any month is a whole number of them.
· property `prop_run_amounts_are_each_months_run_amount`
· store `a_run_reversed_after_its_month_is_caught_up_in_the_reversals_month`,
`reinstating_a_run_a_later_run_caught_up_is_refused`
26. **A disposal's true-up reaches the schedule** through its day from what
30. **A disposal's true-up reaches the schedule** through its day from what
the books carry, up or down, never both (`true_up` is `columns` of the
gap).
- Kani `true_up_reaches_the_target` · bridge `catch_up_and_true_up_agree`
@@ -424,7 +439,7 @@ unit tests check the rest.
The register's tax figures beside book value (rules.md B-25) come from
one kernel function, `kernel::basis_figures`, for every rule:
27. **Each figure follows from the rule and the facts, and none
31. **Each figure follows from the rule and the facts, and none
overflows**: a purchase depreciates from its cost and has no §723 basis
or built-in gain; a contribution's §723 basis is the member's adjusted
basis and its built-in gain the value less it (a loss exactly when the
@@ -575,7 +590,8 @@ the Decimal exactness check in `allocate_cumulative`, and the journal's
old-books fixture `x_member_closes` (docs/testing.md, "P3: odd cents").
Kani times, each harness alone on a busy laptop (Kani 0.68, 2026-10-20):
`apportion_cumulative_is_exact` 257 s, `cumulative_close_never_drifts_a_unit_at_halves`
17 s, `_at_tenths` 320 s, `_at_near_thirds` 449 s,
17 s, `cumulative_close_never_drifts_a_unit_at_tenths` 320 s,
`cumulative_close_never_drifts_a_unit_at_near_thirds` 449 s,
`cumulative_closing_entry_balances` 202 s (`apportion_is_exact`, for
scale: 121 s). Two shapes were too slow and were changed, not weakened in
what they prove: restating apportion's postconditions on the cumulative
@@ -590,18 +606,103 @@ Lean carrying any weights.
```sh
mise install # once: Kani, Lean, and the rest, pinned in mise.toml
just prove
just prove # 47 Kani harnesses and Lean
just prove-slow # the other 8: the justfile's kani_slow
just prove-all # both
```
`just prove` runs every Kani harness, in parallel (`-j $(nproc)`, under
a 30-minute timeout), builds the Lean proofs (seconds),
and fails if any headline theorem depends on anything beyond Lean's
standard axioms (`propext`, `Quot.sound`, `Classical.choice`). In
particular, `sorryAx` fails it. Its time is the slowest harness's:
`entry_flows_of_a_reversal_mirror`, about 14 minutes on the development
laptop (8 cores); the 20 harnesses before P3 take about 17 minutes of
CPU in all (`accounting_equation` the longest, about 4), the cash flow's
other ten about 17 (`constructive_mirrors` about 2.5 minutes,
`constructive_nets_to_zero` about 1.5), so the whole run is about 15 to
20 minutes. Kani
downloads its pinned nightly and CBMC into `~/.kani` on first use.
`just prove` runs every Kani harness but the justfile's `kani_slow`, in
parallel (`-j $(nproc)`), the whole run under a 30-minute timeout; builds
the Lean proofs (seconds); and fails if any headline theorem depends on
anything beyond Lean's standard axioms (`propext`, `Quot.sound`,
`Classical.choice`). In particular, `sorryAx` fails it. `just prove-slow`
runs the slow harnesses, `$(nproc)` at a time, each under its own
30-minute timeout. Both fail if a harness they ask for isn't verified (the
count is checked), if any cover is unsatisfiable (a vacuous proof), or if
`kani_slow` names a harness `src/proofs.rs` doesn't have; a new harness
runs in `prove` until it is listed as slow. CI's weekly run does both, on
two runners (`.github/workflows/scheduled.yml`). Kani downloads its
pinned nightly and CBMC into `~/.kani` on first use.
**Why the split (2026-10-21, the P3 surface review).** With all of P3 the
suite is 55 harnesses. One `just prove` at four jobs (a CI runner's
cores), on the development laptop under heavy load from other work (a
load average near 30 on 16 cores, the run held to 12), could not finish
inside its 30 minutes: 52 of the 53 harnesses of the time were verified,
all of them proved, and the last, `entry_flows_of_a_reversal_mirror` at
three lines, was still running; alone, under the same load, it passed 30
minutes too. A run is only as safe as its slowest harness under the
machine it gets, so: that harness's claim at three lines is now proved in
two halves (`entry_movements_of_a_reversal_mirror`, `flows_of_mirrors`;
law 10) that take a minute or two each, and the end-to-end harness runs
at two lines; the eight harnesses that take the longest run in `just
prove-slow`, each capped on its own. Nothing was dropped.
Measured, four jobs: under that load, `just prove` 1,450 s (48 harnesses
then) and `just prove-slow` 1,365 s (seven then; `entry_flows_sum_to_cash`
1,228 s); on the same laptop quiet (2026-10-22, the final configuration),
`just prove` 308 s (47 harnesses, about 19 minutes of CPU, and Lean's 83
headline theorems) and `just prove-slow` 467 s (8 harnesses, the slowest
`entry_flows_sum_to_cash` at 301 s).
Every harness, the law it proves, the recipe that runs it, and its time
in that quiet run (`prove`: each harness's verification time; `prove-slow`:
each harness's whole run, its compile of about 50 s included):
| Harness | Law | Recipe | Seconds |
|---|---|---|---:|
| `flip_negates` | 5 | prove | 0 |
| `sums_are_exact` | 4 | prove-slow | 82 |
| `check_lines_is_exact` | 1 | prove | 7 |
| `out_of_range_is_rejected` | 1 | prove | 0 |
| `accepted_entry_nets_to_zero` | 2 | prove | 15 |
| `movements_are_per_account` | 3 | prove | 12 |
| `movements_sum_to_the_net` | 3 | prove | 58 |
| `reversal_mirrors` | 5 | prove | 25 |
| `reversal_cancels` | 5 | prove | 69 |
| `normal_balance_is_signed` | 8 | prove | 0 |
| `accounting_equation` | 8 | prove-slow | 176 |
| `normal_side_increases` | 6 | prove | 0 |
| `trial_balance_columns` | 7 | prove | 62 |
| `closing_line_zeroes` | 16 | prove | 0 |
| `credit_line_credits` | 16 | prove | 0 |
| `closing_entry_balances` | 16 | prove | 11 |
| `payment_bucket_is_the_table` | 25 | prove | 0 |
| `payment_split_is_exact` | 25 | prove | 18 |
| `net_buckets_keeps_the_total` | 25 | prove | 5 |
| `plug_last_sums_exactly` | 18 | prove | 4 |
| `apportion_is_exact` | 18 | prove | 56 |
| `apportion_is_fair` | 18 | prove | 18 |
| `apportion_cumulative_is_exact` | 20 | prove | 44 |
| `cumulative_close_never_drifts_a_unit_at_halves` | 21 | prove | 11 |
| `cumulative_close_never_drifts_a_unit_at_tenths` | 21 | prove | 149 |
| `cumulative_close_never_drifts_a_unit_at_near_thirds` | 21 | prove-slow | 216 |
| `cumulative_closing_entry_balances` | 20 | prove-slow | 150 |
| `take_is_odd_and_bounded` | 10 | prove | 1 |
| `apply_cash_mirrors` | 10 | prove | 12 |
| `entry_flows_sum_to_cash` | 10 | prove-slow | 301 |
| `entry_flows_pure_are_movements` | 10 | prove-slow | 151 |
| `entry_flows_are_bounded` | 10 | prove | 20 |
| `entry_flows_of_a_reversal_mirror` | 10 | prove | 36 |
| `entry_movements_of_a_reversal_mirror` | 10 | prove | 30 |
| `flows_of_mirrors` | 10 | prove | 56 |
| `no_cash_no_flow` | 10 | prove | 75 |
| `apply_cash_spends_exactly` | 10 | prove | 36 |
| `exchanged_is_bounded` | 10 | prove | 0 |
| `constructive_nets_to_zero` | 10 | prove | 26 |
| `constructive_mirrors` | 10 | prove | 52 |
| `party_target_direction_is_its_sign` | 23 | prove | 2 |
| `party_target_is_odd` | 24 | prove | 5 |
| `reverse_target_is_an_involution` | 24 | prove | 0 |
| `prorate_parts_sum_to_total` | 26 | prove-slow | 222 |
| `prorate_last_part_absorbs_the_remainder` | 26 | prove | 33 |
| `a_months_charge_is_within_half_a_cent` | 27 | prove | 48 |
| `a_months_charge_in_tenths_is_within_half_a_cent` | 27 | prove | 42 |
| `the_last_months_charge_is_within_half_a_cent` | 27 | prove-slow | 148 |
| `the_last_months_charge_in_tenths_is_within_half_a_cent` | 27 | prove | 36 |
| `prorated_through_refuses_malformed_input` | 28 | prove | 7 |
| `catch_up_never_overtakes_the_schedule` | 29 | prove | 8 |
| `catch_ups_is_every_suffixs_catch_up` | 29 | prove | 35 |
| `true_up_reaches_the_target` | 30 | prove | 0 |
| `basis_figures_laws` | 31 | prove | 0 |
| `kani_symbolic_copy_canary` | (the checker: "Trusting the checker") | prove | 3 |