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 d5385a7e (S4.2,
the version before this slice) through the old-books-writer harness, with
its expected.json; this version opens it and reports the same figures;
(3) see the note on R-A step 4 in the PR (the over-the-wire document write
needs S4.4's arguments; S4.2's invoicing-setting entry already covers the
feature's cut-over); (4) `documents_v2_is_its_own_feature` is green; (5) no
figure moves: no golden changed; (6) merge does not need the owner's
go-ahead; the live books refuse the new writes until he gives it.
just check green (no kernel function, Lean theorem or Ledger command, so no
`just prove` change).
The owner (2026-10-23, via the question tool): "Uploads only"; and
before it: "What we would DEFINITELY want to avoid is passing a bunch
of BLOBs through AI context windows. That's just a pure waste." No file
passes through an agent's context, in either direction.
In: the inline arguments are gone with the 256 KiB limit (INLINE_MAX,
take_file's inline branch): content_base64 and its sha256 on
books.attach_receipt and books.file_document, w9_content_base64 and
w9_sha256 on books.set_w9, csv_text on the two CSV imports. A file comes
only by books.begin_upload and `wanderbooks upload`, named by its
upload_id; existing_attachment_id still names bytes stored already. The
five filing tools deny unknown fields, and any inline argument to any
tool is refused before it runs as an unknown argument pointing to
books.begin_upload (access log: refused inline_file).
Out: books.export (every format), books.asset_labels and
books.get_receipt view bytes hand the file over (HandedFile: download
command and expiry, D-29), as the CPA package and the W-9 do. Resource
reads stay text. A token keeps up to 64 handed files (was 16). The
access log's digest no longer exempts file bytes.
Docs: handbook files-in-out and parties-and-tins, mcp-interface.md,
rules.md, receipt-store.md, parties-and-attachments.md, plan/served.md
(D-46; the inline decisions marked superseded), STATUS.md, testing.md
(D-46: tests migrated to uploads and downloads, rewritten, removed,
added).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
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
The server backs each book up itself: a per-book git archive, pushed
fast-forward only to a git remote and to a folder, when the books open,
after a quiet spell, before they close and on request; every commit,
push and drill is a record in ops/backup.jsonl. Drills clone each
target and verify the chain, the blobs, the vault and the head, weekly
and after a failure. `wanderbooks books restore` previews, then
restores beside or (with --replace) over, never moving books something
holds. The archive refuses anything but a plain bare repository and
pins git against the owner's configuration. books.backup and
books.back_up_now for the agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
Files reach the books through the server: uploads by hash and size,
then filed; exports (the CPA package, the 1099 worksheet, W-9s) handed
over as downloads from the server, never read off disk by the agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
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
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
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 the first company 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