d1fb512e4bf2bfa645a38ba53cb1538857397bbe
64
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d1fb512e4b |
feat(arap): record an invoice's sends as invoice.sent (P4b S5)
The books never send invoices (A-4), but they must remember that one
went out: who sent it, how, to whom, when, whether it was the original
or a reminder, and the channel's own reference. That record is a fact
of the document's life, not a figure: a send moves no money, so it is
one journal line (`invoice.sent`) and no entry, and every derived
figure (outstanding, aging, DocState) is exactly as it was.
`Ledger::mark_sent` refuses a send on a void invoice (it should never
have reached the customer), a send dated before the invoice, no
recipients, and any recipient, reference or note that would put a full
TIN or control characters into the journal. Open, partial, paid and
written-off invoices can be sent (a copy for the customer's records, a
reminder).
The event is `documents_v2`'s (D-52): an older version refuses the
type rather than dropping the record. Replay restores it and the
projection keeps it in schema 16's `invoice_sends` table, so live,
replay and the viewer agree. The new old-books fixture `p4_s5_before`
was written by
|
||
|
|
0e5aebda99 |
feat(store): journal the document fields and file the issued PDF (P4b S4.3)
S4.1 gave documents a number, terms, line accounts and snapshots, and S4.2
gave parties an email and default terms, but the store journaled none of
them: a document carrying one was refused by a temporary guard, because a
public write that replay loses would break I-3. This slice makes the
journal carry them.
- `invoice.created` and `bill.created` write a number, terms, bill-to and
seller snapshots, references, memo and a duplicate's reason, and each
line its account, quantity and unit price, every key only when set. A
document naming none is journaled byte for byte as every earlier version
wrote it (`no_new_fields_journals_byte_for_byte`), so the owner's live
books keep working.
- All of it is behind `documents_v2` (B-27): a write that names one waits
for the owner's upgrade, refused with `format_upgrade_pending` and
journaling nothing; a write that names none never waits
(`documents_v2_waits_for_the_upgrade`).
- `invoicing.first_number` is settable only while no numbered invoice
exists (A-2, B-40): the series has begun, and a number is never reused or
renumbered. Refused at `set_setting`, before anything is journaled.
- The issued copy is filed in the issuing call (A-7, B-40 item 3):
`create_invoice_issued` journals the entry, the invoice, then
`receipt.attached` on that entry (`INV-0007.pdf`, `application/pdf`,
"Issued invoice") and `attachment.linked` to the invoice. There is no
`issued_pdf` field: the attachment id is allocated after the entry exists
(CD-A6), and the link is how the copy is found and how its SHA-256 is
checked. Every refusal happens before the first journal line; the PDF's
bytes are stored before it, so a refused issue leaves an unreferenced,
deduplicated blob at worst.
- A preview runs the issue on a clone and journals nothing: the posting,
the total, the number it would take and the due date (`preview_invoice`,
`preview_bill`). Books that haven't upgraded keep the legacy shape: the
copy is filed as "Invoice {id}.pdf" and no number is taken (migration
item 5).
- Schema 16 adds the document columns; a line's new fields ride
`lines_json` as its project does. `differs_from` already compares whole
invoices, bills and parties, so it sees every new field.
8.0.6: (1) `books_written_under_older_rules_still_open` hand-writes both
new document shapes through JournalWriter and still opens; (2) a new
fixture `old-books/p4_s43_documents` was written by main
|
||
|
|
962b52c991 |
refactor(settings): the slice's contracts and notes (P4b S4.2)
invoicing.first_number's refusal (LedgerError::FirstNumberLocked) lands as the contract the store's check raises once invoices carry numbers (S4.3): a series that has begun is never renumbered (B-40, A-2). Also: the settings invariants name the number prefix's bound, update_party's doc says which facts wait for documents_v2, and the email check no longer reads as a single-arm match. Terms is S4.1's as it landed; this slice no longer carries a copy. |
||
|
|
02cb7f0254 |
feat(parties): an email and default terms per side (P4b S4.2)
A customer's email is copied onto an invoice's bill-to snapshot at issue, and a party's default terms (one as our customer, one as our vendor) are the fallback a document's due date takes when the document itself names none (A-13, rules.md B-39). Both are journaled as party.updated facts and guarded by documents_v2: in books that have not declared it the write is refused with the upgrade remedy and journaling nothing, and replay gives the same directory back. An email must be one plain address: one @, no spaces, no full TIN (the party text rule already refuses nine digits). Terms are typed and already canonical, so there is no second spelling to check. A party.updated naming a field this version does not know is now NewerJournal, never corrupt (B-27): an older version must say update, not damage; a known field with a malformed value is still corrupt. |
||
|
|
13282cf80e |
feat(arap): terms, numbers and line accounts in the core (P4b S4.1)
Documents could name only one revenue or general-expenses account per project, had no number of their own, and took a due date the caller had to work out. This slice gives the core the pieces the rest of S4 needs, and journals nothing: an old-shape document still writes byte for byte, so the owner's books need no upgrade for it. - Payment terms (B-39): Net n, Net n EOM, due on receipt and a custom date, with the early-payment discount and its exact bound (kernel::discount_max, Kani + bridge). A due date past the calendar's end is refused, never a panic, and a discount is refused with terms that have no net days. - Invoice numbers (B-40, A-2): first_number plus the count of numbered invoices, zero-padded to four digits and wider past 9,999, stored at issue, kept by a void; legacy invoices are uncounted. - Line accounts (A-10): a document line may name its account, and the entry posts one line per (account, project) part. An invoice line takes revenue or a liability holding no role (deferred revenue); a bill line an expense or a non-cash asset holding no role. Roles, cash accounts and headers are refused. - A line's amount is exactly its quantity times its unit price (kernel::line_amount, Kani + bridge): both or neither, never rounded. - A vendor's invoice number names one standing bill per vendor, with case, spaces and hyphens ignored, unless a duplicate reason says why (B-40). - The record fields S4.3 will journal: number, terms, bill-to and seller snapshots, customer reference, memo, the empty sends list S5 fills, and the bill's received_on, all optional and defaulted. The store refuses a line carrying an account, quantity or unit price before it rehearses or journals anything: those fields are journaled from S4.3's documents_v2 on, and I-3 will not let a public write replay without them. `document_line_fields_wait_for_documents_v2` proves the refusal, the unchanged journal and allocators, and that an ordinary line journals exactly as before. S4.3 removes this guard in the same change that journals the fields and adds its byte-for-byte test. The document commands keep their signatures; create_invoice_with and create_bill_with take the optional fields for S4.3 and for tests. They are the existing issue commands with more fields, not a new state transition, so no Lean Step is added. Kani: line_amount_is_exact runs in `just prove`; discount_max_is_bounded relates a product to a quotient and runs in `prove-slow` (163 s), as the proration charge harnesses do. just check green; just prove green. |
||
|
|
d7fa16e974
|
feat(arap)!: documents read their figures from standing lines; control accounts
check / check (push) Canceled after 0s
refuse hand entries (P4b S1, part A) Plan P4b (D-45), slice S1, the engine half: rules.md B-38. - ledger-core: every receivables or payables line carries its document (Ledger.line_docs: DocRef, Movement, DocTag), written by the document commands in the step that posts it, derived on replay from the existing document events (the role's account as it stood then; payments by the ordinal rule), copied onto a reversal's mirror. Ledger::stands_on; doc_state / invoice_state / bill_state (total, paid, outstanding, status Open/Partial/Paid/WrittenOff/Void, overdue days) as of any day; ar_aging / ap_aging read them, so reversed payments and voids are right. The control guard (add_line, add_line_fx, post_entry, register_recurring) refuses untagged lines on receivables/payables, exempting a reversal's mirror of a legacy line; replay never refuses. Bounds (kernel::running_bounds, bounds_kept) on payments and reversals; a document reversal never dated before its entry; deposit/payer accounts must be cash-like and hold no role. New API: receive_payment_with / pay_bill_with (PaymentDetails: instrument, reference), void_document / check_void_document, control_check, payments_recorded, standing_payments, replay_reverse_entry. Invoice.amount_paid/status remain only as derived caches for the MCP layer until part B. cashflow bill_splits reads payments from the tags. - ledger-store: journal feature document_standing (D-39 gated) for a payment accepted only because an earlier one was reversed; receive_payment_with / pay_bill_with (instrument journaled as entry.instrument_set, reference in the description), void_document; replay uses replay_reverse_entry; schema 15 drops the projection's amount_paid/status. Fixture p4-arap-before + golden figures written by the version before S1, adjudicated in arap_standing.rs. - ledger-gui: party card, cash "coming up" and company owed totals read doc_state as of today (late from the derived state). - Lean Documents.lean (control_agrees and friends); Kani bounds_check_matches_definition, bounds_kept_is_the_definition_on_books_in_bounds; rules B-38, B-18/B-23 amended; bookspec `older = true` for legacy hand lines; tests that hand-post to 1200/2000 post as replay or renumbered. Review fixes (both reviews of part A): - control_role_of guards only an account the role is assigned to or one holding its documents' lines (a chart's own 1200 Equipment or card at 2000 is free until a document posts there); payment accounts check other roles by holds_role (type-checked). The MCP tests that failed on the guard (oracle_mcp, spec_projects) pass unchanged. - standing_payments leaves out reversals (sorted, deduped); replayed tags match the movement's side; payments_recorded counts every payment event (tagged or not, Ledger.payment_events) and refuses overflow. - The record caches settled (paid + discounted + written off) and owed (Invoice/Bill.owed, never serialized): a void never reads as paid. - assign_role: receivables/payables refuse moving onto an account with untagged lines (RoleAccountHasHandLines) and, on their control account holding a balance, say DocumentRoleHoldsBalance (no hand entry named); B-23 reworded. The reversal-mirror exemption only takes a hand line off (even chain depth); putting it back is refused. - DocStatus Overpaid and Unattached; aging leaves out credit balances; ControlCheck.attention lists out-of-bounds and unattached documents. - stands_on counts a chain in order; indexes (doc_index, doc_accounts, order_pos, reversed_by) keep the derived figures linear. - Viewer reads a post-dated document as of its own date; one state per row. bounds_kept's side-crossing documented as intended. - Tests: ten new spec_documents scenarios, stronger proptests, store proptest arap_props (live = replay = snapshot at every date, closed months), fuzz ops void/payments with details, a GUI post-dated test; Lean Step.assign / Step.release with unattached_zero kept; model limits stated in docs/testing.md and B-38. Review fixes (CPA and engineering lenses): only an assigned role's account or one holding document lines is a control account (a chart's own 1200 asset stays free); a role can't move onto untagged lines or off document lines (Lean Step.assign); a void's cache never reads as paid; no Decimal sum panics; per-document indexes keep figures linear; a replayed tag matches its side; the feature gate counts payment events; a payment's reversal is never listed as standing; an overpaid document has its own status; the viewer keeps post-dated documents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
528c070605
|
feat: invoices and bills take a project, line by line (B-37 item 8)
A client project's invoiced revenue and a contractor's billed cost could never count toward the project: books.create_invoice and books.create_bill posted at once with untagged lines, and only a draft's line takes a class. Job costing (B-37 item 3) collects a job's billings and the supplier invoices for its direct costs on its cost sheet, so each document line now takes an optional project. The owner decided it on 2026-10-22 (plan D-43, rules.md B-37 item 8): each line takes one project or none; a document spanning N projects is N lines, each with its own amount; no percentage split, so every figure is exact; a cost truly shared among projects names none on the document and reaches projects only as entries of their own under the company's monthly allocation convention, when books.conventions has one (otherwise it stays untagged and the agent asks the human). The reasoning, stated in B-37: job costing charges a job the costs traceable to it, whole, and reaches shared costs only by allocation on a stated basis. ASC 340-40-25-7 counts as directly related both the costs traceable to a contract (25-7(a), (b), (d), (e)) and allocations of costs that relate directly to it (25-7(c)); 25-8 expenses general and administrative cost. The books carry the first whole on the document's line and the second as the allocation convention's own entries; neither is a percentage on the document (sources/fasb-asc.md, 25-7 and 25-8 added). The handbook's projects rule and the line's class parameter say the same, and no handbook sentence lets a shared cost be split across a document's lines. The accounting: the document's entry posts one revenue (an invoice's) or expense (a bill's) line per project its lines name, in the order each is first named, each the sum of that project's lines, and the untagged lines' sum as one more line (arap::document_parts, conservation checked by a proptest). A document naming no project therefore posts exactly the one line every document posted before. The receivable or payable line is never tagged: it is a balance-sheet line, which item 3 counts nothing; the payments (receive_payment, pay_bill) move cash against it and take no project either. Nothing but the projects moves: the P&L, the 1099 worksheet (payments made, B-19) and party totals (what moved the money, B-35) read the same with or without the tags (twin-books test); the projects with the untagged lines still partition the P&L. - ledger-core: DocumentLine.class_id (serde-omitted when none, so old documents serialize as before); create_invoice / create_bill split the revenue or expense side by project and tag it; an unknown project is refused whole (atomically). - ledger-store: a document line's project is journaled as class_id on invoice.created / bill.created, and the entry's line as entry.line_class_set right after its entry.line_added (the entry.posted lines carry class_id, as a hand-built entry's do). Behind a NEW journal feature, document_line_projects (KNOWN_FEATURES, declared by new books, asked for before the first journaled line, not even the control accounts written on refusal); not `projects`, which the live books already declared: the owner consents again at the next upgrade. A malformed class_id is corrupt; null or absent is none. Untagged documents journal byte for byte as before; old journals replay unchanged. - ledger-mcp: each line takes class (id or name, resolved like add_line's, every one before anything is written); the result gains notes for a finished or abandoned project, and on an invoice for an on-spec project (is there now an agreement? on spec a hoped-for payment is no receivable, B-37 item 4), tagged anyway; books lacking the feature refuse with format_upgrade_pending. books.get_entry shows a line's class_id and project. The handbook's "an invoice or bill takes no project yet" caveat now depends on the books: gone where the feature is declared, an "until their format upgrade" note where it isn't. - ledger-gui: an invoice's or bill's page (its entry's) lists the document's lines, each project a link that opens it; the demo's client invoices are tagged to "Client work", whose snapshot is now its revenue less its contractors. Looking at the page in a narrow window found a tagged line's tag pushing the amounts past the pane (any tagged entry): the name is now cut to leave the tag room, the tag itself at most half the cell. - bookspec: an `invoice` event, and `lines` with `project` on a bill or an invoice; example projects-invoice-and-bill-lines-count-by-project, worked by hand, both projects client work (Zeta 3,500.00 - 1,200.00 = 2,300.00; Harbor 200.00; untagged -1,750.00; together the P&L's 750.00; Dana's 1099 payments 1,250.00). - fuzz: fuzz_store_replay can declare journal features. - Tests: core, store, MCP, viewer, the P2-served cutover fixture and a viewer/server parity test (its month lists the same length, not empty); a tagged document's reversal nets its projects to zero, live and replayed; books.get_entry's class_id; the handbook served as a resource both ways; each fix broken to confirm its test fails (docs/testing.md). Docs: rules.md B-27 and B-37 items 3 and 8, sources/fasb-asc.md, plan/served.md D-43, mcp-interface.md, testing.md, parity.md, plan/STATUS.md. No proof changed (no kernel arithmetic: document_parts sums with the existing document_total). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
321b94562a
|
Projects: classes as entities, and the sidebar fits short windows
check / check (push) Canceled after 0s
A project is a class with facts (description, status, kind, party, dates, links, notes) in one project.set event behind the format feature projects. Four tools (books.add_project, update_project, projects, project); documents and decisions link to projects; B-37: on-spec costs are expensed as incurred, repayment is revenue, and the unrecovered cost is never below zero (Kani). Viewer: a Projects list and a project page with its figures, by month and by account. The sidebar measures its footer instead of reserving a fixed 172 px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
2c5b5a7171
|
P3 review: P2 can read the books until the owner consents
check / check (push) Canceled after 0s
A schedule-convention setting, the two new tax categories, a disposal before the in-service date and a depreciation catch-up each need their feature now (tax_categories_gain_exempt, disposal_before_service, depreciation_catch_up), so no write before consent leaves books P2 refuses or misreads; replayed names are read with P2's TIN detector (B-28). The cutover runs end to end through the real P2 and P3 binaries in a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
2f999c6050
|
P3: assets' posted and scheduled depreciation, their bases, and the K-1 on the tax basis (defect 13, D-41)
check / check (push) Canceled after 0s
Book value is cost less what the books posted; the schedule's figure is apart. An asset's acquired date comes from its entry. Contributed property keeps its section 723 carryover basis apart from its depreciable basis (the lower of value and adjusted basis), with its 704(c) built-in gain and method. The K-1 worksheet reports tax-basis capital (Form 1065 Item L) beside the book figures, leaving open what only the CPA knows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
3a9aa92315
|
P3: the books' format moves only with the owner's go-ahead (D-39)
Books an update could write a newer format for open on their old format and are asked at every open; anything needing a new feature is refused until the owner agrees with the passphrase, after a verified backup that includes the keys. New books start on the current format. Each projection schema has its own database file, so an older version still opens the books until the owner agrees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
b0a9fd8ad4
|
P3: depreciation straight-line by days through the final month, schedules by day (defects 22, 9, D-37)
The final month takes what the months before leave, spread over its days; a disposed asset stops on its disposal day. A skipped month is caught up in the month the run is made; a reversed run un-posts its month. New schedules spread by day; equal-months schedules must start on the 1st. Amounts round to the currency's own minor unit. Kani and Lean proofs; an exhaustive search over runs, reversals and disposals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
1304b9c0ad
|
P3: dated 1099 decisions, payment methods and the card exception (defects 23, 12, 18, D-40)
check / check (push) Canceled after 0s
A vendor's 1099 decision is its own dated fact (files NEC or MISC, not reportable with its reason, or undetermined), applied by each payment's date. Payment methods are recorded; card and network payments are left out and listed; a member's check through financing counts; a bank payment with no method counts (D-40); $2,000 for payments after 2025. One worksheet for the tool, the viewer and the CPA package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
c2ceabbae9
|
P3: party totals by what moved the money, once (defects 14, 11, 24)
Each party's part of an entry is signed as its own account would show it, with a direction and its cash and in-kind amounts; a reversal is its original negated. Totals on a cash or all basis: for a member, all equals the members' equity statement and cash the K-1's cash. Money is cash, cards and the financing accounts, and a liability credit never pays its own creditor (creditor_of), so accruals and declared distributions count once. tin_last4 from the TIN or W-9 facts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
9ab003d0d1
|
P3: rename accounts, one set of period defaults, tax categories warn or refuse (defects 15, 16, 8)
check / check (push) Canceled after 0s
books.rename_account; account names trimmed, bounded, refused when a sibling's or TIN-shaped; books.show refuses what its target ignores. A report asked for without dates covers this fiscal year so far, everywhere (one rule for tools, descriptions and the viewer; the IIF and CPA package keep their own dates). policy.require_tax_category warns or refuses; a Form 4797 gain is shown apart on every return. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
a7f3cc09a9
|
P3: odd cents split the same running way everywhere (defect 17, D-38)
check / check (push) Canceled after 0s
Members' shares of earnings are split cumulatively by largest remainder in the closes, the members' equity statement, the K-1 worksheet and the viewer, so a member's capital on a given day never moves by a cent when its month is later closed. Old closes replay as they were; the first P3 close carries any true-up. Feature members_cumulative. Kani and Lean proofs of the bounded drift; bookspec examples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
a298d154b6 |
P2 slice 3: opening and closing the books (plan 3.9, D-21, D-23)
One wanderbooks executable. The books are open while the viewer, an agent session or a terminal holds them: a host process takes leases on a socket, starts the server (the hidden mode `wanderbooks server`) with a pipe on its input, and closes it 10 s after the last lease ends. No background service. One host even in a race (run/host.lock). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
b47331e9d5 |
P2 slice 2: the data root (plan 3.4, 5.5)
ledger_store::root: one private folder for every book, found without the environment (--root, else the root setup recorded in a file only this account can write, else the platform default under the home from the password database). Every folder made 0700, files 0600; a root that is a link, another user's, open to others, unmarked, or under a folder another user could swap is refused; no error names it. Setup keeps a recorded root, never makes a second root or moves books while one holds any, and leaves a folder holding other things untouched. Books are created and adopted one at a time; adoption reads the source under its writer lock (changing nothing in it), copies into staging, re-hashes every stored file against its name, checks every named file is there, opens and verifies the copy with the source's head, imports settings for books that keep none, syncs, and only then renames it into place. wanderbooks setup makes the root and never prints where it is. Three review rounds; mutation on the diff (docs/testing.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
c587593d78 |
P1: the books keep how they are kept
Settings, conventions (posting templates, approval rules, policies, close steps, calendar, mappings, notes), decisions, open items and entry approvals are journal events (journal format 2). Books that keep their settings never read wanderbooks.toml or config.json; the server and the viewer take the vault key from --vault-key-file instead. books.import_settings moves a settings file in once, exactly as the server reads it. Approval rules, when enforced, gate every posting path. Replay refuses what it can't read as newer, never as corrupt. Lean: a record changes no figure. Mutation on the whole diff: every survivor killed by a test but two equivalent (docs/testing.md). The Company page says a late open item is overdue in words, not colour alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj |
||
|
|
8693d5a3f5 |
MCP: short connect text, a handbook, tool annotations, safe local imports
- The 13K-character instructions (clients cut them short) become a
connect text of at most 1,400 characters whose first 512 stand alone,
plus books.handbook and books://handbook/{section}: the old guidance by
section, with no helper commands, home paths, pull requests or
file-reading suggestions (tool_text_lint).
- Every tool has a title and read-only/destructive hints; no
requiresUserInteraction anywhere (owner decision 2026-10-16).
- books.import_local_file reads only regular files inside the import
roots (the downloads folder by default, or --import-root), never the
data dir, books dir, vault or the key's folder, no symlink escapes or
hard links, and the file opened must be the file checked.
- The store lock is a tokio mutex taken off the runtime threads.
- get_info refreshes settings first; a bare books://digest reads.
Review fixes (P0 review, 2026-10-16):
- Fail closed: a tokio mutex never poisons and spawn_blocking caught the
panic, so after a panic mid-mutation the server kept serving a Store
behind its journal. A panic (the task, or while holding the store)
now stops the shared store; every later request on every session is
refused with a plain message; a restart re-verifies the journal. The
join error goes through redact_refusal and never carries the panic's
text.
- books.set_w9 w9_file_path read any path (a link named w9.pdf to the
vault key was stored and get_w9 wrote it out): it now goes through the
import roots' lockdown, its type judged on the canonical path.
- Sealed even inside a root: the key file in force, where a linked one
leads, both folders, and the default key folder whatever the settings
say. Refusals check the roots first, so outside them every path gets
one message (no probing what exists); inside, other books-dir files say
to copy them out first.
- The open is O_NOFOLLOW | O_NONBLOCK (a pipe swapped in after the check
can't hang a thread) and must match the checked file's device and
inode. New dependency: libc (unix only), for those two open flags,
which std doesn't name.
- Default import roots are the downloads folder only (the plan); files
with full TINs (get_w9, vendor_1099 include_tins, the CPA package with
TINs) are never written into an import root, and the server refuses to
import any file it wrote with TINs (by SHA-256).
- set_tin and set_w9 overwrite vault records: destructive_hint = true;
annotations_complete pins the key tools' hints. The read-only hint is
checked in behaviour on every spec_mcp call (journal and data-dir files
unchanged).
- Text: the connect text names the settings file as the one local file
to edit, says every entry balances on real accounts and names its
party, says invoice/bill/payment/payroll tools post unchecked, rewords
the exemptions, and shows the company name as one plain line; the
handbook takes section "all", lists its ids in the schema and says to
read contract first; books.settings drops the config.json path and no
error names the data dir or the key file. tool_text_lint covers the
whole tool JSON and resources and bans git/commit/FL-0001; tool
descriptions over 700 characters are pinned to today's eight (P5).
- digest: an empty since= is bare; the default date is pinned on every
day of a month.
Round-2 review fixes (2026-10-16):
- TIN files (B-26): the in-memory, per-session set of TIN-file hashes
became one registry beside the vault (vault/tin-files.sha256, hashes
only), shared by every session and every later server. It holds the
W-9 set_w9 reads (recorded as read), get_w9 copies, 1099 worksheets
with TINs, CPA packages with TINs and each file in one that holds a
TIN; a file is recorded before it is written, and a failure refuses.
import_local_file, attach_receipt and file_document (base64 was never
checked) refuse a recorded file, and a text file (by type, name, or
UTF-8/UTF-16 decoding) holding a full TIN. Not caught: an altered
binary copy; the docs say so (P2/P7 remove the path tools).
- The text check is narrow (owner/engineer decision, 2026-10-16: bank
exports are filed for reconciliation, and has_full_tin's "nine digits
however separated" refused their routing numbers, long references and
dates run into amounts). Text is refused when it holds (a) a TIN the
vault holds for a party, anywhere, as nine digits, 3-2-4 joined by
hyphens, spaces or dots, or 2-7 joined by a hyphen or space, compared
on the server against the decrypted vault and never returned or
logged (a vault that can't be opened leaves (b)); or (b) a stand-alone
printed SSN (123-45-6789) or EIN (12-3456789) shape: no digit, and no
hyphen or dot followed by a digit, beside it. Full-width digits and
Unicode hyphens and spaces read as ASCII. has_full_tin still guards
refusals and results (redact_refusal). Known limit, for P7: a refusal
under (a) tells the agent a text it chose holds a vault TIN.
New dependency: zeroize in ledger-mcp (already in the workspace
through ledger-vault, same major version), so the vault's TINs decoded
for the scan are wiped from memory on drop as the vault's own are.
- Mutation testing of the final P0 diff (docs/testing.md): tests for
each law a survivor broke (handbook titles, a dropped default root, a
.jpg W-9, the waker, the walk's folder flags against a link and a
pipe, the 40-link limit, save paths refused as such, the package
marking only its TIN files, a registry that can't be read, text types,
the short receipt rule's counts and a 12-character code). Simpler
code where only a race could tell: inspect places an existing path by
canonicalize itself (no second comparison), read_checked's read limit
is its one size check, openat fails on -1 as specified. The 25 MiB
upload test removes its 50 MB of books however it ends.
- The GUI's stale-rule prompt no longer says to open a pull request: it
asks the agent to find the figure from the rule's official authority,
confirm it with the human, pass it explicitly, and tell the human so it
can be reported upstream (as the handbook says); a lib test pins it and
bans files, git, GitHub and pull requests from every attention prompt.
- Import roots (local_files.rs INVARIANT 0): XDG_DOWNLOAD_DIR="$HOME/"
made $HOME (or /) the root. No root may be /, the home folder or hold
it, or hold the books, data dir, vault or a key: a named one stops the
server at startup, a default one is dropped; the default falls back to
~/Downloads only if that is a real folder other than $HOME; with no
root, an import says so.
- The read is a component walk from / with openat(O_NOFOLLOW |
O_DIRECTORY) per folder and O_NOFOLLOW | O_NONBLOCK for the file, so a
folder swapped for a link after the check fails; no step may land on a
protected inode (one renamed into a root); dev/ino and nlink stay. No
/proc. Portable to macOS. (libc's openat, two small unsafe blocks.)
- One refusal for missing, dangling and outside paths: resolved()
follows every link, dangling ones too, so a link's target existing or
not no longer changes the answer.
- Sealing: the key files (in force, default) and their targets always;
their folders only when not $HOME, not a root, not holding a root; and
~/.config/wanderbooks whatever XDG_CONFIG_HOME says.
- Agent text (I-12): RuleNotKnown names the authority (IRS, with its
address) and says to tell the human, never a repo path, PR or GitHub;
the digest's stale_rules drop the repo catalog and path for it; vault,
outside-books, STOPPED and local-file refusals name no git, path or
flag. tool_text_lint bans docs/, .md, /home, data dir, --data-dir,
--import-root, vault.key, .jsonl, sqlite, .gitignore, github, pull
request, and also lints every error template (scanned from source),
built error texts, and the server's refusal constants.
- The receipt rule told (ReceiptPolicy::rule) is the one enforced: the
exact threshold in the base currency (no "$", no rounding), required
and exempt accounts, reversals exempt; the connect text fits 1,400
characters by dropping reasons, then codes, never the rule.
- The connect text follows the parties setting; "offer books.digest"
everywhere. shown_name drops Unicode format characters (bidi, zero
width).
- read_only_tools_change_nothing compares file contents, the database
by every row, the vault and the settings file (raw database bytes did
not change either; rows are the stable comparison).
- import_local_file takes the name and type from the canonical path;
the digest's journal repair says when the tail was set aside, not the
journal's file name; "version-history messages" back in the TIN rule;
openWorldHint false on every tool, pinned in annotations_complete.
- Outside the MCP crate, for the same texts: ledger-core's DatedRule
gains `authority` (the statute or publication with its address) and
RuleNotKnown carries it; ledger-store's "sqlite error" reads "database
error"; ledger-config's ReceiptPolicy::rule takes the base currency.
New dev-dependency: rusqlite in ledger-mcp (already in the workspace,
same version and features as ledger-store), to read the database's
rows in read_only_tools_change_nothing.
- The filing scan is no TIN oracle (tin_files.rs INVARIANT 4): text
holding more than two TIN-shaped numbers (any rendering rule (a)
reads) that end in a known TIN's last four (the books' TIN and W-9
facts, via known_tin_last4, before the vault is opened; then the
vault's own) is refused with TIN_PROBE, the same words whether or not
one is the vault's TIN; only one or two candidates are compared
against the vault. Tests: unit
text_probing_for_a_tin_is_refused_alike_whether_or_not_it_holds_it,
one_or_two_candidates_are_compared_against_the_vault; over HTTP
text_probing_for_a_tin_is_refused_alike_by_every_filing_tool.
Plan: plan/served.md P0, section 6 "Immediate hole (P0)", 4.1, 4.3, 4.9.
Review round 3. S1: a panic stops the server for every session, so no
input may reach one. Dates are four-digit years (new ledger_core::
calendar, 0001-01-01..9999-12-31): the MCP's parse_date is strict
YYYY-MM-DD, periods are bounded, last_day_of_month returns a refusal
(server and store); schedules, assets, recurring entries, periods,
documents, disposals and entries refuse dates outside the calendar, and
replay refuses such journal dates. Every amount a command takes is at
most a line's (draft lines by size, document lines, payouts, payroll,
statements, shares, tax basis, proceeds, recurring templates), on
replay too, so sums are exact; the 1099 vendor part and the K-1
property share use checked products (TooLargeToCompute); a recurring
run makes at most 1,000 drafts. tests/hostile_args.rs drives every tool
with schema-derived hostile arguments and every resource with generated
URIs, asserting the server never stops; the audit's panics are pinned.
S2: a `.` or `..` path name is refused as NO_SUCH_FILE up front. S3:
NUL-interleaved UTF-16 without a byte-order mark is scanned both ways.
S4: resource-read refusals are TIN-redacted. S5: set_w9 records its W-9
only once the call passes every check, just before the vault write.
S8: idempotentHint only on verify, run_depreciation, run_schedules,
run_recurring, set_party_roles (a repeat leaves journal and vault
alone; spec_vault checks every candidate twice); every tool's hints
pinned in one table. C1: books.settings lists receipt_required_accounts
and receipt_exempt_accounts uncut. C2: the default company name reads
"a new company". C3: the parties rule names no-party reasons. C4/C5:
the handbook names the settings file only as books.settings' books_toml,
and restores "check books.digest and offer it". C6: an import root
inside the books, data dir or vault is refused at startup. The lint
catches `just` however it ends and where the books are;
digest_resource_bare_reads reads the server's month.
Review round 4 (2026-10-17): round 3's bounds ran in replay too, so
journals main wrote and reopened (an entry dated 0000-06-01 or
+10000-01-01, a draft line of 2e15, an asset ending past 9999, a basis
or share past the line limit, sub-cent document and payroll lines) no
longer opened. Posted is forever: the bounds are the commands' only.
- Replay applies main's rule exactly: parse_date takes any date chrono
reads; replay_add_line(_fx) has no size bound (add_line/add_line_fx
do); replay_register_asset/correct_asset (chrono overflow only),
replay_set_asset_tax_basis (check_journaled), replay_set_entry_parties
(check_shares_journaled), replay_register_recurring
(validate_recurring_journaled), replay_register_schedule (months 1 to
600), replay_create_period (order only), replay_dispose_asset, and no
line, date, payment or payroll range checks in replay_invoice/bill,
their payments and replay_payroll_run.
- Derived figures over such books refuse (TooLargeToCompute,
DateOutOfRange), never panic: amortization plans, depreciation into
chrono's last month (and the true-up for lives past 9999), intervals,
document totals and aging, net pay and payroll plans, money flows and
share resolution, the asset register, receipt-policy debits; the MCP
tools and the viewer show what can't be computed.
- tests/fixtures/old-books: the review's 13 journals, its control and 15
more edges, written by main's Store API with main's figures
(expected.json); every one opens in the writer and the viewer to those
figures, every derived figure computes or refuses, every MCP tool and
GUI page answers; hostile_args restarts the server after each
property; each bound has a command/replay test pair.
Final review: a schedule's plan refuses a total of 10^26 or more
(TooLargeToCompute) instead of letting Decimal round its cents silently,
and a null `remaining` in the schedule output is always explained by
plan_error. Replay is unaffected: no journaled schedule month can belong
to a total that large (the line limit bounds every posted month).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
|
||
|
|
9f9f00ed9a |
Store: say "newer wanderbooks", not "corrupt", for books from a newer version
An unknown event type on a whole, chained line, or a journal.format_set
declaring a newer journal format, now raises StoreError::NewerJournal
(update wanderbooks) instead of JournalCorrupt. Corruption (bad hash,
seq, or JSON) is still corruption. The gate runs in the store, the
Snapshot and the GUI, after each line's parse, seq and hash checks, and
never skips a line it can't read. Adds the journal.format_set event
(format 1 today; nothing writes it yet) and a registry of every event
type this version reads, tested against the replay and projection arms.
Review fixes (P0 review, 2026-10-16):
- A line's own hash is checked only by the next line's prev, and the
gate used to refuse a line before reading the next: a flipped bit in a
middle line's type (or a format_set edited from 1 to 2) was reported
as "newer, the books are not damaged". Now the reader settles every
refusal against the next line (format::settle): a next line that
doesn't chain makes it corrupt; one that chains makes it newer and
verified; with no next line the message says "newer or damaged; update,
then run verify", never "not damaged" (NewerJournal.verified).
- A newer format number is judged newer before its features are parsed:
a newer format may shape them differently.
- Snapshot treats a blank complete line as corrupt, as the writer does.
- docs/spec/rules.md B-27, which the code cites, now exists.
Round-2 review fixes (2026-10-16):
- The envelope {seq, ts, type, payload, prev} and its hash (SHA-256 of
the canonical JSON) are frozen across formats (rules.md B-27); new
fields go in payload. settle reads only the next line's prev, leniently,
so a newer format's next line (a float, a string seq, keys of its own)
is never called corrupt: no readable prev means unverified newer.
- A key beside the envelope's was silently dropped (and the chain then
broke as "corrupt"): it is now NewerJournal, hashed over the raw
canonical JSON so the next line still checks it; JournalLine::parse
refuses such a line.
- "the books are not damaged" overclaimed: a verified refusal says line N
is intact and this version can't check the lines after it.
- The writer read the journal with BufRead::lines (invalid UTF-8 was an
I/O error); it now splits bytes on newlines as the Snapshot does, so a
non-UTF-8 line is JournalCorrupt in both.
- Store::open truncated a torn tail before the gate ran, altering newer
books (perhaps mid-append) before refusing them. The gate now runs over
the complete lines first; the tail is set aside (and journaled) only
when they pass.
- Mutation testing (docs/testing.md, P0): the viewer and the writer say
the same words about the same damaged line (the newline is no part of
it); read_journal_and_format returns every line and the format in
force; the journal page tells a format declaration.
Plan: plan/served.md P0 and 5.1.
Review round 3 (S6, S7). Store::open replays every complete line in
memory (the first line's currency, then the ledger) before a cut-off
last line is set aside, so a complete line the books can't apply
refuses the open with the journal byte for byte as it was and nothing
set aside (B-27 SCENARIO 3; test
a_bad_complete_line_before_a_torn_tail_leaves_the_journal_as_it_was);
catch_up now only projects. The viewer
applies the writer's first-line rule (created_currency, shared) instead
of guessing USD: any other first line, or a store.created without a
sound base_currency, is corrupt to both (test
the_first_line_is_store_created_for_the_viewer_and_the_writer). fuzz_store_replay varies the
first line (14,696 runs clean).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
|
||
|
|
8498e6cfab |
Audit fixes: party money, reconciliations, require policy, K-1 property
An independent recompute of the real books found no numeric disagreements; review found 15 defects, all fixed with spec-first regression tests: - k1: a prepaid credited straight to capital is property; a member paying a vendor through 2100 is cash. Docs and B-13 match. - parties: a reversal of a reversal counts on its root's side; liability-to-liability payments (member pays the card) count as paid. - viewer: party money uses the server's cash accounts and full calendar year; per-party "What for" scales multi-party entries. - reconciliations: refuse a withdrawn statement before journaling; new books.void_statement; reconciled lines can't be un-cleared; one shared reconcilable-accounts rule for digest and viewer. - CPA package includes receipts linked to entries; a file can't be both filed on and linked to one entry. - parties = "require" covers payroll, tax remittance (agency) and disposal (buyer); no-party marks follow reversal chains and clear when a vendor is tagged. - derived member shares are frozen to exact amounts when posted, so a settings change never re-splits history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
e1b2509c22 |
Shared parties with exact shares, member capital statements, TINs for any party, K-1 worksheet
Entries may name several parties, each with an exact amount, a percent, or a share derived from the members' capital lines or [[company.members]] shares (entry.parties_set); shares always sum exactly. The 1099 worksheet counts a vendor's share. A member's party page is their partner capital statement, from the same ledger_core::k1 source as the CPA package. books.set_tin stores any party's TIN (members' SSNs for K-1s) in the vault (party.tin_set, last four in the books). The CPA package carries a K-1 member worksheet (k1-members.csv, with a members' total that ties to Schedule L) and the 1099 worksheet with full TINs, saved outside the books only. files_jointly_with notes spouses' two K-1s land on one 1040. Adversarial review fixes: reversed contributions no longer inflate K-1 contributions and distributions; a K-1 row never takes another party's TIN; TIN detection covers dot/slash/dash separators; refusals redact TINs; vendor tagging can't silently replace shared parties; reversals follow re-apportioning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
08ce558ed3 |
Attachments, statements, reconciliations, TIN vault, counterparty policy
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 |
||
|
|
a299077991 |
Parties, attachment links, statements, reconciliations (store), TIN vault crate
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 |
||
|
|
a4a3165b2c |
fix: books with an asset linked to a since-reversed entry failed to open (the projection read-back re-checked the link against today's books); links restore as recorded, Ledger::acquisition_reversed reports ones to relink; regression test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
1500d18cea |
Assets can be corrected before depreciation: books.correct_asset (cost, salvage, life, in-service date, with a reason), journaled as asset.corrected with the old figures; refused once depreciation is posted
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
ba7373bdb8 |
Fixed assets carry federal tax basis beside book value; tests sweep stale temp dirs
- 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 |
||
|
|
6adc7b34c7 |
Asset descriptions can change: books.label_asset description, journaled as asset.described (with the old one), replayed, projected, shown in the journal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
cd085108c1 |
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
- 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 |
||
|
|
762d47268a |
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
- 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 |
||
|
|
3b1317cad2 |
store journals payroll.taxes_remitted; viewer tests for the numbers mutation found unchecked
A payroll tax remittance was journaled as a plain entry, so the viewer labeled it "Journal entry" and, at a 0.00 threshold, flagged it as missing a receipt (the server posts it without one, like a payroll run). The store now names it with payroll.taxes_remitted, as it names runs with payroll.run; replay and the projection pass over it, and the viewer's journal page describes it. tests/view_model.rs: the cash history is never empty when cash moved and ends on the last day it did; the prior month runs to the same day; burn averages over only the months the books cover, half away from zero; cash accounts found by name when the settings name none; card accounts; each entry's accounts debits first, once each; only what arrived since the last refresh is fresh; integrity counts files again without rereading them; the invoice's customer, the bill payment's vendor, payroll and remittance origins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
09efa17670 |
books.renumber_account: give an account a new code, history kept
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 |
||
|
|
c3ba9e0884 |
store: second mutation pass (345/373); trim what rehearsal covers; tests
Store mutation rerun: 345 caught of 373 viable (92.5%, from 82%). The survivors, triaged: - Removed code the survivors showed could never matter: set_line_class's hand-written checks (its rehearsal refuses first), ensure_on (the engine creates missing control accounts itself), and verify's live-vs-replay halves of the asset, class, and payroll comparisons (the whole-ledger comparison above them already checks that; the SQLite read-back halves stay). - New tests: disposal account types; depreciation periods must be real months; search_entries (filters, newest first, paging, no drafts); the database's invoice amount paid and status; replay refusing a re-chained journal whose closing entry differs from what it re-derives; a projection missing a table. - Equivalent: sort_value's arms (serde_json's maps are already sorted without preserve_order, which nothing enables; the function stays as the guard if anything ever does), OpenFlags | vs ^ (disjoint bits), and writing the long form of a sweepless retained-earnings close (replay reads both). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
22b5a699b5 |
store: a database with the wrong currency is rebuilt, not blamed on the journal
Store::open refused books whose database recorded a base currency other than the journal's, with JournalCorrupt, though the journal was fine and the database is derived (a mutation-testing survivor: nothing tested the case). Now the currency comes from the journal, and a database that fails its integrity check or disagrees on currency goes through the same path: kept aside for forensics and rebuilt (open SCENARIO 6). Also tests: the viewer tells a database Behind the journal from one Ahead of what it has read; the table check counts missing and unexpected rows; a new journal creates its directory; a stored blob exists and an unknown one doesn't. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
309572fc65 |
store: tests for mutation survivors; drop pre-checks the rehearsal covers
From the first store mutation run (324 caught of 396 viable): - receive_payment and pay_bill repeated the engine's refusals by hand after rehearsing the engine itself, so those checks could never fire (their mutants survived). Removed: the rehearsal is the check. - New hand-computed tests: a disposal's final catch-up after some depreciation was posted (200 of 300, gain 100); a disposal on the purchase date (allowed) or the day before (refused); a retained earnings close with a sweep replays it; a float inside an array is refused; the viewer's journal and head hash. - The concurrency test's reader stops when the writer thread ends, not only when it says it's done, so a mutant that kills the writer fails the test at once instead of spinning to the five-minute timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
111a5af470 |
store: refusals leave the live books untouched; per-filing names; spec suite
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 |
||
|
|
27da4b63bf |
store: specs for the write path, open, verify, heal, receipts, viewer
GOAL/SOURCE/INVARIANTS/SCENARIOS blocks, citing R-11, R-12, R-13a, docs/receipt-store.md, IRS Pub. 583, and Rev. Proc. 98-25: the module spec every command method follows (refuse whole, journal first, gapless chained lines, replay equals live, multi-step commands rehearsed whole, nothing deleted), Store::create, open, verify, verify_and_heal, attach_receipt, link_receipt, unlink_receipt, withdraw_attachment, and Snapshot::refresh. Also moves ensure_control_account's doc comment back onto it (it had been stacked on rehearse). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
6fa1b382ae |
closes: a run of members closes splits cumulatively
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. The first company'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 |
||
|
|
e2c421be35 |
core: no entry posts a fraction of a cent; verify compares whole ledgers
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 |
||
|
|
54f632dfea |
store: set aside a torn last journal line on open; server waits for lock
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 |
||
|
|
55f9e8f6e0 |
store: refusal property reaches every success path; two engine fixes
The refusal property now starts from real books and adds compound commands with valid arguments (book an entry, register an asset or schedule, raise an invoice or bill, close a month to members), so every command's success path is exercised. Compound commands checkpoint after each step, so the law is checked against the one command refused. STORE_CASES sets the case count (256 by default). It found two bugs: - run_recurring created drafts dated inside a closed period (B-12). generate_recurring now refuses them, atomically; the store leaves them ungenerated and books.run_recurring reports them in skipped_closed_period for a human to decide. Replay uses replay_generate_recurring so older journals still open. The store also journals before mutating the ledger here, as everywhere else. - Disposing an asset for no proceeds (scrapping it) journaled a catch-up depreciation entry and a half-built draft with a 0.00 line, then refused. Zero lines are now left out (Store::disposal_lines, shared with the MCP preview), and the whole disposal is rehearsed on a clone before anything is written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
02d1f20f90 |
fix(store): a full disk mid-command leaves the books whole
Max-testing pass on ledger-store, fault injection (rank 1). A full disk is simulated with no code seam: the test re-runs its own binary as a child under bash with the file size capped (ulimit -f, SIGXFSZ ignored), so writes past the cap fail as on a full disk. Two bugs, both of which left the books unopenable: 1. SQLite side: applying one journal line runs several statements (rows, events mirror, last_applied_seq) in autocommit. When the disk filled partway, the rows committed but last_applied_seq didn't, and the next open's catch-up re-applied the line into "UNIQUE constraint failed: entries.id". Each line now applies inside a savepoint: all or nothing. (docs/receipt-store.md promised exactly this heal; it wasn't atomic.) 2. Journal side: an append that failed partway left a partial line, and any later line would land after it, mid-journal. append now cuts the file back to its length before the write on any error. Tests (tests/faults.rs, its own binary because the child re-runs it): a_full_disk_mid_command_leaves_the_books_whole (the database fills first) and a_full_disk_mid_append_leaves_only_whole_journal_lines (the journal writer alone). Both shown to fail without their fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
b2b49267db |
fix(store): one writer at a time; a second is refused, not allowed to corrupt the books
Max-testing pass on ledger-store, found during the survey: nothing
stopped two processes from opening the same books for writing. Two
agent sessions (two MCP servers) on one books dir would each extend the
hash chain from the same last line; one write from each broke the
journal ("seq gap: expected 3, found 2") and the books would not open.
The store now holds an exclusive lock on books-data/wanderbooks.lock
(std's File::try_lock, no new dependency) from before it reads the
journal until it is dropped; the OS releases it if the process dies. A
second writer gets StoreError::Locked saying another process has the
books open. The viewer reads through Snapshot and never takes the lock.
The lock file is git-ignored in the books scaffold.
Regression: a_second_writer_is_refused_while_the_first_has_the_books
(refused while open, allowed after close, books whole).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
|
||
|
|
bac81593e9 |
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 the owner): 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. The owner 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 |
||
|
|
4647611f96 |
fix(core,store): child accounts share their parent's type; periods nest; lines checked when added
Three spec decisions from the owner (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
|
||
|
|
8559bba6bc |
fix(core,store): refuse control accounts of the wrong type, with a reason
Invoices, bills, payments, payroll, and closes post to accounts the engine finds by code (1200, 2000, 4000, 6999, 3999, 6100-2150). If an account with that code existed it was used whatever its type: a 1200 "Customer Deposits" liability would have taken receivables. Now (the owner, 2026-10-13: never force an imported chart into our numbering) the engine refuses with ControlAccountMismatch, naming the code, the account, its type, the role, and the type needed, so the agent can work it out with the human. A placeholder is refused too. Every command checks all its control accounts before changing anything (Ledger::check_controls, spec + SOURCE in its doc comment), so a refusal never leaves a half-made account behind. One table of control accounts (ledger_core::RECEIVABLES ... PAYROLL) replaces the codes duplicated across the engine and the store. Tests (spec-derived, all assert the books are untouched after the refusal): an_invoice_refuses_a_1200_that_is_not_receivables, an_invoice_refuses_a_placeholder_1200, an_invoice_creates_nothing_when_its_second_account_is_wrong, a_bill_refuses_a_2000_that_is_not_payables, a_close_refuses_a_3999_that_is_not_equity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX |
||
|
|
e0b6d6988f |
fix(core): due-today is Current; odd cents split by largest remainder
Max-testing pass, step 2: two spec decisions the owner 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 |
||
|
|
847dd4388d |
fix(store,core): a refused command never reaches the journal; reversal rules
Max-testing pass, step 2 (rank 1 regressions + a rank 2 law). Two ways
to make the books unopenable, both found by test, both fixed:
1. Reversing into a closed period: the store journaled `entry.reversed`,
then the engine refused it at post. Replay could never apply that
line, so Store::open failed ("period 1 is closed"). Regression:
reversal_into_a_closed_period_is_refused_before_journaling.
2. An oversized invoice (or any command whose hand-written store
pre-checks lagged the engine, here the new line range) journaled,
then failed in the engine. Found by the new property
refused_commands_never_reach_the_journal within 4s.
The fix is structural, not another hand-written check: every
journal-first store command now rehearses on a scratch clone of the
ledger (`Store::rehearse`), so whatever the engine refuses is refused
before the journal, now and after any future engine change. The
create_entry rehearsal uses the checked constructor: no draft is ever
dated in a closed period (the Lean invariant Inv.draftsOpen, which the
store had bypassed).
The property (1,000 random command sequences, heavy on refusals)
asserts, after every command: a refusal leaves the journal
byte-identical; every posted entry validates; the trial balance
balances; no draft sits in a closed period; and the books reopen and
verify at the end. Shown to have teeth: removing the create_entry
rehearsal makes it fail and shrink to open-period, close, draft.
Engine (spec confirmed by the owner, 2026-10-13), Ledger::check_reversal,
a pure pre-check that reverse_entry and the store share:
- an entry is reversed at most once (EntryAlreadyReversed); reversing
the reversal re-instates it;
- a closing entry is never reversed (ClosingEntryIrreversible);
- a refused reversal leaves no trace (it used to leave a draft and
burn an id).
Specs go in doc comments as a ```text GOAL/INVARIANTS/SCENARIOS block.
New dev dependency: proptest for ledger-store (already the workspace's
property library).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
|
||
|
|
f15b4d43ee |
fix(store): verify no longer chokes on a draft before a posted entry
The SQLite cross-check rebuilt entries by id and assumed no gaps, but drafts aren't projected until posted: a draft #8 before a posted #9 made the server refuse to start. Gaps are now held with empty drafts (which no report counts), and a projection that can't be read back counts as drift to heal, not a reason to refuse to start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu |