Commit Graph
83 Commits
Author SHA1 Message Date
swaitsandClaude Opus 5.5 ae927c3f73 Attachments, statements, reconciliations, TIN vault, counterparty policy
check / check (push) Waiting to run
MCP: books.link_attachment, unlink_attachment, attachments; link_to on
attach_receipt and file_document; record_statement, statements;
books.reconcile previews and, confirmed against a recorded statement,
records the reconciliation; void_reconciliation, reconciliations;
import_bank_csv links its source file; digest lists unreconciled
accounts.

TIN vault: books.set_w9 takes the TIN and the form into the encrypted
vault (last four in the books); books.vendor_1099 include_tins and
save_csv_to for the CPA's copy; books.get_w9; books.purge_tin. A leak
test scans every tool output, the journal, the database, and the blobs.

[books] parties = off | suggest | require (default suggest): every
preview names the entry's party or suggests likely ones; require refuses
to post until it names one or is marked as having none (entry.no_party).

Viewer: documents on every subject page; statements and reconciliation
history per account; reconciliation pages; reconcile and
no-counterparty attention items; party money by period (paid out,
received in, net, what for, by month); Needs attention gets its own
full-width section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-15 15:42:37 -06:00
swaitsandClaude Opus 5.5 e265ed4339 Parties, attachment links, statements, reconciliations (store), TIN vault crate
check / check (push) Canceled after 0s
Parties: one directory for vendors, contractors, customers, employees,
members, platforms, agencies, banks, landlords (ledger-core parties.rs).
W-9/W-4 facts without the number (last four only; any nine-digit run in a
party field is refused, Treas. Reg. 301.6109-4). Vendor and customer
records link to parties on replay; core tags every bill, invoice, payment,
and reversal with its counterparty. Journal events party.*,
entry.party_set; payroll lines name the employee party.

MCP: books.add_party, update_party, set_party_roles, set_w9, set_w4,
parties, party, set_entry_party; draft_entry party; run_payroll employee;
show party. Viewer: Parties directory and party pages, search, journal
text, W-9 attention, demo data.

One definition of a party's money (ledger_core::parties::activity) for
MCP and viewer. 1099 amounts count payments from cash and financing
accounts (a member paying for the company counts; a card doesn't; a
prepayment counts when made): vendor_1099_report_with, B-19.

Store: attachment links to any subject, statements, reconciliations
(schema v11), with replay and projection. ledger-vault: encrypted TIN and
W-9 records (ChaCha20-Poly1305, key file outside the books).

Spec-blind tests for parties (core, store, MCP, viewer) and attachments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-15 14:43:40 -06:00
swaitsandClaude Opus 5.5 6e6f9d65b2 Fixed assets carry federal tax basis beside book value; tests sweep stale temp dirs
check / check (push) Canceled after 0s
- core: TaxBasisRule (cost IRC 1012, carryover IRC 723, personal-use conversion Treas. Reg. 1.168(i)-4(b)), TaxBasis with the contributor's adjusted basis, Asset::tax_basis and ::built_in_gain (IRC 704(c)); the register gains tax rule, contributor basis, tax basis, built-in gain, total tax basis, missing count (rules.md B-25; sources vendored in docs/spec/sources/irs-partnership-basis.md)
- store: asset.tax_basis_set journaled, replayed, projected (schema v10)
- mcp: books.set_asset_tax_basis; register_asset takes tax_basis_rule and contributor_basis, checked before anything registers (found by the spec-blind suite); list_assets and the register export (json, csv) carry tax basis; instructions say to record it for every asset
- import: the register's XLSX and PDF gain the federal tax basis table with citations
- viewer: tax basis on the Assets page, the asset page (a Tax basis card), the report, the journal, and Needs attention for assets without one
- just: check and the mutation recipes sweep this user's test book dirs older than an hour (killed runs had left 23 GB in RAM-backed /tmp)
- docs: the parties, attachments, TIN vault, completeness engine, and updates designs (docs/parties-and-attachments.md)
- tests: spec-blind tax basis suites in core, store, MCP; register PDF/XLSX and viewer tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-15 13:30:09 -06:00
swaitsandClaude Opus 5.5 ee7e205f5c Fixed assets, fully featured: tags, serials, acquisition links, label sheets, the fixed asset register, and an Assets section in the viewer; fix a flaky verify
check / check (push) Canceled after 0s
- core: asset tags (generated PREFIX-0001 or the human's own; unique ignoring case; never reused by another asset) and serial numbers (rules.md B-24); a journaled link from an asset to the entry that acquired it (posted, unreversed, debiting the cost account); fixed_asset_register(as_of), reconciled to the GL (unposted depreciation, unregistered cost)
- store: asset.tagged, asset.serial_set, asset.acquisition_linked journaled, replayed, projected (schema v9); differs_from now also compares control roles and tag history
- config: [books] asset_tag_prefix, default the company's initials without legal-form words
- import: Avery 5160/8160 label sheets with QR codes (geometry vendored from Avery, docs/spec/sources/avery-5160.md); the register as XLSX and PDF
- mcp: register_asset tags (and links) automatically; books.label_asset, books.asset_labels; asset tools take a tag; books.show asset / assets page / register report; export fixed_asset_register (json, csv, xlsx, pdf) and in the CPA package
- viewer: Assets page (register, reconciliation), asset page (identity, schedule posted vs not, entries, documents), Fixed asset register report, global search by tag/serial/description, journal text, needs-attention items
- fix: verify compared snapshot amounts as strings; a zero total summed in hash-map order could print "0" or "0.00", so agreeing books failed verify at random. Now by value (regression test)
- tests: spec-blind suites for tags, register, prefix, store, MCP; label and register PDF/XLSX tests; viewer tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-15 12:18:00 -06:00
swaitsandClaude Opus 5.5 767fd469cd testing: mutation scores for control roles and settings reload
check / check (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-15 10:43:00 -06:00
swaitsandClaude Opus 5.5 1771d87e0e Control roles in the journal (books.assign_role, books.roles); the MCP reloads wanderbooks.toml on change and reports settings problems (books.settings, digest); new writes a full commented wanderbooks.toml
check / check (push) Canceled after 0s
- core: ControlAccount keys, Ledger::assign_role/check_assign_role/role_account (rules.md B-23): one account per role, of the role's type, balance sheet roles move only when empty; invoices, bills, payroll, remittance, and closes post to the assigned account; an assigned role frees its default code for renumbering
- store: role.assigned journaled and replayed (strict account_id)
- mcp: settings.rs reloads by content at the start of each request, keeps the last good settings on a broken edit; books.settings with chart problems; digest settings_problems; instructions say when to edit the toml
- viewer: journal text for role.assigned
- cli: full example wanderbooks.toml, tested to load with every example uncommented
- tests: spec_roles.rs (25, spec-blind), store roles.rs (6), MCP reload/problems/roles, settings unit tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-15 10:30:16 -06:00
swaitsandClaude Opus 5.5 efa370dfa3 mcp: tests for the settings beside the books, year earnings (open periods, last year's partial close), year 1, a failed flush; drop by-class P&L's dead placeholder guard; record config, MCP, GUI mutation scores
check / check (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-14 20:45:49 -06:00
swaitsandClaude Opus 5.5 9a1f009f98 spec: ASC paragraphs read directly (360, 340, 210, 230, 606); all confirm the rules
check / check (push) Canceled after 0s
Read in the ASC basic view and vendored as short quotes with paragraph
numbers (docs/spec/sources/fasb-asc.md). Depreciation: any systematic and
rational allocation of cost less salvage (360-10-35-4); prepaids are
current assets with no prescribed expensing method (340-10-05-4,
210-10-45-1/2); cash flows gross and classified three ways, owners'
advances financing (230-10-45-7/10/14(b)/15(b)); a principal's revenue
is gross (606-10-55-37B). Citation fixed: 230-10-45-14(b), not (2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-14 14:11:59 -06:00
swaitsandClaude Opus 5.5 1291547fc4 lean: apportion's parts sum to the whole for any number of parts
check / check (push) Canceled after 0s
The ranks of n parts are a permutation of 0..n-1 (being ahead is a strict
total order; pigeonhole by hand, core Lean has no subperm), so exactly
u parts get a unit and one gets the fraction. apportion_sum and
apportion_sum_of_shortfall; the audit shows only the standard axioms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-14 10:17:37 -06:00
swaitsandClaude Opus 5.5 3f20df9700 config, mcp, gui: specs from sources, spec-blind suites, and the bugs they found
check / check (push) Canceled after 0s
ledger-config: parse gets a sourced spec. A fiscal year starts on the
first of a month (IRS Pub. 538); tax_classification is read by whole
words (Treas. Reg. 301.7701-3, Schedule C instructions; "business corp"
no longer reads as an S corp, "corporation or S corporation" is refused);
duplicate or blank members and shared capital accounts are refused; a
negative or non-grammatical receipt threshold is refused; codes trimmed.
tests/spec_config.rs (65 spec-blind tests, 14 properties), fuzz target
fuzz_config_parse (weekly). Mutation: 55 of 55 viable caught (from 25/38).

The receipt policy moves to ledger-config, with one loader
(effective_receipt_policy) and one decimal grammar, so the server and
the viewer enforce and show the same rule (the viewer had its own: only
expense-like debits, a silent $25 default, and no config.json).

ledger-mcp: a module spec (MCP 2025-06-18 tools, JSON-RPC 2.0, the
operating contract). Amounts in are exactly -?digits[.digits]; a number
that is one account's code and another's id (vendors, classes alike) is
refused, not guessed; the threshold is validated at startup. Fixed:
add_line with a bad class journaled the line before refusing;
profit_and_loss_by_class ignored `class`; aging buckets said "0". A wire
in front of rmcp answers malformed JSON with -32700 and invalid requests
with -32600 and their id (rmcp dropped the first, omitted the id).
tests/spec_mcp.rs (26 spec-blind tests: every tool's result against its
own outputSchema, a random-call state machine checking the journal is
untouched by every refusal, amount grammar, previews, the receipt policy).

ledger-gui: money, accounting, and dollars never round (they rounded to
cents, banker's style); export file names avoid Windows device names,
trailing dots and spaces, empty names, and 255-byte overflow; specs on
attention, history, fuzzy search, and axis ticks.

ledger-core: the lifetime profit_and_loss is renamed unclosed_earnings
(docs/testing.md #3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-14 10:13:05 -06:00
swaitsandClaude Opus 5.5 5b8910a1cd books.renumber_account: give an account a new code, history kept
check / check (push) Canceled after 0s
Ledger::renumber_account changes only the code (entries name accounts by
id): refuses an unknown account, a blank or used code, and a control code
for an account of the wrong type or a placeholder. Journaled as
account.renumbered (with the previous code), replayed, projected, shown
in the viewer's journal. The MCP tool lists wanderbooks.toml settings
that still name the old code. The server instructions now warn that the
engine can't tell a same-type account with another purpose at a control
code (a hosting expense at payroll's 6100).

Tests: core property (only the code changes; trial balance equal), core
refusals, the store's command property, MCP end to end with restart.
Mutation on the new code: 8 of 8 viable caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-14 08:00:58 -06:00
swaitsandClaude Opus 5.5 ccec4e9439 import: XLSX layout spec and tests; mutation 53% -> one equivalent left
check / check (push) Canceled after 0s
report_to_xlsx gains INVARIANTS 4-6 (header block, right-aligned amount
headings, spacer and account spacing, footer one empty row down, rules
exactly where rule_above says, exact text grouped in threes), tested by
reading the workbook's styles, merges and column widths. The general
ledger's footer now sits one empty row down like the other sheets.

Also killed: IIF transliteration arms, tax summary's optional sections,
and day-equals-month bank dates. `just mutants-import`, weekly in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 23:01:07 -06:00
swaitsandClaude Opus 5.5 9f81fc0817 ci: weekly fuzz_import_csv; testing.md: import in progress
check / check (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 21:03:26 -06:00
swaitsandClaude Opus 5.5 ea180d1d8e tax summary: Schedule K items stated separately; XLSX never shows a wrong amount
check / check (push) Canceled after 0s
From the IRS form instructions (now quoted in docs/spec/sources/
irs-tax-lines.md):
- On Forms 1065 and 1120-S, Schedule K takes "only taxable portfolio
  interest", and charitable contributions are among the items the entity
  "must state separately". The tax summary counted both in ordinary
  income and deductions; they now go in TaxSummary::separately_stated,
  outside book_net and tax_net, and the report, MCP export, and viewer
  list them under "Separately stated to owners (Schedule K)".
- Schedule C: "Do not include charitable contributions". The category no
  longer applies to Schedule C (a sole proprietor's gift is personal, an
  owner draw).
- interest_income's note: interest earned in the business itself is
  ordinary income (other_income).

XLSX: a cell number is a binary double and Excel honors 15 significant
digits (Microsoft, "Excel specifications and limits"), so an amount over
$9,999,999,999,999.99 or finer than a cent is written as its exact text,
right-aligned, instead of a number Excel would show wrong. The old
conversion also fell back to 0.0 silently; gone. Property test: every
whole-cent amount up to 15 digits round-trips exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 20:57:00 -06:00
swaitsandClaude Opus 5.5 c72a762ef1 import: grounded in Intuit's specs; atomic chart and bank imports
check / check (push) Canceled after 0s
New vendored sources (docs/spec/sources/): Intuit's bank CSV upload and
IIF formats, ECMA-376 / Excel number storage, and the IRS form lines the
tax categories map to.

IIF export (rewritten to Intuit's IIF Import Kit):
- declares every account first (!ACCNT NAME, ACCNTTYPE, ACCNUM): an
  undeclared account is created in QuickBooks "as a new bank account";
  the MCP export types them (cash accounts BANK, the control accounts AR
  and AP, registered fixed assets FIXASSET, the rest by type);
- subaccounts by colon path, parents first; colons in a name replaced;
- ASCII only (IIF is ASCII text), dashes and quotes transliterated;
- the kit's 2007+ journal layout and M/D/YYYY dates; an entry using an
  undeclared account is refused.

Bank CSV:
- the 4-column layout is money out then money in by position, which
  matches Intuit's "Credit, Debit" and banks' "Debit, Credit" both (the
  doc's "Expense, Income" labels were wrong, the parsing was not);
- a slash date's order comes from the file (a part over 12 settles it)
  or the caller's new date_order, never a guess: Intuit recommends
  dd/mm/yyyy, US banks write mm/dd/yyyy;
- fractions of a cent and double negatives ("(-5)") are refused; the
  header is recognized on the first non-blank row.

Chart CSV: a blank code or name is refused with its row (found by the
new fuzz_import_csv target), and the core now refuses a blank account
code or name too (replay keeps older books).

MCP:
- books.import_chart_csv and books.import_bank_csv rehearse the whole
  file on a copy of the ledger first: a bad row anywhere refuses the
  file (before, a duplicate within the file, a parent after its child,
  or a date in a closed period left earlier rows behind);
- bank rules: the longest matching pattern wins (they arrive in a map,
  so "first match" changed from run to run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 20:47:41 -06:00
swaitsandClaude Opus 5.5 f1bdac4bca store: last mutation survivors killed; 98% with four equivalent left
check / check (push) Canceled after 0s
Final store mutation run: 357 caught + 1 timeout of 365 viable (from 82%
this morning). Three of its seven survivors now have tests:
- replay refuses a re-chained journal whose schedule posting differs
  from what it re-derives (check_replayed_entry guards that event);
- a disposal refused for an account's type says the type it needed;
- a database whose last pages are damaged while its settings still read
  fails the integrity check and is rebuilt (verified to fail with the
  check disabled: "database disk image is malformed").
The four left are equivalent: sort_value's two arms, OpenFlags | vs ^,
and writing the long form of a sweepless retained-earnings close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 19:59:37 -06:00
swaitsandClaude Opus 5.5 db00b74e82 ci: weekly store mutation run and replay fuzzing
check / check (push) Canceled after 0s
`just mutants-store` (64 random cases per mutant, temp books on a RAM
disk in CI) runs beside the core's weekly; fuzz_store_replay joins the
weekly fuzz matrix. docs/testing.md lists the spec-blind store suite,
the refusal property's converse law, and the replay fuzz oracles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 19:26:27 -06:00
swaitsandClaude Opus 5.5 4d8d5daf37 store: refusals leave the live books untouched; per-filing names; spec suite
check / check (push) Canceled after 0s
The refusal property gained the converse law (an accepted command
journals something; only batch runs may find nothing to do) and checks
the store's class and payroll listings. It found a schedule registered
with an out-of-range total whose first month was refused after its entry
was begun: the live ledger kept a draft the journal never had.

- Core: compound commands (post_schedule_month, reverse_entry,
  close_period_to, create_invoice, create_bill, receive_payment,
  pay_bill, run_payroll, remit_payroll_taxes) run inside
  Ledger::atomically, so a refusal partway leaves the ledger exactly as
  it was.
- Core: a schedule total no line could carry, or finer than a cent, and
  an asset cost beyond the line range, are refused at registration.
  Replay keeps older schedules (replay_register_schedule).
- Store: add_line, add_line_fx, mark_cleared, set_fx_rate,
  post_schedule_month and close_period_to changed the live ledger before
  journaling; they now run on a copy, journal what it produced, then
  swap it in, so a failed write leaves the live books as journaled.

New spec-blind suite tests/spec_store.rs (57 tests written from the
store's specs alone). Its two findings, adjudicated:
- A file filed twice under different names listed the first name and
  type for both (the projection kept them per blob). Schema v8 gives
  each attachment its own; older databases backfill and heal by verify.
- Withdrawn attachments stay in the store's listing, marked (the MCP
  listing hides them): the spec and doc said "omit" and are corrected.

Also removes journal::truncate_to_last_good_seq: unused, and it dropped
everything after the first bad complete line, which the books now treat
as corruption for a person, never repaired automatically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 19:24:34 -06:00
swaitsandClaude Opus 5.5 2cbb6a263f store: benchmarks at 10k entries on disk
check / check (push) Canceled after 0s
benches/store.rs (criterion, in `just bench`): open 249 ms, viewer load
616 ms, full verify 809 ms, post one entry (four synced appends) 62 ms.
The books are built on a temp dir and copied under target/ so every
sync hits a real disk. Baselines in docs/testing.md; no budget, by
decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 19:06:11 -06:00
swaitsandClaude Opus 5.5 fb3b4ab476 closes: a run of members closes splits cumulatively
check / check (push) Canceled after 0s
Each close into members' capital rounded on its own, ties to the
earlier member, so twelve monthly 50/50 closes of odd-cent income could
leave one member several cents ahead of half the year (docs/testing.md
#12, found with the members' equity fix).

- CloseTo::Members now splits cumulatively over a run (closes back to
  back with the same members and shares): each close gives a member
  allocate(run total so far) - allocate(run total before). Periods
  record their close order and a cumulative close's members and net
  (Period::close_seq, member_close) so the next close continues the run
  and replay recomputes it exactly. A close to anything else, or other
  shares, ends the run.
- Journaled "rounding": "cumulative". The per-close rule is kept as
  CloseTo::MembersPerClose for closes journaled "largest_remainder";
  older closes still replay with the last member absorbing it.
- Lean: runPart_telescopes (a member's total from a run is their
  allocation of the run's total) and closePart_total (each close hands
  out exactly its net), audited to the standard axioms.
- Tests: hand-computed three one-cent months (per close A 0.03 / B 0;
  cumulative A 0.02 / B 0.01), a run ending, a property that each member
  stays within a cent of their share (fails on the per-close rule), and
  a store contract that each rule journals and replays as itself.

Wanderland's books have no closes yet, so all of theirs use the new rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 18:58:21 -06:00
swaitsandClaude Opus 5.5 f4381f170c viewer: no false "database differs" while the books are being written
check / check (push) Canceled after 0s
New concurrency test: one thread writes entries, drafts and receipts as
fast as it can while a Snapshot (the viewer) refreshes in a loop. At
every refresh the snapshot's books must balance, never go backward, and
never see the writer's database as differing from the journal; at the
end it must equal the writer in full. It failed 30 runs in 50:
check_disk read the applied position and the tables in separate
statements, so a write landing between them made a consistent database
look like it "differs", which the viewer shows as a problem in Needs
attention.

- check_disk reads in one read transaction (one consistent view).
- A database that has applied lines the viewer hasn't read yet is now
  DiskCheck::Ahead ("viewer catching up"), not Behind.
- Without the transaction the test fails 14 runs in 20; with it, 50/50.

Also: members' equity spec 3b states the exact bound (a member's period
income is within two cents of their share, one cent of rounding at each
end; the running balance chains exactly), and the spec-blind property
checks that bound. docs/testing.md gains the concurrency layer and item
12: each close into members' capital still rounds on its own (pending).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 18:51:15 -06:00
swaitsandClaude Opus 5.5 37824e55d3 members' equity: split undistributed earnings cumulatively
check / check (push) Canceled after 0s
A capital account is one running balance (Form 1065 Schedule K-1
instructions, Item L: beginning and ending capital "based on the
partnership's books and records"), but the statement split prior
earnings and the period's income separately, so a member's ending on one
statement could differ by a cent from their beginning on the next.

Now each member's share of undistributed earnings is allocated as of a
date (largest remainder, as before): beginning as of the day before the
period, ending as of its last day. A member's period income is the
roll-forward's remainder (their share within a rounding cent); incomes
still sum to net income. New property
each_members_capital_chains_across_statements (random books, odd-cent
shares, closes to members or not, any split date) fails on the old code
and passes now. The hand-computed test is recomputed: the old split gave
two cents off an even 50/50 to date.

Also: rules.md B-13 quotes Item L; docs/testing.md #7 and #8 marked
changed; the CPA-question list is replaced by the rule that wanderbooks
settles questions from the sources and makes real choices per-company
settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 18:41:37 -06:00
swaitsandClaude Opus 5.5 819cd09e0d cash flow: financing and investing gross; account roles are a setting
check / check (push) Canceled after 0s
From ASC 230 (via Deloitte DART and PwC Viewpoint), replacing two
formerly assumed behaviors:

- Financing and investing are gross (230-10-45-7): per account, cash in
  and cash out are separate lines (contributions apart from
  distributions, borrowing apart from repayment), netted only within
  one entry. Lines come in code order, in before out.
- Investing includes making and collecting loans and buying investments
  (230-10-20, 45-13), not only registered fixed assets:
  CashFlowParams gains investing_accounts.
- The roles are per-company settings: [books] financing_accounts and
  investing_accounts in wanderbooks.toml (beside cash_accounts). The MCP
  tools (cash_flow, export, CPA package) default every list to them and
  share one path; the viewer reads them too, falling back to its name
  guess for debt. A due-to-owners liability listed as financing gives
  PwC 6.9.3.1's constructive treatment of expenses an owner paid.

rules.md B-14 quotes the sources; new tests cite them; docs/testing.md
#8 is resolved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 18:36:21 -06:00
swaitsandClaude Opus 5.5 08d1a9c230 docs: the assumed list, checked against the standards; mutation score
check / check (push) Canceled after 0s
Each formerly ASSUMED behavior is now CONFIRMED, CONVENTION, DESIGN, or
CHANGE (pending), with its source (Reg. S-X, ASC 230 via Deloitte DART,
IRS K-1 instructions, OpenStax). Two need engine work: members' equity
should allocate cumulatively so each member's beginning equals the prior
ending (#7), and the cash flow should show financing gross and classify
investing by account role (#8). Two questions for the CPA are listed.
The test doc comments that relied on them now cite the verdicts.

Core mutation run: 1,011 caught + 13 timeouts of 1,024 viable, 0 missed.
No benchmark budget, by decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 18:18:30 -06:00
swaitsandClaude Opus 5.5 0dcf865ff8 core: no entry posts a fraction of a cent; verify compares whole ledgers
check / check (push) Canceled after 0s
CI's run of the store refusal property found a payroll run that posted
$65.27794: the employer tax was passed in unrounded and nothing refused
it, though rules.md B-3 already says a ledger in one money holds no
fractions of its smallest unit. Sums of it then differed in scale
between journal replay and the SQLite read-back.

- Ledger::post_entry refuses any line finer than its currency's ISO 4217
  minor unit (FinerThanMinorUnit), whatever made the line: payroll,
  closes, disposals. add_line refuses it up front. Older journals replay
  as booked through replay_post_entry. Spec tests for the kernel's
  ten-place limit now run on the replay path.
- The property's body is now run_ops, so failing sequences become named
  regression tests (regression_payroll_with_a_fraction_of_a_cent). The
  generator rounds payroll tax to the cent, as callers must.
- Ledger derives PartialEq and gains differs_from; Store::verify compares
  the live ledger with the journal replay in full (drafts, documents,
  schedules, periods, settings), not just balances, and names the parts
  that differ. The property checks the ledger its commands built against
  that replay and against the reopened books.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 18:17:13 -06:00
swaitsandClaude Opus 5.5 4af2f6c0bb store: set aside a torn last journal line on open; server waits for lock
check / check (push) Canceled after 0s
A crash or power loss mid-append can leave a partial last line. The
writer acknowledges an append only after its whole line is synced, so
that line was never acknowledged: opening now saves those bytes to
journal.jsonl.torn.<time>, truncates to the last newline, and journals
journal.tail_repaired (replay and projection ignore it; the GUI shows
it; books.digest reports it in journal_repairs). Damage before the last
newline is still corruption and still refuses to open. Tested at every
byte of the last append.

The MCP server now waits up to ten seconds for the writer lock: a
restarted server (a client reconnecting) raced the exiting one and died
with Locked, which also made mcp_schedules_and_recurring_over_stdio
flaky. The test harness now waits for each server to exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 17:58:29 -06:00
swaitsandClaude Opus 5.5 c539004d1a test: fuzzing, Miri, benchmarks, CI, and a strategy note; the journal refuses floats
check / check (push) Canceled after 0s
Max-testing pass, ranks 4-6 and step 9 for ledger-core.

Fuzzing (rank 4): fuzz_op_sequence now drives balanced pairs, foreign
lines, periods, closes, and amounts from raw decimal bits, and asserts
the books stay valid (TB balances, posted entries validate, no draft in
a closed period); new fuzz_core_serde (every persisted core type
round-trips) and fuzz_store_open (a corrupt journal is an error, never a
panic); hangs count (-timeout=10). 54M executions; one crash:
- fuzz_journal_line: a number too big for an integer parsed as a float,
  and its canonical form (which the hash chain covers) re-canonicalized
  differently. The journal never holds floats (R-2), so the reader now
  refuses them (JournalLine::parse, used by every journal reader);
  regression a_journal_line_with_a_float_is_refused. Checked first: the
  real books' journal has none. Rerun: 13.2M executions, clean.
Seeds in fuzz/seeds/ (synthetic, minimal); the grown corpora stay out of
git. `just fuzz <target> <seconds>`.

Miri (rank 4): the core's unit, bridge, and invariant tests under Miri
(our code has no unsafe; rust_decimal and chrono do): no undefined
behavior. `just miri`. A pinned nightly (nightly-2026-10-01, with miri)
is now in mise.toml.

Benchmarks (rank 6): criterion (new dev-dependency: the playbook's Rust
benchmark tool; none existed) in benches/books.rs; baselines in
docs/testing.md. The store's per-command rehearsal clone costs ~1 ms at
10k entries. No budget yet, so nothing gates on them. `just bench`.

CI: .github/workflows/check.yml runs `just check` on every push;
scheduled.yml runs proofs, the GnuCash oracle, fuzzing, and mutation
weekly. Tools come from mise.toml.

docs/testing.md: every layer, its oracle rank, command, runtime, and
gate; benchmark baselines; the ASSUMED invariants for Stephen to reject;
the gap list with triggers. docs/oracle.md's fuzzing paragraph had
claimed persistent corpora that didn't exist; corrected.

Also: ledger-import now inherits the workspace lints (nightly cargo
warned).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 15:09:22 -06:00
swaitsandClaude Opus 5.5 c9810ea0a3 fix(core): payments never predate their documents; aging as of a date; refusals leave no draft
Three spec decisions from Stephen (2026-10-13), raised by the spec-blind
test writers, each test-first:

- A payment dated before its invoice or bill is refused
  (PaymentBeforeDocument); a real prepayment is a deposit. Test:
  a_payment_never_predates_its_document. Replay is unaffected (it goes
  through replay_invoice_payment / replay_bill_payment).
- Aging as of a date shows only documents issued by then, less only the
  payments made by then. The ledger now records each document's payment
  entries (live and on replay), and aging counts those dated by as_of.
  Test: aging_as_of_a_date_sees_only_what_existed_then; the A/R and A/P
  conservation properties' aging oracles updated to the confirmed rule
  (they had assumed aging ignored dates).
- A schedule through a mid-month date prorates that month by days; the
  code was right and its doc wrong. The spec-blind test that disagreed is
  rewritten to the confirmed spec and no longer ignored.

Also found while wiring payments: a payment to an unknown cash account
created the payment's draft and then failed, leaving it behind; the cash
account is now checked before anything is created.

The unreviewed golden van_schedule.json is gone: every row was first
checked against an independent hand computation (all 61 matched), then
replaced by van_schedule_is_the_hand_computed_one, which asserts that
computation directly (548.39, 59 x 1,000.00, 451.61; total 60,000.00).

Tests: 452. No spec-disagreements left ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 14:47:10 -06:00
swaitsandClaude Opus 5.5 beb1fcf233 test(core): spec-blind suites and a live GnuCash oracle; four fixes they found
Max-testing pass, steps 2-4.

Spec-blind tests (rank 1-2): four agents, each given only the specs and
the public interface (doc comments and signatures, every body removed,
never src/ or the existing tests), wrote tests/spec_reports.rs (62),
spec_ledger.rs (72), spec_arap.rs (42), spec_assets_payroll.rs (50):
hand-computed examples per scenario and a proptest law per invariant
(conservation, partition, invariance, scaling, reversal cancels,
"a refusal leaves the books unchanged" over random invalid commands).
They found:

1. A refused payroll run changed the books: an all-zero run passed
   validation, then failed at post after creating seven payroll
   accounts and using up an entry id. validate_payroll_lines now
   refuses a run with no gross pay.
2. A refused add_account used up an account id (empty code, duplicate,
   unknown parent). The id is now taken only when the account is added.
3. Stale prose: close_period_to and members_equity still described the
   old last-member rounding.
4. Open (left #[ignore]d pending Stephen): depreciation_schedule's doc
   says "through the month containing" the date; the code prorates that
   month by days.

GnuCash oracle (rank 3, an oracle never the spec): `just oracle` builds
GnuCash 5.10 in a container pinned by base digest and package versions
(oracle/gnucash/) and runs tests/oracle_gnucash.rs: 200 seeded random
books built through wanderbooks (entries, foreign lines, reversals,
monthly closes), every account's balance at month ends and random dates
and each month's P&L, compared exactly: 15,800 comparisons. First run:
4,724 differences, all from foreign lines; without them, zero.
Adjudicated from principles (rules.md B-3): wanderbooks booked
foreign * rate unrounded ($108.34567); a ledger in one money holds no
fractions of its smallest unit, and its cash must match a bank. Stephen
decided: book in the base currency's minor unit, half away from zero.
add_line_fx now rounds; minor units per ISO 4217 list one (2026-09-17,
SIX), generated into Currency::minor_units and recorded in
docs/spec/sources/iso-4217.md. Replay now takes the journaled base
amount (replay_add_line_fx), so lines booked before replay exactly as
booked (contract: books_written_under_older_rules_still_open). Now:
zero differences, foreign lines included.

Tests: 449, from 223.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 14:39:06 -06:00
swaitsandClaude Opus 5.5 925c3ba523 fix(core,store): child accounts share their parent's type; periods nest; lines checked when added
Three spec decisions from Stephen (2026-10-13), each test-first:

- A child account has its parent's type (ParentTypeMismatch): a child
  subdivides its parent. Test: a_child_account_has_its_parents_type.
- Periods nest (a month in a quarter in a year) but never partially
  overlap (PeriodsOverlap; a refusal uses no id). Test:
  periods_nest_but_never_partially_overlap.
- A line's account must exist and take postings when the line is added,
  not only at post (UnknownAccount / PlaceholderAccount from add_line
  and add_line_fx); posting still re-checks. Two older tests that pinned
  the post-time refusal now check both (the post-time check is reached
  through the unchecked replay path, as old journals can).

Compatibility: replay reads history through new Ledger::replay_*
commands (add_account, add_line, add_line_fx, create_period) that skip
checks added later, so books written before these rules still open.
The contract test books_written_under_older_rules_still_open writes such
a journal directly and found, on its first run, that the SQLite
cross-check rebuild used the checked commands and would have failed
verification ("sqlite projection can't be read back"); it now replays
too. Documented in docs/spec/README.md; decisions recorded in rules.md
(B-1, B-4, B-12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 14:16:51 -06:00
swaitsandClaude Opus 5.5 df38dcccf9 feat(core,mcp,gui): never apply a legal rule past what wanderbooks knows
Stephen (2026-10-13): when what wanderbooks knows is stale, check the
date, research it or ask the owner or CPA, and update the rule for
everyone with a pull request (best) or an issue (worst).

- ledger_core::rules: a registry of rules set by law that change over
  time. Each says the years it's known for, its rules.md entry, its
  vendored official source, and a review date. stale_rules(today) is
  pure; the engine has no clock.
- The 1099 threshold is the first: $600 through 2025, $2,000 in 2026,
  then indexed for inflation (26 U.S.C. §6041(h)). default_threshold
  and vendor_1099_report now return Result; for 2027+ they refuse with
  LedgerError::RuleNotKnown, whose message says what to do: get the
  figure from the official source or the owner/CPA, pass it explicitly,
  then open a PR (or issue) at github.com/swaits/wanderbooks. Before,
  2027+ silently used $2,000.
- MCP: books.vendor_1099 relays that refusal; books.digest gains
  stale_rules; the server instructions tell the agent the procedure (and
  how to handle control-account conflicts: explain, never renumber).
  The CPA package writes a NOT-INCLUDED note instead of a guessed 1099
  worksheet.
- Viewer: "Needs attention" shows each stale rule with a ready prompt
  for the agent; the 1099 page explains instead of guessing.

Review date for the 1099 rule: 2026-11-01, when the IRS usually
publishes next year's inflation adjustments (CONVENTION, marked as such
in the registry).

Tests: an_unknown_year_threshold_is_never_guessed,
dated_rules_go_stale_on_their_review_date (dates passed in, so no clock
in the test), MCP stdio: 2027 refused with guidance, explicit threshold
accepted, digest carries stale_rules (asserted date-independently).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 14:07:22 -06:00
swaitsandClaude Opus 5.5 ad572ae89d docs(spec): every rule traced to an authority; public-domain sources vendored
Stephen asked that specs come from recognized documents, not from
anyone's memory. There is no single US bookkeeping bible; authority is
layered, and this records it:

- docs/spec/sources/: verbatim excerpts (whitespace normalized), each
  with URL, retrieval date, and the SHA-256 of the fetched file, of
  public-domain texts: Pacioli's 1494 treatise in Geijsbeek's 1914
  translation (double entry, the trial balance, one money, write only
  forward, correct by opposite entry, close into capital); IRS Pub. 583,
  538, 946, 15; the 1099-MISC/NEC instructions (2025 and 2026); 26 U.S.C.
  §6041, §6041A, §704; Treas. Reg. §1.263(a)-1(f) and -4(f); Notice
  2015-82; Rev. Proc. 98-25; Form 1065 instructions; 17 U.S.C. §105. The
  excerpts were extracted by matching each passage against the fetched
  file, so none can contain text the source doesn't.
- docs/spec/rules.md: the rule catalog, B-1 to B-22. Each rule says
  AUTHORITY, CONVENTION, or DECISION; cites its sources (FASB's
  Codification and Concepts Statement 8 and OpenStax quoted briefly,
  never copied: copyright and CC BY-NC-SA); and names the code that
  enforces it and the tests and proofs that check it.
- Spec blocks in the code now carry SOURCE: lines into the catalog;
  AGENTS.md makes sourcing rule 0 of the formal-methods bar.

Corrections the sources turned up: the de minimis limit is $500 in the
regulation and $2,500 by Notice 2015-82 (both now cited); 1099
thresholds after 2026 are inflation-indexed, so the engine's flat
$2,000 for 2027+ is an open question (B-19). Unverified ASC paragraphs
(360-10-35-4, 340-10, 230-10-45-10, 606-10-55-37B, 210) are listed in
docs/spec/README.md until someone with a FASB basic-view login reads
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 12:18:57 -06:00
swaitsandClaude Opus 5.5 0340f316ee fix(core): due-today is Current; odd cents split by largest remainder
Max-testing pass, step 2: two spec decisions Stephen confirmed
(2026-10-13), each test-first.

Aging (arap::aging_bucket): a document due today was "1-30 days past
due"; it is Current, since past due starts the day after (QuickBooks'
convention). Spec in the doc comment. Boundary examples at every edge
(due today, 1, 30, 31, 60, 61, 90, 91 days) fail on the old code. A
property: the balance lands in exactly one bucket, and time only ages
it.

Member splits (periods::allocate): the last member absorbed every odd
cent, so the same partner always got or lost it. Now largest remainder:
each share rounded toward zero to the cent, leftover cents to the
largest remainders, ties to the earlier member, a loss the negation of
the gain. The core is a new kernel function, kernel::apportion,
allocation-free so Kani proves it (n <= 3): apportion_is_exact (it
succeeds exactly when the shortfall is under a unit per part; then the
parts sum to the whole, each gains 0 or 1 unit, one may take the
sub-unit fraction) and apportion_is_fair (never a unit to a part while
a larger remainder goes without). Kani found an off-by-one in the first
version: a shortfall of n units plus a fraction was accepted. Native
regression apportion_refuses_a_shortfall_of_a_unit_per_part_or_more.
Properties at Decimal (2,000 cases, up to 5 members): conservation,
within a cent of each exact share, fairness, sign symmetry; they fail on
the old rule. Bridge: apportion agrees on i64 and Decimal.

Contract: closes journaled before this change replay exactly. New
member closes journal "rounding": "largest_remainder"; journal lines
without it replay as CloseTo::MembersLastAbsorbs, the old rule
(periods::allocate_last_absorbs, pinned by
closes_journaled_under_the_old_rule_replay_with_it). The members'
equity statement isn't journaled and uses the new rule.

Open gap: Lean proof of apportion's sum law for any n (needs a
permutation lemma on the ranking); Kani covers n <= 3, properties n <= 5.
The Lean closing theorems already hold for any split summing to the
net.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 12:10:55 -06:00
swaitsandClaude Opus 5.5 3609bf065e feat(core): prove the accounting laws (Kani + Lean); fix three engine gaps
The money arithmetic every invariant rests on now lives in one kernel,
ledger-core/src/kernel.rs: signing a line, totals, net, per-account
movements, reversal, normal balances, trial-balance columns, closing
lines, and the rounding plug for member splits. It is generic over a
checked `Amount` trait. The engine runs it on Decimal (validate,
movements, reverse_entry, account_balance, close_period_to, allocate,
trial_balance): there is one copy of the arithmetic, and it is the one
under proof.

Three layers, docs/verification.md maps every law to its proofs:

- Kani (src/proofs.rs): 17 harnesses prove the kernel at i64 for every
  input up to 4 lines over 3 accounts. An entry is accepted exactly
  when it is two or more positive, in-range lines whose debits equal
  credits; accepted entries net to zero; grouping by account loses
  nothing; a reversal mirrors and cancels its original; trial-balance
  columns balance iff the books net to zero; closing lines zero their
  accounts and a close balances however the net is split; the plug
  makes parts sum to the whole. No panics, no overflow. kani::cover!
  shows the proofs are not vacuous.
- Lean 4 (verify/lean): 24 headline theorems for books of any size. The
  ledger is a state machine (add account, draft, post, reverse, open,
  close), and in every reachable state the books net to zero, the trial
  balance balances (as of any date), and the accounting equation holds.
  Posting only appends ("posted is forever"), closed periods are
  frozen, a close zeroes revenue and expenses and credits exactly the net
  income, and period statements add up. Audit.lean shows standard axioms
  only, no sorry.
- Bridge (tests/kernel_bridge.rs): the Decimal and i64 kernels agree.

What the proofs found (each one missed by every test, fuzzer, and
mutant):

- The engine accepted negative line amounts; only MCP refused them. Now
  NegativeAmountLine: direction is the side's job.
- Summing lines near Decimal::MAX panicked. Lines are now capped at one
  quadrillion and ten decimal places (AmountOutOfRange), which keeps
  every realistic sum exact, and the kernel's sums are checked.
- FX conversion (foreign_amount * fx_rate) could panic the same way; it
  is now a checked multiply.
- Kani itself: 0.68 mis-models symbolic-length copy_from_slice
  (model-checking/kani#4904) and reported a reversal that doesn't net to
  zero. The counterexample, replayed natively, passes, and a 15-line
  standalone crate reproduces the bug. Harnesses avoid bulk copies, and
  a should_panic canary will flag the upstream fix.

`just prove` runs it all (Kani memory- and time-capped, fails on an
unsatisfiable cover or a non-standard Lean axiom). AGENTS.md now makes
formal coverage part of every core change.

Also, docs: GnuCash is *an* oracle, never *the* oracle (Stephen). The
spec is accounting principles, proved. Golden files are recomputed from
principles. External implementations (GnuCash, the old Sheet, QBO/Xero)
are cross-checks whose disagreements get adjudicated, never obeyed.
docs/oracle.md gains "Who is right"; AGENTS, README, architecture,
roadmap, parity, and STATUS are reworded to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
2026-10-13 11:12:13 -06:00
swaitsandClaude Opus 5.5 475d08b28e feat: books.show — the agent puts things on the viewer's screen
books.show asks the viewer to open an entry, an account register, a
report (dates, month/quarter/year columns, prior-period or prior-year
comparison), a document, a search, or a page. The request lives outside
the books (runtime dir, keyed by data dir, written atomically); the
viewer watches it beside the journal and applies books before
navigating. 66 tools.

Site: the dashboard is the hero again; 'Here's what using it looks like'
shows the session beside the viewer, ending in books.show; the cash page
replaces the repeated overview lower down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-12 19:03:19 -06:00
swaitsandClaude Opus 5.5 9694d2a89a feat: end-to-end integrity checks with self-healing; website; installer and Windows fixes
Store::verify compares the whole SQLite projection, every table row for
row, against a fresh projection of the journal; verify_and_heal rebuilds a
drifted database (keeping the old file) at every server start and via the
new books.verify tool, which also re-hashes every blob. The GUI checks the
server's database read-only and every stored file, and shows the result.
rebuild() also drops stale WAL/SHM files.

site/: the wanderbooks website (just site, just publish). install.sh:
OS detection (Windows points to WSL2), C compiler and git checks,
WANDERBOOKS_REPO. Windows: no directory fsync off Unix; .exe-aware lookup.
Repo justfile: check, build, install, demo, site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-12 17:05:05 -07:00
swaitsandClaude Opus 5.5 105fd52694 feat: ledger-gui — a read-only, live desktop view of the books
egui/wgpu app for owners and their accountant: Overview, Cash, Profit,
Transactions (with rendered receipts and journal history), Reports
(statements with interval columns, comparisons, drill-down), Accounts,
Documents, Company, Journal; Ctrl+K palette; back/forward; light/dark.
Follows the journal live (~30 ms from write to screen). --demo opens a
deterministic demo company, also used for screenshot tests.

Engine: ledger-store Snapshot (read-only, incremental, tolerant of a
half-written tail); ledger-core analysis (intervals, balance series,
P&L by interval, cash flows); ledger-config extracted from ledger-mcp
with the full company profile and cash_accounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-12 16:20:14 -07:00
swaitsandClaude Opus 5.5 b48addd0e0 feat: 1099 tracking — vendor forms, payment tags, year-end worksheet
Vendors carry form_1099 (NEC/MISC) and tin_on_file (never the TIN).
Entries carry the vendor they paid; bills, bill payments, and reversals
are tagged automatically. books.vendor_1099 counts cash paid per vendor
per calendar year (card payments excluded) against $600 through 2025 and
$2,000 from 2026, or an override, and flags missing W-9s. Four new tools
(64), two journal events, schema v7. Docs: finished R-FUTURE work is no
longer listed as future.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 21:18:32 -07:00
swaitsandClaude Opus 5.5 d65f9aa840 feat: payout gross-up (books.record_payout)
A platform deposit becomes a draft entry with the deposit, fees,
adjustments, and gross revenue (by class), refused unless gross - fees
+/- adjustments equals the deposit to the cent. The payout statement is
attached as its receipt before posting. 60 tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 21:12:38 -07:00
swaitsandClaude Opus 5.5 ddb67a2ca2 feat: prepaid/deferred amortization schedules and recurring entries
Amortization schedules spread a prepaid expense or deferred revenue over
the months it covers, to the cent, posting on month ends; runs preview
first, are idempotent, honor closed periods, and are exempt from the
receipt policy like depreciation. Recurring entries generate drafts only,
never posts. Six new tools (59 total); four new journal events, replayed
and checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 21:09:43 -07:00
swaitsandClaude Opus 5.5 dbdc8e671f feat: entity-aware owner equity — chart templates, draws, distributions, dividends
Every business has owners, and how owners put money in and take it out
depends on what the business is. wanderbooks now knows the four common
US shapes:

- books.chart_template {entity_type, owners}: a starter chart for a sole
  proprietorship (owner's capital and draws), partnership or
  multi-member LLC (capital and distributions per partner, guaranteed
  payments), S corporation (stock, APIC, shareholder distributions,
  officer compensation, retained earnings) or C corporation (dividends,
  income tax expense), plus common income and expense accounts with
  tax categories, and the wanderbooks.toml settings that go with it
  (tax form, members with equal-share placeholders that sum to exactly
  100, close target). A proposal for the agent to review with the human.
- Closing: CloseTo is now RetainedEarnings { sweep }, Account { into,
  sweep } (a sole proprietor's capital) or Members([MemberClose { capital,
  share, distributions }]). Sweeps zero owner-pay equity accounts into
  their target at close: draws into owner's capital, dividends or
  distributions into retained earnings, each partner's distributions
  into their own capital. Validation (equity, distinct, shares sum to 1)
  runs in the pre-close preview, before anything is journaled. The
  journal records the target; journals with no target or the old
  "retained_earnings" string still replay.
- Members' equity: a partner's distributions account is part of their
  capital; its activity reports as distributions.
- Config: members' distribution_account; [books] close_into and
  close_sweep for single-owner businesses (rejected alongside members).
  The cash flow names members on their distribution accounts too.

Tests: a sole proprietor (net 2,000 and 500 of draws into capital: 6,500,
the cash); a corporation (dividends 300 into retained earnings: 600); a
partnership with distribution accounts (Bob 100 + 25 - 20 = 105, swept at
close, February begins there); bad sweep accounts; templates round-trip
through the chart parser, unique codes, equity per entity, every tax
category valid for the entity's return; stdio: template -> settings ->
import -> postings -> close with the exact closing lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 21:01:31 -07:00
swaitsandClaude Opus 5.5 f858fb4cfe feat: tax categories and the tax summary report
wanderbooks is a general MCP-first accounting application: every
business needs its accounts mapped to where they land on its income tax
return, and its CPA needs book-to-tax differences surfaced, not hidden.

- ledger_core::tax: a catalog of stable categories (gross_receipts,
  commissions_fees, meals, guaranteed_payments, officer_compensation,
  nondeductible, ...) with kind (income, cost of goods, deduction), the
  returns each appears on (Schedule C, 1065, 1120-S, 1120), a default
  deductible percent (meals 50, nondeductible 0), and guidance. Keys,
  not line numbers: form lines move from year to year.
- Accounts carry tax_category and an optional deductible_percent
  override, validated against the account type (and the return, at the
  MCP layer), journaled as account.tax_set, replayed, projected (schema
  v6).
- tax_summary(start, end, form): the P&L grouped by category, book and
  deductible amounts (half away from zero to the cent), contra income
  (refunds) reducing receipts, and every account still uncategorized.
- MCP: books.tax_categories (filtered to the configured return),
  books.set_tax_category, a tax_summary report in every format and in
  the CPA package, chart output with categories, and a fifth
  tax_category column in chart CSV imports, validated before anything is
  created. [company] tax_form, or derived from tax_classification.

Scope stays book basis: no tax depreciation, basis, or filing.

Tests: a hand-computed quarter (50% meals 333.33 -> 166.67, penalties at
0%, refunds as negative receipts, guaranteed payments, one uncategorized
account; book net 5,276.67 vs tax 5,483.33); overrides; category/account
/return validation; form parsing and derivation; CSV parsing; stdio:
filtered catalog, mapping, refusal, summary, all-or-nothing import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 20:53:56 -07:00
swaitsandClaude Opus 5.5 8164abb2b8 fix: period close sweeps only through the period end; partnerships close into members' capital
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
2026-10-11 20:39:17 -07:00
swaitsandClaude Opus 5.5 8f623ba810 feat: canonical report set in QuickBooks-style presentation
Every report a CPA expects, laid out the way US small-business books
read (QuickBooks as the reference):

- Exports: books.export gains general_ledger (by account, running
  balances, splits), general_journal (the entry-ordered listing, which
  the CPA package used to mislabel "general ledger"; alias journal),
  members_equity, and trial_balance as of a date, in xlsx/csv/json, and
  PDF for all five statements (P&L, balance sheet, cash flow, trial
  balance, members' equity). books.trial_balance takes as_of.
- CPA package: every statement as PDF and XLSX, plus the general ledger
  and general journal, all for the fiscal year to date.
- Layout (ledger-import::report): multi-column statements (Total;
  Debit/Credit; one column per member plus Total), "$" on subtotals and
  totals, a rule above every sum (none over an empty section), a double
  rule under statement totals, zero as 0.00, negatives in parentheses
  with the closing paren hanging so decimal points align.
- Terms: "Net income (loss)"; for a multi-member LLC, "Members' equity",
  member contributions and distributions named in the cash flow, and the
  balance sheet splitting retained earnings (the Retained Earnings
  account plus earnings not yet closed from earlier years) from the
  fiscal year's net income (loss); the cash flow's "Adjustments to
  reconcile net income (loss) to net cash from operating activities" and
  "Net increase (decrease) in cash".
- PDF: Inter replaces the typst-assets serif fonts (bundled, SIL OFL 1.1,
  license in fonts/; from the official v4.1 release), centered title
  block, column-header row, full-width rules, footer "Accrual basis ·
  Prepared <date> · Journal seq N" with page numbers. Amounts on total
  rows stay regular weight so bold figures can't leave the decimal
  column. typst-assets dependency removed.
- XLSX: centered title block, header row, Excel accounting formats with
  "$" attached on sums, general ledger and general journal workbooks.
- Config: wanderbooks.toml [[company.members]] (name, share "50%" or
  "0.5", capital_account; shares must total 100%) and [books]
  accounting_basis (accrual | cash).
- Core: RETAINED_EARNINGS_CODE names the "3999" that period close uses.
- Fix: "today" is local time, not UTC; in US evenings UTC is tomorrow,
  so reports defaulting to "through today" included the next day and
  footers carried the wrong date.

Tests: layout (currency and rule placement, retained earnings vs. this
year's net income for a member LLC, footer text); exact template data;
deterministic PDFs; config parsing (members, shares, basis); stdio:
trial balance as of, February general ledger (beginning 1,004.00, splits,
ending 1,003.50), members' equity (contributions 1,000.00, net income
3.50, ending 1,003.50), members' equity PDF, the CPA package's ten files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 20:29:12 -07:00
swaitsandClaude Opus 5.5 02b3b9c053 feat: general ledger by account, trial balance as of, members' equity
Three reports a CPA expects that the engine could not produce:

- general_ledger(start, end): the canonical general ledger, organized by
  account in code order. Each account has its beginning balance, every
  posting in the period (date, entry, description, debit, credit, running
  balance on the normal side) and its ending balance, which ties to the
  trial balance. Each posting names its split: the one account on the
  opposite side of its entry, or "-Split-" when that side has several,
  so a compound entry (one debit, several credits) reads correctly from
  every account it touches. The entry-ordered listing the exports called a
  "general ledger" is really the general journal.
- trial_balance_as_of(as_of): a trial balance is always as of a date.
- members_equity(start, end, members): the statement of changes in
  members' equity for a multi-member LLC taxed as a partnership. Each
  member's capital roll-forward: beginning (capital plus their share of
  undistributed earnings), contributions, allocated net income (loss),
  distributions, ending. Allocation is by ownership share to the cent,
  the last member absorbing rounding, so totals are exact and the ending
  total equals balance-sheet equity; any equity change the roll-forward
  can't explain shows as "other" instead of disappearing. Shares must be
  positive and sum to exactly 1; capital accounts must be distinct
  equity accounts (LedgerError::InvalidMembers).
- CashFlowLine.account: the account a line is about, so renderers can
  name members instead of account codes.

Tests (every expectation computed by hand): trial balance as of Jan 31;
February general ledger with beginning balances, running balances, a
card that nets to zero, an inactive account still listed, and every
ending balance tied to the trial balance; a compound entry's splits from
all three sides; a two-member partnership where 666.67 and 100.01 split
unevenly (333.34/333.33, 50.01/50.00), with and without closing January,
tying to the balance sheet each month; bad member configuration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 20:16:25 -07:00
swaitsandClaude Opus 5.5 b5af21bb0e feat: real dates and per-account detail in every statement export
Every statement export rendered life-of-ledger totals under whatever date
range or as-of label was asked for, and the P&L and balance sheet XLSX
were totals-only. Now:

- books.profit_and_loss / books.balance_sheet and the csv, json, xlsx,
  pdf exports and CPA package all use core's profit_and_loss_for_period
  and balance_sheet_as_of, with a line per account. The tools keep their
  totals and add revenue/expense/asset/liability/equity account lines.
  An omitted start is the first posted entry, never "0001-01-01".
- ledger-import::report lays a statement out once (section, line,
  subtotal, total) for every renderer, with one exact amount formatter:
  thousands separators, negatives in parentheses, no floats. XLSX keeps
  numeric cells with an accounting number format, named sheets, company
  header, totals with borders; the trial balance gains its totals row.
- ledger-mcp reads wanderbooks.toml (next to --data-dir, or
  --books-toml): company name for headers, fiscal_year_start for the CPA
  package's fiscal year to date (end derived; Feb 29 rejected), and
  [books] receipt_threshold, which the server had been ignoring.
  config.json's receipt_policy still wins when set. New dependency: toml
  1.x (already in the lockfile via another crate).
- Store::journal_seq(); every report notes the journal seq it reflects.

Tests: company.rs unit tests (fiscal years incl. a July start, scaffold
parsing, bad settings); report.rs formatting tests; an MCP stdio test with
--books-toml (threshold 5.00 enforced, February-only P&L with account
lines, start defaults to the first entry, as-of balance sheet).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 19:11:36 -07:00
swaitsandClaude Opus 5.5 c6faeeb827 feat: period-scoped P&L and as-of balance sheet; closes don't zero periods
The engine only had life-of-ledger P&L and balance sheet totals, so every
export labeled with a date range or as-of date (PDF, XLSX, CPA package)
printed all-time numbers under that label. Core now has:

- profit_and_loss_for_period(start, end, class?) and
  balance_sheet_as_of(as_of), each with a line per account
  (StatementLine: code, name, normal-side amount) and exact totals.
- Ledger::closing_entry_ids(). Period statements exclude closing entries.
  A close (Dr revenue / Cr expense / net to Retained Earnings, dated at
  period end) moves earnings; it doesn't earn or spend. Counting it made a
  closed period's P&L read as break-even, and made the cash flow show zero
  net income plus a fake "owner contribution" to Retained Earnings. The
  as-of balance sheet keeps closing entries: after a close the earnings
  belong in Retained Earnings, not current earnings.

Tests: tests/statements.rs, a two-month fixture with every expected number
computed by hand (Jan NI 1,700; Feb 880; balance sheets on four dates; a
closed January still reporting 1,700 and a cash flow with one financing
line). Mutation-checked: dropping the closing exclusion fails it (0 vs
1,700). Properties: period P&Ls partition a year at any split, and a
range's net income equals the change in current earnings across it, with
A = L + E on both dates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 19:04:18 -07:00
swaitsandClaude Opus 5.5 5548902f27 feat: cash flow statement PDF, pdf export format, cash flow in CPA package
The cash flow statement was computed and exportable as csv/json/xlsx but
missing from every PDF path, so the CPA package carried two of the three
financial statements. Now:

- ledger-import: cash_flow_pdf renders the indirect-method statement
  (operating with net income, depreciation add-back, working-capital
  lines; investing; financing; net change reconciled to beginning and
  ending cash) from the computed CashFlowStatement, so the period and
  cash accounts are the caller's.
- books.export gains format "pdf" for profit_and_loss, balance_sheet, and
  cash_flow (base64, like xlsx), plus financing_accounts for cash_flow.
- books.export_cpa_package adds cash-flow.pdf (fiscal year to date, like
  the P&L) and requires cash_accounts: without them the statement has
  nothing to reconcile to.
- One cash_flow_statement helper resolves codes and dates for xlsx, pdf,
  and the package.

Test: MCP stdio test exports all three statement PDFs, checks the
cash_accounts and bad-report refusals, and unzips the CPA package to find
all five files. Known report gaps are logged in plan/STATUS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 18:57:12 -07:00
swaitsandClaude Opus 5.5 e59e8001c6 feat: receipt unlink and attachment withdrawal
Posted entries stay immutable (R-3), but their evidence can now change the
same way the ledger does: by appending events, never by editing or
deleting. New R-13a.

- books.unlink_receipt {attachment_id, reason} journals receipt.unlinked
  and leaves the receipt filed, unlinked. The receipt policy holds: an
  entry that requires a receipt can't lose its last one (structured
  receipt_required error), so a replacement is attach-new, then
  unlink-old. Drafts and reversals are exempt, as at posting.
- books.withdraw_attachment {attachment_id, reason} journals
  attachment.withdrawn for an unlinked receipt or a filed document: hidden
  from list_receipts (unless include_withdrawn), never linkable again. The
  blob stays in the store, hash-verified and readable.
- link_receipt now refuses an already-linked or withdrawn attachment.
  Before, re-linking silently moved a receipt off a posted entry,
  bypassing the policy; evidence now leaves an entry only through a
  recorded unlink. dispose_asset checks its receipt ids before posting
  instead of after.
- Schema v5 adds attachments.withdrawn_at/withdrawn_reason; existing
  stores migrate on open and replay rebuilds the projection.

Tests: store test (errors, reopen, rebuild, verify, journal history) and
an MCP stdio test (policy refusal, replace flow, withdraw, listings,
under-threshold unlink). Dry-run and live use on the Wanderland Labs books:
8 migrated .eml attachments swapped for their PDF renders, trial balance
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
2026-10-11 18:40:51 -07:00
swaits 6540bceac1 Payroll accounting, class tracking, CPA handoff package (48 MCP tools) 2026-10-11 15:32:27 +00:00