refactor!: one way to serve books: the stdio server is gone (D-44)
check / check (push) Canceled after 0s

The owner (2026-10-23): "We should probably just delete the stdio path
altogether. THere's no point in that. And it sitting there unused will
just lead to bitrot. Nuke it?"

Books are served only by the books server (`wanderbooks server`),
reached by agents through the bridge (`wanderbooks mcp --book ID`),
which stays.

Removed: `wanderbooks dev-stdio`, the `ledger-mcp` binary, and
`ledger_mcp::run` (stdio and `--http` over a bare data dir), with all
that only that mode had:
- files the books make coming back inline: `HandedFile.content_base64`
  is gone, and `download` and `expires_at` are always there;
- books with no access log or backups: `books.access_log`,
  `books.backup` and `books.back_up_now` are always offered, by scope;
- the settings file read and reloaded (`legacy_files`), the vault key
  named by a file or `--vault-key-file`, and the books dir's
  key-inside-the-books and git checks (with `paths.rs`);
- `wire.rs` (the bridge frames an agent's lines itself);
- `books.settings`' file fields and `books.start`'s `settings_source`.

`BooksServer` is built one way, from `Served` (a book as the books
server serves it), in production and in the unit tests. An unknown tool
is now a protocol error (mcp-tools.md). Inline file input is unchanged.

Then the owner's three decisions (2026-10-23, the question tool):
(a) `books.import_settings` is removed entirely (118 tools): old books
come in whole by `wanderbooks books adopt`, which reads their settings
file once; ledger-config keeps its settings-file parsing only as far as
adoption uses it (`load` and the path-reading `effective_receipt_policy`
are gone), and `books.chart_template` gives its settings as
`books.set_setting` changes. (b) The viewer's loose-books-folder mode is
removed (`--data-dir`, `--books-toml`, `--vault-key-file`, the settings
file and config.json followed): it opens only books of the data root,
reading settings from the journal and the vault from the book's own
`vault/` and `keys/`. (c) `wanderbooks dev-stdio` has no stub: it is an
unknown command like any other word. Also: an upload's expiry past the
calendar is an error, never an empty string.

Tests: every suite that drove the stdio server moved to
crates/ledger-cli/tests/ and drives a real `wanderbooks server` on a
data root of its own (tests/common), handed files by their download;
mcp_stdio.rs is mcp_journeys.rs. Tests of what is gone are deleted and
listed in docs/testing.md. Docs: plan D-44 (D-18 superseded), STATUS,
testing, rules, mcp-interface, the handbook, README; justfile recipes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-23 19:45:18 -06:00
1 parent 0df7f52ac3
commit 7741e96eb3
87 files changed
+3477 -6476

No files matched your search

+7 -6
View File
@@ -114,12 +114,13 @@ engine keeps them beside the ledger (`ledger_core::settings::Settings`,
its replay and the viewer's `Snapshot` share one reading; `Ledger::
differs_from` and `verify` compare them, and the projection has a table
for each (schema v12). `ledger-config` is the codec between them and a
`wanderbooks.toml` (`codec::import`, `codec::export`) and still reads the
file for books that predate P1 (the "file" source, until
`books.import_settings`). The server (`settings::Live`) takes the books'
settings whenever their last `settings.set` changes (on every lock of the
store) and then never reads a file; "today" is the company's time zone's,
set per request. Rule: `docs/spec/rules.md` B-29.
`wanderbooks.toml` (`codec::import`, `codec::export`): adoption (`wanderbooks
books adopt`) moves an older books dir's file in through it, once, and
`books.export_settings` writes the books' settings out as one. The books
are their settings' only source (D-44): the server (`settings::Live`)
takes them whenever their last `settings.set` changes (on every lock of
the store), the viewer reads them from the journal it shows, and neither
reads a file; "today" is the company's time zone's, set per request. Rule: `docs/spec/rules.md` B-29.
## The MCP server's store lock (`ledger-mcp`, P0)
+55 -96
View File
@@ -53,18 +53,15 @@ one return, a fiscal year that doesn't move under a closed period). No
setting holds nine digits that read as an SSN or EIN (bare, or printed
3-2-4 or 2-7) except `company.ein` and `company.duns`.
Books from before P1 hold no `settings.set` and still read the books dir's
`wanderbooks.toml` (default: next to `--data-dir`; `--books-toml PATH`)
and `config.json`'s `receipt_policy`, reloaded whenever either changes, as
before. `books.import_settings` moves them in once; from then on no file
is read, not even at start (a broken file beside the books can't stop the
server). A vault key file is never a books setting: `--vault-key-file PATH`
names it to the server and the viewer (it wins over a file's `[vault]
key_file`).
No settings file is ever read (plan D-44): books keep their own settings,
the defaults until the first is set. Books from before P1, which kept
theirs in a `wanderbooks.toml`, bring it in as they are adopted into the
data root (`wanderbooks books adopt`, plan 5.5). A vault key file is never
a books setting: a book in the data root keeps its own (`keys/legacy.key`
in its folder).
- `books.settings {}` → `{ source ("books" or "file"), timezone, today,
- `books.settings {}` → `{ timezone, today,
approval_rules, keys: [{ key, doc, identity, value, set_by, seq }],
books_toml, books_toml_exists, config_json_present, reload_error, loads,
company, fiscal_year_start, tax_form, basis, receipt_rule,
receipt_required_accounts, receipt_exempt_accounts, parties, members,
cash_accounts, financing_accounts, investing_accounts, close_into,
@@ -73,39 +70,18 @@ key_file`).
`problems`, settings that disagree with the chart, each saying what to
change (conventions naming an account no longer in the chart among
them); `books.digest` repeats them as `settings_problems`. Settings are
named by key for books that keep their own, as the file spells them for
books that still read one; for those, `keys` shows the file's values,
with no journal line.
named by key.
- `books.set_setting { changes: [{ key, value }], decided_by, decided_on?,
citation?, note? }` → `{ changed: [{ key, was, value }], problems }`: one
or more changes, together or not at all (`value` null clears a key back to
its default; a value already set changes nothing, idempotent;
destructive, since it can clear or switch off a setting). Refused while
the books still read a settings file (or `config.json`): import it first,
so no setting is lost.
- `books.import_settings { toml_text, config_json_text?, confirmed?,
skip_unknown?, decided_by, decided_on?, citation?, note? }` → `{ status
("preview" or "imported"), changes, notes, unknown, comments }`: exactly
the changes
that make the books' settings what the file (and `config.json`'s
`receipt_policy`, which wins as it does for the server) says. `notes` are
what isn't a books setting and where it goes (`[books] data_dir`, `[vault]
key_file`, `[viewer]`, `base_currency`); `unknown` are entries the books
don't know, not imported (confirm with `skip_unknown`); `comments` are
the file's comments, in order, which are not settings and are not
imported (record what matters in them as a decision, open item,
convention or a setting's note). The text must be
the file the server reads, whole (its import is compared with the
server's own reading of its files, key by key), and the server must read
every setting afterwards as it does now; the vault key must then come
from `--vault-key-file` (on the viewer too). A file never turns approval
rules on. Books that keep their own settings take no import that changes
them (an identical one is no change). Without `confirmed`, a preview that
records nothing. An identity value exported masked imports as
"unchanged".
destructive, since it can clear or switch off a setting).
- `books.export_settings {}` → `{ toml_text }`: the settings as a settings
file, each with who decided it as a comment, the EIN masked; a copy, and
importing it back changes nothing (`ledger_config::codec`).
file, each with who decided it as a comment, the EIN masked; a copy:
editing it changes nothing (`ledger_config::codec`). No tool reads a
settings file: old books bring theirs in once, whole, when the owner
adopts them (`wanderbooks books adopt`), and only `books.set_setting`
changes the books' settings after that.
The accounts invoices, bills, payroll, and closes post to are not settings:
they are control roles in the journal (rules.md B-23).
@@ -128,7 +104,7 @@ settings-and-conventions section says how to use them.
- `books.start { focus? }` (read-only, at most 8 KB) → `{ book: { name,
timezone, today, fiscal_year, base_currency, basis }, head: { seq, hash },
state ("needs_onboarding" or "active"), settings_source, rules: {
state ("needs_onboarding" or "active"), rules: {
evidence, parties, approval_rules }, settings_summary: [{ key, value,
set_by, seq }], conventions: [{ id, kind, title, summary }], open_items:
[{ id, title, due }], projects: [{ id, name, status, kind? }],
@@ -144,8 +120,7 @@ settings-and-conventions section says how to use them.
to fit say so in `truncated` ("conventions: 4 more (books.conventions)").
`projects` lists the active and paused projects by name, at most 20
("projects: N more (books.projects)" past that).
`next_steps`, in order: the format upgrade's (when waiting);
`books.import_settings` (books that still read a settings file); the
`next_steps`, in order: the format upgrade's (when waiting); the
onboarding step (`books.handbook {section: "contract"}`, when the chart
is empty or `focus` is `onboarding`); `books.open_items` (an item due
within 30 days or overdue); `books.backup` (none set up, or one stale,
@@ -224,20 +199,12 @@ as it is made.
## Transports
- **stdio** (default): `ledger-mcp --data-dir PATH`. For Claude Code,
Claude Desktop and other local agents. No auth: the OS user boundary is
the only boundary.
- **Streamable HTTP**: `ledger-mcp --data-dir PATH --http 127.0.0.1:8080`.
Serves MCP at `http://127.0.0.1:8080/mcp` via rmcp's tower integration.
**There is no authentication on this transport today**: no token and
no Origin check. The only guard is rmcp's default `Host` check, which
accepts `localhost`, `127.0.0.1` and `::1`. Any local process that can
reach the port can read and change the books. Bind it to loopback only;
binding a LAN, tailnet or public interface is an explicit human
decision, never a default. Auth, scopes and Origin validation arrive
with the served books (`plan/served.md`, P2).
Books are served one way (plan D-44): by the books server, which agents
reach through the bridge.
- **Served books** (`wanderbooks server`, started by the host; an agent
connects with `wanderbooks mcp --book ID`, a stdio bridge): every book
connects with `wanderbooks mcp --book ID`, the bridge: stdio to the
agent, the server's socket upstream): every book
in the data root, on a socket in the root's private `run/` folder that
only this account can connect to. Every request carries a bearer token
live for that book (refused 401 without one, 403 with another book's).
@@ -305,8 +272,9 @@ as it is made.
The MCP `initialize` response carries an `instructions` string, the
**connect text** (`ledger-mcp/src/handbook.rs`), composed in `get_info`
from the settings in force when the client connects (the settings are
re-read first, so a client connecting after an edit is told the new rule).
from the settings in force when the client connects (the books' settings
are taken first, so a client connecting after a change is told the new
rule).
It is short on purpose: clients cut long instructions (one cut the old
text at 4,096 characters), so it is at most **1,400 characters**, and its
first paragraph, at most **512**, stands alone: the entry point
@@ -327,9 +295,7 @@ the books); that the invoice, bill, payment and payroll tools post at
once, unchecked (in those words, naming no tool); TINs
only into the vault; show the numbers, and after posting check
`books.digest` and offer it to the human; and where settings live: in the
books, changed only through the tools, or, for books that still read a
settings file, that file, the one local file the agent may edit until
`books.import_settings` moves it in. When a long rule (many
books, changed only through the tools. When a long rule (many
accounts, a huge threshold, a long name) would push it past 1,400
characters, it drops the tracing hint first (the handbook's
tracing-figures section has it), then the reasons in parentheses, then the
@@ -368,17 +334,15 @@ path, the data dir or its flag, the old import-root flag, a file of the books
(`.jsonl`, the database, `vault.key`, `.gitignore`), git, GitHub, a pull
request, a repo doc (`docs/`, `.md`), or tells the agent to read the
books' files; no tool title or description, connect text or handbook
section names `wanderbooks.toml` (the input and output schemas name it by
design, for `books.settings`' `books_toml`) (`tool_text_lint`). A rule set by law that the books
section names `wanderbooks.toml` (`tool_text_lint`). A rule set by law that the books
don't know for a year names its official source (the statute or
publication, with its address) and says to tell the human.
Tool descriptions are at most 700 characters; eight longer ones are listed
in `tool_descriptions_fit_700_characters`, to be shortened in P5.
**Annotations.** Every tool has a `title` and a `readOnlyHint`; every tool
that changes anything also has a `destructiveHint`, true for these 29:
`books.post_entry`, `books.reverse_entry`, `books.import_settings`,
`books.set_setting`, `books.retire_convention`, `books.unlink_receipt`,
that changes anything also has a `destructiveHint`, true for these 28:
`books.post_entry`, `books.reverse_entry`, `books.set_setting`, `books.retire_convention`, `books.unlink_receipt`,
`books.unlink_attachment`, `books.withdraw_attachment`,
`books.correct_asset`, `books.dispose_asset`, `books.run_depreciation`,
`books.run_schedules`, `books.run_recurring`, `books.renumber_account`,
@@ -388,11 +352,10 @@ that changes anything also has a `destructiveHint`, true for these 29:
`books.set_tin` (both overwrite a vault record), `books.purge_tin`,
`books.create_invoice`, `books.create_bill`, `books.receive_payment`,
`books.pay_bill`, `books.run_payroll`, `books.remit_payroll_taxes`; and an
`idempotentHint` on the eleven that a repeat with the same arguments leaves
`idempotentHint` on the ten that a repeat with the same arguments leaves
the journal and the vault as they were: `books.verify`,
`books.run_depreciation`, `books.run_schedules`, `books.run_recurring`,
`books.set_party_roles`, `books.set_setting`, `books.import_settings`,
`books.set_vendor_1099`, `books.set_entry_instrument`,
`books.set_party_roles`, `books.set_setting`, `books.set_vendor_1099`, `books.set_entry_instrument`,
`books.update_project` and
`books.back_up_now` (a setter such as `books.update_party`,
`books.set_fx_rate` or `books.set_tin` journals a line, and `books.set_tin`
@@ -407,11 +370,12 @@ ones with TINs, so they are not read-only. No tool carries
`requiresUserInteraction` (owner decision
2026-10-16: no added prompts). `annotations_complete` pins all of this.
The read-only hint is true in behaviour: `spec_mcp` checks, on every call,
that a read-only tool leaves the journal, the contents of every file under
the data dir (the database by every row of every table; SQLite's -wal and
-shm files, which move pages and hold read marks on their own, aside),
the vault and the settings file as they were
(`read_only_tools_change_nothing`). `books.show` is read-only though it
that a read-only tool leaves the journal and the contents of every file of
the books (their folder, its vault, the database by every row of every
table; SQLite's -wal and -shm files, which move pages and hold read marks
on their own, aside, and what every call writes by design: the access
log, the viewer's side channel, the books' own backup, files handed over)
as they were (`read_only_tools_change_nothing`). `books.show` is read-only though it
writes the viewer's request into the books' `ops/` folder: that folder
is a side channel, never journaled, and moves only the view.
@@ -927,12 +891,11 @@ through the policy.
`error` or `refused` (anything else is refused). `total` counts the
whole log; `more` says older records match too. The log is read whole
and its chain checked first: a damaged log is reported, never shown in
part. Books served over stdio keep no access log: the tool is not
offered there (and is refused if called).
part.
## Tools — backups
Books served from the data root back themselves up (plan 3.7, D-30,
Books back themselves up (plan 3.7, D-30,
D-31): each keeps `archive.git`, a bare repository whose commits are its
journal, stored files, vault, keys and access log as they were (one
commit per backup, made under the store's lock, its message
@@ -968,10 +931,9 @@ minutes, then tried again, written to or not; the other targets don't
wait for it. Every
commit, push and drill is a record in the book's `ops/backup.jsonl` (a
chain log; a backup never moves the books' head), each target's records
keyed by the target itself. Setting `backup.*` (books.set_setting, or
books.import_settings with a `[backup]` table) needs books:admin, as
keyed by the target itself. Setting `backup.*` (books.set_setting) needs
books:admin, as
backing up does: a backup is the whole book, its vault and key included.
Over stdio neither tool is offered.
- `books.backup {}` (books:read) → `{ head_seq, targets: [{ target:
"repository" | "folder", at, last_ok_at, through_seq, stale,
@@ -1175,7 +1137,7 @@ first entry's).
the package's `as_of`; so `books.vendor_1099` given the package's
`cash_accounts`, `financing_accounts` and `through` = `as_of` (and its
`include_tins`) hands over the package's `1099-worksheet-<year>.csv`
byte for byte (the tool names one with TINs `-with-tins`; checked by `ledger-mcp` `oracle_mcp.rs`
byte for byte (the tool names one with TINs `-with-tins`; checked by `ledger-cli` `oracle_mcp.rs`
`cpa_package_holds_the_year_and_its_evidence`).
## Tools — parties
@@ -1420,8 +1382,8 @@ and the CPA package carries them.
missing.
- **Never out:** no tool output, error, or log holds more than a TIN's
last four, except `books.vendor_1099 { include_tins: true }`; the CPA
package carries them only in its zip, handed over as an export (or
inline over stdio), never written to a path.
package carries them only in its zip, handed over as an export, never
written to a path.
The tool arguments that take a TIN never echo it, even when refused.
- **TIN facts:** any party (any role) may have `tin` facts:
`{ tin_type: "ssn" | "ein" | "itin", last4, vault_record?, purged? }`,
@@ -1460,13 +1422,12 @@ and the CPA package carries them.
- **Handed files** (D-29): no tool takes a path. A file the books make
for an agent (`books.get_w9`'s form, `books.vendor_1099`'s `csv`,
`books.export_cpa_package`'s zip) comes back as a `HandedFile {
file_name, mime_type, size_bytes, sha256, download?, expires_at?,
content_base64? }`. On books served by `wanderbooks mcp` it is an
file_name, mime_type, size_bytes, sha256, download, expires_at }`: an
export, kept 15 minutes for the token that asked (at most 16 per
token, swept like uploads), and `download` is the command that saves
it in a folder the human names: `wanderbooks download --book B
[--token NAME] ex_… --to <a folder>` (`GET /b/{book}/x/{export}`,
"Transports"). Over stdio it comes back inline as `content_base64`.
"Transports"). Its bytes never come back inline (D-44).
A file with full TINs never goes into the books: the server keeps, beside the vault, the SHA-256 of every
file it read or wrote with full TINs (the W-9 `books.set_w9` reads,
recorded once the call has passed every check, just before the vault
@@ -1544,9 +1505,7 @@ never pass through the agent), or `content_base64` with its `sha256`
(at most 256 KiB decoded; longer text is refused before it is decoded).
Never both, never neither. Inline bytes that aren't base64, or whose
SHA-256 isn't the one given, are refused (guard `base64_corrupt`): send
the file as an upload. Uploads exist only for books served from the
data root (the server's socket carries the bytes); over stdio, only
inline, and the refusal says so and to tell the human. On the socket an
the file as an upload. An upload is kept for the token that began it. An
MCP request is at most 1 MiB. A file's name and type are checked when
it is filed (every filing tool, `books.begin_upload`, and the W-9's
name): a name of 1 to 255 bytes, with no `/` or `\`, not `.` or `..`,
@@ -1617,8 +1576,7 @@ and no control or invisible (Unicode format) characters; a MIME type
`download`, the command `wanderbooks download --book B <id> --to <a
folder>` (with `--token NAME` when the caller's token isn't the
default `agent`; `GET /b/{book}/a/{attachment}`, "Transports"), which
saves a copy as a new file in that folder. Over stdio there is no
download: a larger file is refused.
saves a copy as a new file in that folder.
- `books.file_document { category: "formation" | "legal" | "tax" |
"accounting" | "statement" | "contract" | "photo" | "warranty" |
"import_source" | "other", file_name?, mime_type?, upload_id? |
@@ -1641,8 +1599,7 @@ posting.
(account codes whose entries always need one, e.g. meals and travel)
and `policy.evidence.exempt_accounts` (e.g. automated bank fees; an
entry is exempt only when every line is in one). Changed with
`books.set_setting`; books from before P1 read `config.json`'s
`receipt_policy`.
`books.set_setting`.
- **Preview.** Every `post_entry` preview includes the receipt checklist
(`receipts: [...]`, `receipt_required: bool`, `receipt_missing: bool`,
`split_detected: [...]`), so the human sees it before confirming. A
@@ -1759,16 +1716,17 @@ posting.
## Tools — chart templates
- `books.chart_template { entity_type, owners? }` → `{ csv, settings_toml,
- `books.chart_template { entity_type, owners? }` → `{ csv, settings,
notes }`: a starter chart for a sole proprietorship (incl. single-member
LLC), partnership (incl. multi-member LLC), S corporation, or C
corporation — the equity and owner-pay accounts that entity needs (owner's
capital and draws; per-partner capital and distributions plus guaranteed
payments; officer compensation and shareholder distributions; dividends
and income tax expense), common income and expense accounts with tax
categories, and `settings_toml`, the settings that go with it (tax form,
members, where closes go) as settings-file TOML for
`books.import_settings`. A proposal: review with the human, then `books.import_chart_csv`.
categories, and `settings`, the settings that go with it (tax form,
members, where closes go) as `[{ key, value }]` changes for
`books.set_setting`. A proposal: review with the human, then
`books.import_chart_csv` and `books.set_setting`.
## Tools — tax categories
@@ -1877,7 +1835,8 @@ Hand-rolled on `ServerHandler` (rmcp 3.x has no resource macros):
view `meta`; no bytes)
Resources exist so read-only clients (dashboards, the viewer,
auditors) never need tool credentials. Tested end-to-end over stdio.
auditors) never need tool credentials. Tested end to end through a real
books server (ledger-cli's tests).
## The agent operating contract
+6 -6
View File
@@ -304,8 +304,7 @@ damn CPA package."
`books.export_cpa_package` (its K-1 worksheet, `k1-members.csv`, and its
1099 worksheet; `include_tins` defaults to true there) decrypt them.
The package is handed over as a file (an export the asking token
fetches with `wanderbooks download`, or inline over stdio); no tool
takes a path. The MCP server
fetches with `wanderbooks download`); no tool takes a path. The MCP server
never returns a TIN in any other tool, log, or error. Tests check that
no other output contains a nine-digit run.
- **Books without a vault** (another machine, books adopted without
@@ -331,10 +330,11 @@ damn CPA package."
is the one tool output with full TINs (with a warning; `csv: true`
also hands the worksheet over as a CSV file);
`books.export_cpa_package` carries them in its zip, handed over the
same way. A key file inside the books dir is
refused. `tests/spec_vault.rs` scans every tool response, the server's
log, and every file in the books dir outside `vault/` for the TINs in
any formatting.
same way. The key is the book's own (`keys/legacy.key` in its folder in
the data root). `tests/spec_vault.rs` (ledger-cli) scans every tool
response, every file handed over, the server's log, and every file in
the books' folder outside `vault/` (and the files handed over, kept for
their download) for the TINs in any formatting.
## Attachments
+38 -36
View File
@@ -757,7 +757,7 @@ shapes), `prop_pure_entries_sections_are_movements`; regressions
(`crates/ledger-core/tests/spec_cash_flow.rs`); the seam review's year
of books (`ledger-package` `tests/p3_seams.rs`, its books
`tests/seams/books.rs`; the viewer's `ledger-gui` `tests/p3_seams.rs`;
the tools' `ledger-mcp` `tests/p3_seams.rs`).
the tools' `ledger-cli` `tests/p3_seams.rs`).
## B-15 Depreciation: straight line, salvage never depreciated, partial months by days
@@ -943,7 +943,7 @@ dates, `accumulated_posted` and its edges,
`a_reinstated_run_counts_and_is_live`); `ledger-store`
`tests/asset_tags.rs` (`runs_replay_reload_and_verify_catches_a_tampered_run`,
`the_viewer_and_writer_agree_on_a_bad_depreciation_posted_payload`);
`ledger-mcp` `tests/spec_assets.rs` (`a_reversed_run_is_not_posted`,
`ledger-cli` `tests/spec_assets.rs` (`a_reversed_run_is_not_posted`,
`list_assets_book_value_is_what_the_books_carry`).
## B-16 Prepaid expenses and deferred revenue are recognized over the period benefited or earned
@@ -1003,8 +1003,8 @@ B-16 cites them (P3 depreciation review R7).
`a_schedule_month_posted_again_waits_for_month_reposting_and_replays`,
`daily_plans_pin_month_end_clamping`,
`prop_daily_plan_sums_exactly_and_lands_on_month_ends`,
`prop_monthly_even_plan_is_unchanged`; `ledger-mcp`
`mcp_daily_schedule_over_stdio`.
`prop_monthly_even_plan_is_unchanged`; `ledger-cli`
`mcp_daily_schedule`.
## B-17 Platform payouts are recorded gross when the business is the principal
@@ -1256,7 +1256,7 @@ bucket table is `kernel::payment_bucket` (Kani `payment_bucket_is_the_table`,
(ledger-core `tests/form1099.rs`);
`the_untagged_note_counts_only_money_out`,
`the_other_receipts_note_says_what_each_entry_did` (ledger-package); the
MCP stdio tests; bookspec
MCP tests through a real books server (ledger-cli); bookspec
`bookspec/examples/1099-*.toml`.
## B-20 Every transaction has supporting documents; records are kept
@@ -1341,7 +1341,7 @@ the books' figure, never the schedule's (P3, defect 13).
**Enforced:** `Ledger::set_asset_tag` / `check_asset_tag_for`,
`assets::check_asset_tag`, `assets::check_serial`. **Checked:**
`ledger-core` `tests/spec_asset_tags.rs`; `ledger-store`
`tests/asset_tags.rs`; `ledger-mcp` `tests/spec_assets.rs`.
`tests/asset_tags.rs`; `ledger-cli` `tests/spec_assets.rs`.
## B-25 Tax figures beside book value: the §723 basis, the depreciable basis, the §704(c) built-in gain
@@ -1418,7 +1418,7 @@ non-negative, in cents, at most one quadrillion) and its replay pairs
never assumed). **Checked:** `ledger-core` `tests/spec_asset_basis.rs`,
`tests/kernel_bridge.rs` (`basis_figures_agree`), `tests/properties.rs`
(`prop_basis_figures_laws`); `ledger-store` `tests/asset_tags.rs`;
`ledger-mcp` `tests/spec_assets.rs`.
`ledger-cli` `tests/spec_assets.rs`.
## B-26 Parties: one directory, no full TIN in the books
@@ -1447,8 +1447,9 @@ plain rule: a nine-digit job or PO number is written by its last four,
as everywhere else in the books.
The full TIN, when the books need it for the 1099 worksheet, lives in the
TIN vault (`vault/` in the books dir, git-ignored; encrypted under a key
file outside the books dir): `books.set_w9` `tin` writes it there and
TIN vault (`vault/` in the book's own folder in the data root, encrypted
under the book's own key, `keys/legacy.key` beside it): `books.set_w9`
`tin` writes it there and
journals only the last four and the vault record's hash; the signed W-9
goes there too, never to the attachment store. Any party may have its
TIN in the vault, whatever its roles: every member's (for their K-1) and
@@ -1472,7 +1473,7 @@ don't count toward the three: `sources/unicode-tin-text.md`, Unicode
worksheets carry every member's and payee's TIN by default) are never
written to a path: no tool takes one (D-29, P2 slice 6). Each is handed
over as an export the asking token fetches with `wanderbooks download`
(kept 15 minutes), or inline over stdio. (Until slice 6 they were
(kept 15 minutes), never inline (D-44). (Until slice 6 they were
written only outside the books dir: DECISION, P0 review, 2026-10-16, so
none could be filed back in as a receipt and read out again.) No file
holding a full TIN is filed into the books
@@ -1547,16 +1548,17 @@ W-9 facts agree), `set_entry_party`, `validate_payroll`; `ledger-core`
`k1` (the K-1 worksheet); `ledger-vault` (Tin::new, the encrypted
records, records_held, purge_records); `ledger-mcp` `books.set_w9`,
`books.set_tin`, `books.get_w9`, `books.purge_tin`, `books.vendor_1099`,
`books.export_cpa_package` (open_vault refuses a key file in the books
dir; outside_books for files with a TIN; vault_tin checks the vault
against the books); `ledger-mcp` `tin_files` (the registry, the text
`books.export_cpa_package` (open_vault opens the book's own key;
vault_tin checks the vault against the books); `ledger-mcp` `tin_files`
(the registry, the text
scan and its probe bound, on every filing tool; `known_tin_last4` gives
the books' last fours).
**Checked:** `ledger-core` `tests/spec_parties.rs`; `ledger-store`
`tests/parties.rs`; `ledger-vault` `tests/vault.rs`; `ledger-mcp`
`tests/spec_parties.rs`, `tests/spec_vault.rs` (every response, the log,
and every file in the books dir outside `vault/` scanned for the TINs in
any formatting), `tests/tin_files.rs`
`tests/parties.rs`; `ledger-vault` `tests/vault.rs`; `ledger-cli`
`tests/spec_parties.rs`, `tests/spec_vault.rs` (every response, every
file handed over, the server's log, and every file in the books' folder
outside `vault/` and the files kept for their download scanned for the
TINs in any formatting), `tests/tin_files.rs`
(`a_tin_bearing_file_is_refused_from_every_session_and_after_a_restart`,
`text_with_a_vault_tin_or_a_printed_one_is_refused_and_a_bank_export_is_not`,
`text_probing_for_a_tin_is_refused_alike_by_every_filing_tool`), and the
@@ -1738,7 +1740,7 @@ ledger-gui `owner.rs` (the notice and the dialog).
`an_old_reader_never_repairs_newer_books`,
`a_blank_line_is_corrupt_for_the_viewer_and_the_writer`,
`damaged_journals_are_still_corrupt`) and the unit tests in `format.rs`;
`ledger-gui` `tests/newer_journal.rs`; `ledger-mcp` `tests/startup.rs`;
`ledger-gui` `tests/newer_journal.rs`; `ledger-cli` `tests/startup.rs`;
the `fuzz_store_replay` target (the writer and the viewer agree on newer
versus bad). The upgrade: `ledger-store` `tests/format_upgrade.rs` (new
books in this version's format, a change waiting for the upgrade, every
@@ -1866,7 +1868,7 @@ replay pair), `calendar.rs`, the unit tests of `amortization_plan`,
`tests/fixtures/old-books`, written by main 188c90ea, opens in the writer
and the viewer and replays to exactly the figures main computed) and
`every_figure_over_old_books_is_computed_or_refused`, `tests/bounds.rs`;
`ledger-mcp` `tests/hostile_args.rs` (every tool with generated hostile
`ledger-cli` `tests/hostile_args.rs` (every tool with generated hostile
arguments and every resource with generated URIs: the server never
stops, and the books reopen after; the inputs that once panicked,
pinned; every tool over every old book) and `server.rs`
@@ -1887,8 +1889,7 @@ kept in the journal like every other fact, each with who decided it and
when (journal format 2: `settings.set`, `convention.set`,
`convention.retired`, `decision.recorded`, `open_item.raised`,
`open_item.resolved`, `entry.approved`). A settings file is only text to
import from or export to; books that keep their own settings never read
one.
import from or export to; no server reads one (D-44).
- A setting is one of the known keys, checked by its key and kept in one
spelling; a change is all or nothing, and the settings after it hold
@@ -1905,9 +1906,9 @@ one.
fiscal year, the cash flow roles, the members) present them by the
settings in force; the fiscal year can't move once a period is closed
under it (a changed tax year needs the IRS's consent, Form 1128).
- Moving a settings file in reads every setting exactly as the server read
the file: the text must be the file the server reads, and books that
keep their own settings take no import that changes them.
- Books keep their own settings, and only `books.set_setting` changes
them; old books bring their settings file in once, whole, as they are
adopted (`wanderbooks books adopt`), and nothing reads one after.
- A setting key or convention kind this version doesn't know is a newer
version's: the books are "newer", never "damaged" (B-27).
- A posting template's lines split the amount exactly on each side, odd
@@ -1933,8 +1934,9 @@ one.
(format 2 declared before the first such line); `ledger_config::codec`
(`import`, `export`); `Store::post_entry` and `approval_gate`
(`ApprovalRequired`, `ApprovalNeedsDraft`), `check_renumber_account`;
`ledger-mcp` `books.import_settings` (the file the server reads, whole),
`books.post_entry` (`approval_required`).
`ledger-mcp` adoption (`local.rs` `settings_to_move`: a settings file,
whole, through `codec::import`), `books.post_entry`
(`approval_required`).
**Checked:** `ledger-core` settings and conventions unit tests;
`ledger-store` `tests/settings.rs` (`settings_replay_is_pure`, 256 cases
per check, 2,000 by hand, with posts between records and every record
@@ -1943,7 +1945,7 @@ checked to change no figure or entry; `approval_rules_off_by_default`,
`a_newer_key_or_kind_is_newer`;
`books_written_under_p1_open_to_their_record`, fixture
`old-books/p1_record`); `ledger-config` `tests/codec_props.rs`
(`exports_round_trip`) and codec unit tests; `ledger-mcp`
(`exports_round_trip`) and codec unit tests; `ledger-cli`
`tests/spec_kept.rs`; Lean `Step.record_changes_no_figure`,
`Reachable.records`; fuzz `fuzz_settings_replay`.
@@ -2008,8 +2010,8 @@ generated descriptions (`described`) and parameter docs; `ledger-gui`
`Books::periods`, `default_period`, `fiscal_ytd`, `App::show`,
`RegisterRange`. **Checked:** `ledger-core` `calendar.rs` unit tests and
`tests/properties.rs` `prop_period_defaults_resolve`; `ledger-mcp`
`server.rs` `period_tools_describe_the_one_default`, `tests/mcp_stdio.rs`
`one_default_period_everywhere_defect_16` and
`server.rs` `period_tools_describe_the_one_default`; `ledger-cli`
`tests/mcp_journeys.rs` `one_default_period_everywhere_defect_16` and
`the_default_period_never_shortens_the_iif_or_the_package`,
`tests/oracle_mcp.rs`
`show_sends_the_dates_it_names_defect_16`,
@@ -2072,7 +2074,7 @@ books declare before their first rename, B-27); `books.rename_account` (scope
(`Cmd::Rename`); `ledger-store` `tests/store.rs`
(`rename_account_changes_only_the_name_defect_15`,
`prop_rename_changes_no_figure`, `a_journaled_rename_replays_past_the_command_rules`),
`tests/format.rs`; `ledger-mcp` `tests/mcp_stdio.rs`
`tests/format.rs`; `ledger-cli` `tests/mcp_journeys.rs`
(`rename_account_end_to_end_defect_15`), `tests/hostile_args.rs`;
`ledger-core` `tests/spec_ledger.rs`
(`a_new_name_is_one_trimmed_line_of_bounded_length`), `ledger-store`
@@ -2135,7 +2137,7 @@ their first `settings.set` naming the key, B-27); `ledger-mcp` `books.add_accoun
(Info under warn, Todo under refuse). **Checked:** `ledger-core`
`tests/tax.rs`; `ledger-store` `tests/store.rs`
(`prop_tax_policy_refuses_exactly`, `a_journaled_clear_replays_under_refuse`);
`ledger-mcp` `tests/spec_tax_policy.rs`; `ledger-gui` `tests/view_model.rs`
`ledger-cli` `tests/spec_tax_policy.rs`; `ledger-gui` `tests/view_model.rs`
(`needs_attention_flags_every_uncategorized_pl_account_defect_8`);
bookspec `examples/tax-category-required.toml`.
@@ -2188,7 +2190,7 @@ fixed asset register, mid-year and at year end; `as_of_package_iif_stops_at_as_o
`the_1099_worksheet_stops_at_as_of`,
`one_list_of_cash_accounts_per_package`,
`payments_to_a_vendor_come_from_the_packages_accounts`; the property
`iif_ties_to_the_trial_balance_as_of`, 2,000 cases); `ledger-mcp`
`iif_ties_to_the_trial_balance_as_of`, 2,000 cases); `ledger-cli`
`oracle_mcp.rs` (`cpa_package_holds_the_year_and_its_evidence`: the
package's CSV is the tool's for the same accounts and `through`); the
bookspec examples `bookspec/examples/as-of-package-stops-at-as-of.toml`
@@ -2349,10 +2351,10 @@ pinned); `tests/spec_import.rs` (`iif_docnum_is_the_entry_id`,
`the_bookspec_iif_example_matches_the_engine`, and B-33's); the bookspec
example `bookspec/examples/iif-quickbooks-journal-file.toml` (formats
appendix: the file line by line and its changes, derived by hand);
`ledger-mcp` `oracle_mcp.rs`
`ledger-cli` `oracle_mcp.rs`
(`iif_declares_accounts_by_role_and_balances_every_transaction`,
`iif_without_as_of_is_as_of_today_and_ties_to_the_trial_balance`),
`mcp_stdio.rs` (`mcp_import_export_over_stdio`,
`mcp_journeys.rs` (`mcp_import_export`,
`the_default_period_never_shortens_the_iif_or_the_package`).
## B-35 A party's part of an entry, and a party's totals
@@ -2582,7 +2584,7 @@ distributions declared or of depreciated property, the properties), Kani
`reverse_target_is_an_involution`, bridge `party_target_agrees`; Lean
`reverseTarget_involutive`, `capitalTarget_odd`, `partyTarget_odd`,
`sumOver_movement_reversal`; `ledger-store` `tests/party_creditors.rs`;
`ledger-mcp` `tests/spec_party_totals.rs`; `ledger-gui` `tests/parties.rs`.
`ledger-cli` `tests/spec_party_totals.rs`; `ledger-gui` `tests/parties.rs`.
## B-36 The K-1 worksheet reports tax-basis capital beside book capital
@@ -2835,7 +2837,7 @@ period_margin_pct, period_lines}`, `kernel::unrecovered_cost`;
an invoice's or bill's page (each line's project).
**Checked:** `ledger-core` `tests/spec_projects.rs` and
`tests/spec_document_projects.rs`, `ledger-store` `tests/projects.rs` and
`tests/document_projects.rs`, `ledger-mcp` `tests/spec_projects.rs`,
`tests/document_projects.rs`, `ledger-cli` `tests/spec_projects.rs`,
`ledger-gui` `tests/projects.rs`, `ledger-cli` `tests/projects_parity.rs`
and `tests/format_upgrade.rs` (`books_p2_served_cut_over_whole`); bookspec
`projects-net-is-its-tagged-lines`,
+3 -2
View File
@@ -1,7 +1,8 @@
# JSON-RPC 2.0 Specification (jsonrpc.org/specification)
Governs `ledger-mcp`'s wire protocol (MCP is JSON-RPC 2.0). Fetched
2026-10-14.
Governs the wire protocol of the books server (`ledger-mcp`) and of the
agents' bridge, which frames each message an agent sends as one line
(`ledger-host` bridge.rs) (MCP is JSON-RPC 2.0). Fetched 2026-10-14.
**5 Response object**
+4 -3
View File
@@ -42,6 +42,7 @@ docs/specification/2025-06-18/server/tools.mdx (MIT). Fetched 2026-10-14.
> - Rate limit tool invocations
> - Sanitize tool outputs
(A local stdio server for one bookkeeper: access control is the file
system's, and rate limiting has no one to limit; wanderbooks takes
"validate all tool inputs" and "sanitize tool outputs" as binding.)
(A local books server for one company's agents: access control is the
bearer tokens and their scopes on the data root's private sockets, and
rate limiting has no one to limit; wanderbooks takes "validate all tool
inputs" and "sanitize tool outputs" as binding.)
+183 -57
View File
@@ -14,7 +14,7 @@ promise.
|---|---|---|---|---|---|
| Spec-derived examples | written specs with sources (1) | `tests/spec_*.rs`, `invariants.rs`, `statements.rs`, `assets.rs`, ...; the store's `tests/spec_store.rs` (written blind, from its specs only) | `just check` | seconds | every change |
| Regressions | the bug, named (1) | beside the code they cover, `*_regression`/named for the defect | `just check` | seconds | every change |
| Compatibility contracts | journals written under older rules must open, and replay to the figures their version computed (1) | `ledger-store/tests/store.rs` (`books_written_under_older_rules_still_open`; `books_an_earlier_version_wrote_open_to_its_figures` over `tests/fixtures/old-books`: books main 188c90ea wrote, with main's own figures in `expected.json`, in the writer and the viewer; `every_figure_over_old_books_is_computed_or_refused`); `ledger-mcp/tests/hostile_args.rs` `old_books_answer_every_tool`; `ledger-gui/tests/old_books.rs` | `just check` | ~30s | every change |
| Compatibility contracts | journals written under older rules must open, and replay to the figures their version computed (1) | `ledger-store/tests/store.rs` (`books_written_under_older_rules_still_open`; `books_an_earlier_version_wrote_open_to_its_figures` over `tests/fixtures/old-books`: books main 188c90ea wrote, with main's own figures in `expected.json`, in the writer and the viewer; `every_figure_over_old_books_is_computed_or_refused`); `ledger-cli/tests/hostile_args.rs` `old_books_answer_every_tool`; `ledger-gui/tests/old_books.rs` | `just check` | ~30s | every change |
| Properties | laws over all inputs (2) | `properties.rs`, `spec_*.rs`, `kernel_bridge.rs` | `just check` | seconds | every change |
| Metamorphic | scaling, reversal, ordering invariance (2) | `spec_reports.rs` | `just check` | seconds | every change |
| Refusal law (store) | a refused command never reaches the journal, an accepted one always does; the live ledger equals its journal's replay in every part, and the books reopen to it (2, 3) | `store.rs` `refused_commands_never_reach_the_journal` | `just check` (`STORE_CASES=5000` deeper) | ~20s | every change |
@@ -56,26 +56,26 @@ that would fail if it broke:
| Law | Test |
|---|---|
| Books from a newer wanderbooks are "newer", never "corrupt", from the writer, the viewer and the server; damage stays corruption (rules.md B-27) | `ledger-store` `tests/format.rs` (`old_reader_gets_newer_journal`, `damaged_journals_are_still_corrupt`); `ledger-gui` `tests/newer_journal.rs`; `ledger-mcp` `tests/startup.rs` |
| Books from a newer wanderbooks are "newer", never "corrupt", from the writer, the viewer and the server; damage stays corruption (rules.md B-27) | `ledger-store` `tests/format.rs` (`old_reader_gets_newer_journal`, `damaged_journals_are_still_corrupt`); `ledger-gui` `tests/newer_journal.rs`; `ledger-cli` `tests/startup.rs` |
| A line the gate refuses is called newer only once the next line's `prev` checks its hash; a last line is "newer or damaged" | `a_damaged_line_that_looks_newer_is_corrupt_when_the_next_line_checks_it`, `an_unreadable_last_line_is_reported_as_newer_or_damaged`, `a_newer_line_the_next_line_chains_onto_is_newer_and_intact` (the message vouches for that line only) |
| The envelope and its hash are frozen across formats: a next line in a newer format (a float, a string seq, keys of its own) settles by its `prev` alone, never as corrupt; a key beside the envelope is newer, never dropped, and still checked through; a refused line followed by an unreadable one is "newer or damaged" | `format.rs` `a_newer_format_frames_its_lines_its_own_way`, `an_unknown_envelope_key_is_newer_and_checked_through`, `a_refused_line_followed_by_a_garbled_line_is_unverified` |
| The writer and the viewer read bytes alike: a line that isn't UTF-8 is corrupt to both; an older version never repairs (truncates) newer books | `a_line_that_is_not_utf8_is_corrupt_for_the_viewer_and_the_writer`, `an_old_reader_never_repairs_newer_books` |
| A newer format is newer whatever its features look like; a blank line is damage to the viewer as to the writer | `format.rs` `a_newer_format_is_newer_whatever_its_features_look_like`; `a_blank_line_is_corrupt_for_the_viewer_and_the_writer` |
| A request that panics stops the server: every later request, on every session, is refused | `server.rs` `a_request_that_panics_stops_the_server` |
| No input reaches a panic (each would stop the server for every session): every tool with arguments generated from its schema and made hostile (wrong types, u64::MAX, decimals of 30+ digits or absurd exponents, dates like 0000-00-00, 99999-12-31, +262142-12-31, 2026-02-30, periods like 999999-01, control and format characters, long strings), every resource URI generated; the inputs that once panicked, pinned; after each property the server restarts and the books reopen (a call a command accepts but replay refuses would brick them); every tool and resource over every old book (rules.md B-28) | `ledger-mcp` `tests/hostile_args.rs` `hostile_tool_arguments_never_stop_the_server` (with a floor: most calls reach a tool, some succeed), `hostile_resource_uris_never_stop_the_server`, `inputs_that_once_panicked_are_refused_or_answered`, `old_books_answer_every_tool`; `MCP_CASES` and `OLD_BOOKS_CASES` (default 3 per tool per book) set the cases |
| No input reaches a panic (each would stop the server for every session): every tool with arguments generated from its schema and made hostile (wrong types, u64::MAX, decimals of 30+ digits or absurd exponents, dates like 0000-00-00, 99999-12-31, +262142-12-31, 2026-02-30, periods like 999999-01, control and format characters, long strings), every resource URI generated; the inputs that once panicked, pinned; after each property the server restarts and the books reopen (a call a command accepts but replay refuses would brick them); every tool and resource over every old book (rules.md B-28) | `ledger-cli` `tests/hostile_args.rs` `hostile_tool_arguments_never_stop_the_server` (with a floor: most calls reach a tool, some succeed), `hostile_resource_uris_never_stop_the_server`, `inputs_that_once_panicked_are_refused_or_answered`, `old_books_answer_every_tool`; `MCP_CASES` and `OLD_BOOKS_CASES` (default 3 per tool per book) set the cases |
| Dates are in 0001-01-01 through 9999-12-31 and amounts at most a line's, for every new command; replay accepts every line an earlier version accepted (each bound has its pair: the command refuses past it, the replayed old line is accepted); a figure derived past exact arithmetic or chrono's calendar is refused, never a panic; a recurring run makes at most 1,000 drafts (rules.md B-28) | `ledger-core` `tests/spec_bounds.rs`, `calendar.rs` tests, `plans_at_the_edges_are_computed_or_refused`, `intervals_reach_chronos_last_day`, `a_vendor_part_is_scaled_exactly_or_refused`, the `share_property` cases; `ledger-store` `tests/bounds.rs`, `tests/store.rs` (the old-books tests); `server.rs` `periods_are_a_year_and_a_month`, `dates_are_four_digit_years_and_real_days` |
| Opening the books changes nothing until every complete line passes in memory (chain, gate, first line, replay); the viewer applies the writer's first-line rule (rules.md B-27) | `format.rs` `a_bad_complete_line_before_a_torn_tail_leaves_the_journal_as_it_was`, `the_first_line_is_store_created_for_the_viewer_and_the_writer`; `fuzz_store_replay` (the first line varies) |
| UTF-16 text without a byte-order mark is scanned like any text; a refused `books.set_w9` marks no file; a refused resource read never echoes a TIN | `tin_files.rs` (tests) `utf16_text_without_a_byte_order_mark_is_scanned_either_way_round`, `a_refused_set_w9_never_marks_its_file`, unit test `only_nul_interleaved_bytes_read_as_utf16_without_a_mark`; `spec_vault.rs` `resource_refusals_never_echo_a_tin` |
| idempotentHint only where a repeat with the same arguments leaves the journal and the vault as they were; every tool's hints pinned in one table | `spec_vault.rs` `idempotent_tools_change_nothing_when_repeated`; `annotations_complete` |
| books.settings lists every receipt-required and exempt account, uncut; a fresh company is "a new company"; the lint catches `just` however it ends and where the books are | `mcp_stdio.rs` `every_receipt_account_left_out_of_the_connect_text_is_in_books_settings`; `handbook.rs` `a_fresh_company_is_a_new_company`; `tool_text_lint_catches_helpers_and_locations` |
| A file with full TINs is never written to a path (handed over instead, D-29), and never filed into the books: every file the server read or wrote with TINs (from any session, after a restart, out of a CPA package), by every filing tool | `mcp_stdio.rs` `tin_bearing_files_are_refused_when_given_back_to_be_filed`; `spec_vault.rs` `a_package_is_handed_over_and_never_written`; `tin_files.rs` (tests) `a_tin_bearing_file_is_refused_from_every_session_and_after_a_restart`; unit test `a_recorded_tin_file_is_refused_by_every_reader_of_the_registry` |
| books.settings lists every receipt-required and exempt account, uncut; a fresh company is "a new company"; the lint catches `just` however it ends and where the books are | `mcp_journeys.rs` `every_receipt_account_left_out_of_the_connect_text_is_in_books_settings`; `handbook.rs` `a_fresh_company_is_a_new_company`; `tool_text_lint_catches_helpers_and_locations` |
| A file with full TINs is never written to a path (handed over instead, D-29), and never filed into the books: every file the server read or wrote with TINs (from any session, after a restart, out of a CPA package), by every filing tool | `mcp_journeys.rs` `tin_bearing_files_are_refused_when_given_back_to_be_filed`; `spec_vault.rs` `a_package_is_handed_over_and_never_written`; `tin_files.rs` (tests) `a_tin_bearing_file_is_refused_from_every_session_and_after_a_restart`; unit test `a_recorded_tin_file_is_refused_by_every_reader_of_the_registry` |
| Text filed into the books is refused when it holds a TIN the vault keeps (nine digits, 3-2-4 by hyphen, space or dot, 2-7 by hyphen or space, anywhere) or a stand-alone printed SSN or EIN (123-45-6789, 12-3456789), and only then: a bank export (routing numbers, long references, a date next to an amount) and a longer number holding an SSN shape go in; a locked vault leaves the printed shapes (rules.md B-26) | `tin_files.rs` (tests) `text_with_a_vault_tin_or_a_printed_one_is_refused_and_a_bank_export_is_not`; unit tests `text_holding_a_vault_tin_is_refused_in_every_rendering`, `a_printed_ssn_or_ein_is_refused`, `a_longer_number_holding_a_tin_shape_passes`, `an_ordinary_bank_export_passes`, `binary_bytes_are_not_scanned` |
| Filing is no TIN oracle: text with more than two TIN-shaped numbers ending in a known TIN's last four (the books' TIN and W-9 facts, then the vault's own) is refused before the vault is opened, byte for byte alike whether one is the vault's TIN or none is, the vault open or locked, by every filing tool; one or two such numbers are compared against the vault, one number written many ways counts once, and other last fours never count (rules.md B-26) | `tin_files.rs` (tests) `text_probing_for_a_tin_is_refused_alike_by_every_filing_tool`; unit tests `text_probing_for_a_tin_is_refused_alike_whether_or_not_it_holds_it` (the vault never opened for a probe), `one_or_two_candidates_are_compared_against_the_vault` |
| The connect text fits its budgets, its lead stands alone, it states the receipt rule enforced (exact threshold in the base currency, required and exempt accounts, reversals) and the parties rule in force, and the company name can't break it (control and format characters) | `handbook.rs` `instructions_budget`, `a_typical_company_gets_the_whole_connect_text`, `the_company_name_is_one_plain_line`; `receipt_policy.rs` `the_rule_told_is_the_rule_enforced`, `a_long_or_odd_account_list_stays_short_and_plain`; a new session is told the settings in force: `a_new_session_is_told_the_settings_in_force` |
| No text the agent is given (whole tool JSON, resources, connect text, handbook, every error template and refusal) points at files, git, GitHub, repo docs, flags or helper commands; descriptions fit 700 characters (listed exceptions, P5; and what git itself said of a failed backup, which `books.backup` passes on to the human as git said it, its paths hidden) | `tool_text_lint`, `tool_descriptions_fit_700_characters`; `ledger-core` `an_unknown_year_threshold_is_never_guessed` |
| Every tool is annotated (openWorldHint false on all but `books.back_up_now`), and the read-only hint is true in behaviour (journal, every data-dir file's contents but `ops/`, the viewer's side channel for `books.show`, which is not the books; the database's rows, the vault and the settings unchanged) | `annotations_complete`; `spec_mcp.rs` `read_only_tools_change_nothing` (checked on every call in `Mcp::tool`) |
| A bare (or empty-`since`) digest covers this month so far, on any day of it, as the server counts it (the stdio test reads the server's own month, and starts over if it turns) | `a_bare_digest_covers_this_month_so_far`; `mcp_stdio.rs` `digest_resource_bare_reads` |
| The handbook: every section, "all" in order, the schema names every id | `handbook_sections_render_the_rule_in_force`, `the_whole_handbook_is_every_section_in_order`, `mcp_stdio.rs` `handbook_tool_and_resources_over_stdio` |
| Every tool is annotated (openWorldHint false on all but `books.back_up_now`), and the read-only hint is true in behaviour (journal, every file of the books' folder but what every call writes by design: the access log, `ops/` (the viewer's side channel for `books.show`, which is not the books), the books' own backup and the files handed over; the database's rows and the vault unchanged) | `annotations_complete`; `spec_mcp.rs` `read_only_tools_change_nothing` (checked on every call in `Mcp::tool`) |
| A bare (or empty-`since`) digest covers this month so far, on any day of it, as the server counts it (the end-to-end test reads the server's own month, and starts over if it turns) | `a_bare_digest_covers_this_month_so_far`; `mcp_journeys.rs` `digest_resource_bare_reads` |
| The handbook: every section, "all" in order, the schema names every id | `handbook_sections_render_the_rule_in_force`, `the_whole_handbook_is_every_section_in_order`, `mcp_journeys.rs` `handbook_tool_and_resources` |
## P0 mutation testing (2026-10-16)
@@ -105,9 +105,9 @@ config and 1 in the viewer. Each was killed by a test of the law it
broke, removed by simpler code, or is equivalent:
- **Killed (MCP).** A section's title is its heading
(`a_title_is_the_heading_text`; over stdio the handbook tool titles a
(`a_title_is_the_heading_text`; through the server the handbook tool titles a
section by its heading and "all" as the whole handbook,
`handbook_tool_and_resources_over_stdio`). A `.jpg` or `.jpeg` W-9
`handbook_tool_and_resources`). A `.jpg` or `.jpeg` W-9
was taken (`every_w9_file_type_is_taken`); P2 slice 5 judges the W-9
by its first bytes, not its name, so that mutant's code is gone (the
test now checks each type by its bytes). The waker wakes: a request pending until another thread wakes it completes
@@ -116,9 +116,9 @@ broke, removed by simpler code, or is equivalent:
root dropped, never fatal; a folder swapped for a link or a pipe never
walked through; a read failure naming its kind, never the path): P2
slice 5 removed `books.import_local_file` and the import roots, and
those tests with them. `resolved` follows 40 links, as the kernel
does, and no more
(`resolved_follows_forty_links_and_no_more`). A file is saved only to
those tests with them. `resolved` followed 40 links, as the kernel
does, and no more (`resolved_follows_forty_links_and_no_more`): it went
with ledger-mcp's `paths.rs`, and that test with it, in D-44. A file is saved only to
a new file in a folder that exists, and a missing folder or a taken
file is refused as such, never as a failed write
(a CPA package test, and
@@ -285,11 +285,11 @@ since `asset_long_life` alone takes three minutes a run):
|---|---|
| Replay is pure: after any sequence of settings, convention, decision, open-item, template and approval commands, valid or refused, the live store, the store reopened from its journal and the viewer's snapshot hold the same settings and conventions, and the store verifies (database row for row) | `ledger-store` `tests/settings.rs` `settings_replay_is_pure` (proptest, 512 cases per check; 2,000 by hand, release, passed) |
| Settings need journal format 2: books this version made declared it as they were created, so a change journals only itself; a change to what is set journals nothing; books in format 1 refuse a change (nothing journaled) until their format upgrade, then make it after the one declaration; refused changes journal nothing | `a_change_needs_journal_format_2`, `refused_changes_journal_nothing` |
| Approval rules block nothing until enforced; then an entry a rule covers posts only once its approver approved it as it stands | `approval_rules_off_by_default`; `ledger-mcp` `spec_kept.rs` `approval_rules_through_the_server` |
| Approval rules block nothing until enforced; then an entry a rule covers posts only once its approver approved it as it stands | `approval_rules_off_by_default`; `ledger-cli` `spec_kept.rs` `approval_rules_through_the_server` |
| A template drafts exactly, odd cents by largest remainder, its slots named by each draft, all or nothing | `templates_draft_exactly_or_not_at_all`, `template_slots_take_the_account_each_draft_names`, `templates_draft_through_the_server`, core `splits_are_exact` |
| Books written under P1 open to the same record forever | `books_written_under_p1_open_to_their_record` (fixture `old-books/p1_record`, also in every old-books test of the store, the viewer and the server) |
| A settings file imports exactly, reads as it did, and an export round-trips; a masked EIN never imports | `ledger-config` codec unit tests, `tests/codec_props.rs` `exports_round_trip` (512 cases), `ledger-cli` `the_toml_and_its_examples_import_and_round_trip` |
| Moving settings in: set refused before, the preview records nothing, the file never read after (not even to start), an import that reads differently or loses the vault key refused, the EIN only ever masked | `spec_kept.rs` `a_settings_file_moves_into_the_books`, `an_import_that_reads_differently_is_refused`, `an_import_needs_the_vault_key_on_the_command_line` |
| The books keep their settings: changed only with books.set_setting, the EIN only ever masked, read from the books after a restart; no tool imports a settings file (D-44: no settings file is read, books.import_settings is gone, and the file-moving tests went with it) | `spec_kept.rs` `settings_live_in_the_books_and_the_ein_leaves_masked`, `books_keeping_their_settings_take_no_settings_file` |
| books.start fits in 8 KB and says what it cut | `start_fits_in_8_kb` |
| Values are canonical; no full TIN but the company's EIN; settings hold together; replay takes shapes | core `settings::tests` |
| Every new tool on the happy path, schema-checked; read-only tools change nothing; idempotent tools repeat as no-ops | `spec_mcp.rs` `inv1_*`, `read_only_tools_change_nothing`; `spec_vault.rs` `idempotent_tools_change_nothing_when_repeated` |
@@ -333,18 +333,19 @@ the law it broke:
strings, an escaped quote), comments carried by an import, a masked
EIN matching, not matching and into empty books, an empty list
changing nothing, `differences` naming each field, the export's
provenance lines, `effective_receipt_policy` with and without
`config.json` (`tests/effective_policy.rs`).
- **MCP.** `Live::journaled` reads no file, `sync` reloads once per
change; the books://start, settings and conventions resources; a
provenance lines, the receipt policy with and without `config.json`
(`tests/effective_policy.rs`, on its text since D-44).
- **MCP.** `sync` takes the books' settings once per change
(`books_take_their_own_settings_once_per_change`); the books://start, settings and conventions resources; a
renumber names exactly the conventions that name the old code.
- **Viewer.** The Company page's cards (`tests/kept.rs`): live
conventions and not retired ones, standing decisions and not
superseded ones, open items and not resolved ones, who decided each
and when, a late item saying so in words ("overdue since", added for
this: colour alone told only the sighted), and the empty-profile copy
of books that keep their settings. `--vault-key-file` wins over the
settings and an unknown argument exits 2 (`tests/vault_key_flag.rs`).
of books that keep their settings. An unknown argument exits 2
(`tests/command_line.rs`; until D-44 it also checked the vault key
flag, gone with the viewer's loose-books-folder mode).
The first run's 12 timeouts were builds slowed by parallel work: the
rerun caught them all.
@@ -367,9 +368,10 @@ two-place share to two places changes nothing).
## P2 slice 1: one executable (2026-10-18)
`wanderbooks dev-stdio` and `wanderbooks gui` run the server and the
The development stdio mode (gone since D-44: books are served by
`wanderbooks server` alone) and `wanderbooks gui` ran the server and the
viewer from the one executable (plan D-22); `tests/one_executable.rs`
drives both, and `just server-only` builds, clippies and tests the build
drove both, and `just server-only` builds, clippies and tests the build
without the viewer and fails if it links a window library. Mutation on
the diff, two jobs, the viewer left out (owner, 2026-10-18): the server's
`run` against the server's tests, 14 mutants, 12 caught or unviable, the
@@ -692,8 +694,8 @@ a close to retained earnings; it opens to main's figures in
`books_an_earlier_version_wrote_open_to_its_figures`, R-2); `format.rs`
`an_older_reader_stops_at_the_members_cumulative_feature`.
**MCP and viewer**: `ledger-mcp` `close_period_shows_each_members_running_split`
(`tests/mcp_stdio.rs`: the preview's `members` is the confirmed one; after
**MCP and viewer**: `ledger-cli` `close_period_shows_each_members_running_split`
(`tests/mcp_journeys.rs`: the preview's `members` is the confirmed one; after
a restart, `list_periods`' `close_rule` and the statement of members'
equity); `old_books_name_each_closes_rule_and_true_up_at_the_next_close`
(the old-books fixture `x_member_closes` through the server: every
@@ -824,7 +826,7 @@ cases: random books with reversals and closed months, random as_of; the
file's entries and balances against the trial balance without the
closes).
**End to end.** `ledger-mcp` `oracle_mcp.rs`:
**End to end.** `ledger-cli` `oracle_mcp.rs`:
`iif_declares_accounts_by_role_and_balances_every_transaction` (as_of,
DOCNUM the ids, the warnings exact: the fixture's line-broken memo and
the engine's own "Depreciation — Laptop — 2025-03" memos, the alias, a
@@ -832,7 +834,7 @@ range refused), `iif_without_as_of_is_as_of_today_and_ties_to_the_trial_balance`
(review R5), `cpa_package_holds_the_year_and_its_evidence` (the IIF from
the prior year's sale through as_of; a payment after as_of to the
contractor left out of the 1099 worksheet; the note's count);
`mcp_stdio.rs` (a Chinese account name over stdio: ASCII text, CR LF,
`mcp_journeys.rs` (a Chinese account name through the server: ASCII text, CR LF,
`warnings`); `spec_mcp.rs` (iif with as_of); `ledger-gui`
`tests/exports.rs` (the README's IIF line names the as_of).
@@ -1052,7 +1054,7 @@ the end.
the CPA package's statement with both schedules
(`the_cash_flow_carries_both_schedules`); the MCP tool and its exports
field for field against the core (`cash_flow_exports_equal_the_core_statement`),
the CSV's warnings, the settings' roles and a member-paid expense over stdio
the CSV's warnings, the settings' roles and a member-paid expense through the server
(`cash_flow_reads_its_account_roles_from_the_books_settings`), refusals
naming the setting (`cash_flow_refuses_contradictory_roles`), the
settings' problems from the same check; the viewer's statement as the
@@ -1083,7 +1085,7 @@ Every fixture is fictional (Fernhill Labs LLC, its members and vendors);
no mutation run (D-33): the targets are listed for P12 below.
**Named regressions**, from the brief's reproductions:
- `ledger-mcp` `tests/oracle_mcp.rs`: `show_refuses_start_after_end_defect_15`
- `ledger-cli` `tests/oracle_mcp.rs`: `show_refuses_start_after_end_defect_15`
(1.1.1-1.1.2: refused, nothing reaches the viewer),
`show_refuses_a_derived_inverted_range_defect_15` (1.1.3: a start after
today with no end, the message saying the end was defaulted),
@@ -1092,7 +1094,7 @@ no mutation run (D-33): the targets are listed for P12 below.
`shown` names), `show_refuses_options_its_target_ignores_defect_15`, and
`show_writes_the_viewer_command_for_each_target` (resolved dates; a
split never compared).
- `ledger-mcp` `tests/mcp_stdio.rs`: `one_default_period_everywhere_defect_16`
- `ledger-cli` `tests/mcp_journeys.rs`: `one_default_period_everywhere_defect_16`
(a July fiscal year, a first entry the year before: P&L, by class, cash
flow, the exported tax summary, members' equity, general ledger and
journal (csv, json: the period's entries only) and the CPA package all
@@ -1100,10 +1102,10 @@ no mutation run (D-33): the targets are listed for P12 below.
gives today's fiscal year to date, the clock read before and after, R14),
`rename_account_end_to_end_defect_15` (the chart, a past period's P&L,
the IIF's full names, verify, two journal lines, refusals write nothing,
a restart keeps the name); `mcp_period_statements_over_stdio`'s old pin
a restart keeps the name); `mcp_period_statements`'s old pin
(no start = the first entry) now pins the fiscal year (02-01) that holds
the end.
- `ledger-mcp` `tests/spec_tax_policy.rs`: `add_account_warns_without_tax_category_defect_8`
- `ledger-cli` `tests/spec_tax_policy.rs`: `add_account_warns_without_tax_category_defect_8`
(1.4.1), `require_tax_category_refuses_defect_8` (refused without,
created with, the import refused whole, clearing refused, a bill's 6999
created and named in its warnings and the digest),
@@ -1159,7 +1161,7 @@ parameter is the shared constant; "all time", "beginning of time" and
"first entry" are gone from every tool and schema; the resources say
their period) and `show_reads_each_reports_dates_as_the_viewer_does`;
`handbook.rs` (the period rule rendered from the same constant);
`hostile_args.rs` `hostile_renames_ranges_and_categories_are_answered`.
`ledger-cli` `hostile_args.rs` `hostile_renames_ranges_and_categories_are_answered`.
Test fixtures that gave two accounts one name under one parent
(`ledger-core` `invariants.rs` and `spec_roles.rs`, the store's refusal
@@ -1173,7 +1175,7 @@ uncategorized expense account is listed.
before its fix (checked by breaking the code and watching it fail):
- `ledger-core` `tests/tax.rs` `a_gain_on_business_property_is_figured_on_form_4797_on_every_return`,
`ledger-import` `tests/spec_import.rs` `tax_summary_lists_form_4797_apart`,
`ledger-mcp` `tests/spec_tax_policy.rs` `a_gain_on_business_property_is_exported_apart`:
`ledger-cli` `tests/spec_tax_policy.rs` `a_gain_on_business_property_is_exported_apart`:
a `form_4797_gain_loss` amount was ordinary income on every return
(Schedule C included, whose instructions send it to Form 4797; on a
partnership net section 1231 gain is Schedule K line 10), its Tax column
@@ -1187,10 +1189,10 @@ before its fix (checked by breaking the code and watching it fail):
- `ledger-gui` `tests/view_model.rs` `all_dates_holds_an_entry_dated_after_today`:
"Show all dates" and "All time" ended today, leaving out entries dated
after it.
- `ledger-mcp` `tests/oracle_mcp.rs` `show_refuses_options_its_target_ignores_defect_15`
- `ledger-cli` `tests/oracle_mcp.rs` `show_refuses_options_its_target_ignores_defect_15`
(a split past 240 columns): `books.show` sent a split the viewer shows
as one total.
- `ledger-mcp` `tests/mcp_stdio.rs` `rename_says_what_the_new_name_moves_defect_15`:
- `ledger-cli` `tests/mcp_journeys.rs` `rename_says_what_the_new_name_moves_defect_15`:
the notes for the name-picked cash and card sets and conventions had no
test of their own.
- `ledger-store` `tests/store.rs` `rename_account_changes_only_the_name_defect_15`
@@ -1200,7 +1202,7 @@ before its fix (checked by breaking the code and watching it fail):
**Landing on main (2026-10-21),** beside odd cents, the IIF and the
cash flow statement:
- `ledger-mcp` `tests/mcp_stdio.rs` `the_default_period_never_shortens_the_iif_or_the_package`:
- `ledger-cli` `tests/mcp_journeys.rs` `the_default_period_never_shortens_the_iif_or_the_package`:
on a July fiscal year with entries before it, the general journal asked
without dates is the fiscal year to date, while the IIF without dates
holds every posted entry from the first through today under
@@ -1287,8 +1289,8 @@ no mutation run here; what P12 must cover is at the end.
| Decimal and i64 agree on party_target and reverse_target | `kernel_bridge.rs` `party_target_agrees` | the bridge |
| Kani: the direction is the sign, the target is odd, reverse_target an involution | `proofs.rs` `party_target_direction_is_its_sign`, `party_target_is_odd`, `reverse_target_is_an_involution` | the kernel spec |
| The journal: party.creditor_set one line each, under the feature the books declared when created; in books an older version wrote, refused (FormatUpgradePending, nothing journaled) until the format upgrade; refusals journal nothing; books without creditors never change, in either format; corrupt lines refused | `ledger-store/tests/party_creditors.rs` | the format gate |
| Over the MCP: get_entry's direction, cash, in_kind; party totals by basis, echoed, others refused; basis all equals members' equity; creditor_of round trips and a card fee isn't a payment; the post_entry preview's parts are what it posts | `ledger-mcp/tests/spec_party_totals.rs` | the tool docs |
| Defect 11 over the MCP: a member's TIN last four in the directory, a W-9's, none, after a purge | `ledger-mcp/tests/spec_vault.rs` `defect11_the_directory_shows_a_members_tin_last4` | B-26 |
| Over the MCP: get_entry's direction, cash, in_kind; party totals by basis, echoed, others refused; basis all equals members' equity; creditor_of round trips and a card fee isn't a payment; the post_entry preview's parts are what it posts | `ledger-cli/tests/spec_party_totals.rs` | the tool docs |
| Defect 11 over the MCP: a member's TIN last four in the directory, a W-9's, none, after a purge | `ledger-cli/tests/spec_vault.rs` `defect11_the_directory_shows_a_members_tin_last4` | B-26 |
| The viewer shows the tools' numbers, labelled cash basis, with the in-kind line; its what-for reads a reversal as `parties::flow` does | `ledger-gui/tests/parties.rs` `the_party_page_shows_the_basis_and_in_kind`, `a_reversed_member_card_payment_is_nothing_on_the_issuers_page` | `activity_with`, `flow` |
| bookspec | `bookspec/examples/p3-party-*.toml` | the same cases, by hand |
@@ -1323,7 +1325,7 @@ mutation run (D-33); the list P12 must cover is at the end.
| Law (rules.md B-19) | Tests |
|---|---|
| Undecided is not "not reportable" (defect 18) | `undecided_is_not_not_reportable` |
| "No 1099" is a decision with who and when; "none" refused (defect 23) | `a_vendor_marked_no_1099_is_not_undecided`, `the_cutovers_reasons_are_recorded_as_given`, `set_vendor_1099_says_what_a_decision_needs` (`ledger-mcp` `spec_1099.rs`) |
| "No 1099" is a decision with who and when; "none" refused (defect 23) | `a_vendor_marked_no_1099_is_not_undecided`, `the_cutovers_reasons_are_recorded_as_given`, `set_vendor_1099_says_what_a_decision_needs` (`ledger-cli` `spec_1099.rs`) |
| A member's card is excluded, their check counts, unknown is undetermined (defect 12) | `a_members_personal_card_payment_is_not_a_1099_payment`, `payments_come_from_cash_and_financing_only` |
| Card-account payments are listed, not dropped | `card_account_payments_are_listed_not_dropped` |
| A bank payment counts unless tagged with a card (D-40) | `a_debit_card_payment_from_the_bank_is_excluded_when_tagged` |
@@ -1342,9 +1344,9 @@ mutation run (D-33); the list P12 must cover is at the end.
| Journal: lines written only under their features (refused, nothing journaled, in books an older version wrote until the format upgrade; never declared on the way); replay and projection exact; refusals and no-ops journal nothing; newer values newer; old books as they always meant | `ledger-store` `tests/form1099.rs` (`each_feature_waits_for_the_upgrade_and_is_never_declared_on_the_way`, `decisions_and_methods_replay_and_project`, `refusals_and_no_ops_journal_nothing`, `an_unknown_1099_value_is_newer_not_corrupt`, `books_written_before_decisions_read_as_they_always_meant`), unit tests in `ledger-store` `form1099.rs`, the op model (`refused_commands_never_reach_the_journal` with `SetVendor1099`, `SetW9Flag`, `AddInstrument`, `SetEntryInstrument`; an accepted command journals or changes nothing), `books_an_earlier_version_wrote_open_to_its_figures` (fixture `p2_vendor_1099`) |
| Records change no entry (verification 13a) | `decision_and_method_records_change_no_entry` |
| The quieter untagged note (R7) | `the_untagged_note_counts_only_money_out` (`ledger-package`) |
| One builder: the package CSV is the worksheet CSV for the package's accounts; README wording | `ledger-package` `tests/package.rs` (the zip's CSV equals `vendor_1099_csv` for the request's accounts, else the settings', through as_of: `payments_to_a_vendor_come_from_the_packages_accounts`, `one_list_of_cash_accounts_per_package`, `the_1099_worksheet_stops_at_as_of`; the README line pinned); over the MCP, the package's CSV is `books.vendor_1099`'s for the same `cash_accounts`, `financing_accounts` and `through` (`ledger-mcp` `oracle_mcp.rs` `cpa_package_holds_the_year_and_its_evidence`) |
| One builder: the package CSV is the worksheet CSV for the package's accounts; README wording | `ledger-package` `tests/package.rs` (the zip's CSV equals `vendor_1099_csv` for the request's accounts, else the settings', through as_of: `payments_to_a_vendor_come_from_the_packages_accounts`, `one_list_of_cash_accounts_per_package`, `the_1099_worksheet_stops_at_as_of`; the README line pinned); over the MCP, the package's CSV is `books.vendor_1099`'s for the same `cash_accounts`, `financing_accounts` and `through` (`ledger-cli` `oracle_mcp.rs` `cpa_package_holds_the_year_and_its_evidence`) |
| The viewer counts as the server does (brief 1.4) | `the_viewer_counts_the_1099_as_the_server_does` (`ledger-gui` `tests/parties.rs`) |
| End to end, restarting between steps | `payment_methods_round_trip_and_classify_payments` (`spec_1099.rs`), `mcp_vendor_1099_over_stdio`, `spec_mcp.rs` (every tool exercised and schema-checked, `books.instruments` read-only) |
| End to end, restarting between steps | `payment_methods_round_trip_and_classify_payments` (`spec_1099.rs`), `mcp_vendor_1099`, `spec_mcp.rs` (every tool exercised and schema-checked, `books.instruments` read-only) |
| bookspec | `bookspec/examples/1099-*.toml` |
**For P12 (mutation targets, none run):** `ledger-core` `form1099.rs`
@@ -1382,8 +1384,8 @@ must cover is at the end.
| One rule for what a card is: the recorded business cards' liabilities, else (none recorded) liabilities named "card", not "gift"; never a member's card, a member's check or a platform; a debit card on the bank is none; money follows it | `ledger-core` `tests/spec_party_totals.rs` `a_card_is_a_recorded_business_card_else_a_liability_named_card` | B-35 item 5, `parties::card_accounts` INVARIANTS 1-4 |
| Party totals and the 1099 worksheet never contradict: paid less received is the worksheet's paid + excluded_card + undetermined + not_reportable plus its other receipts, for a vendor named alone, whatever the methods, refunds and reversals | property `party_totals_are_the_1099s_buckets` (`ledger-core` `tests/form1099.rs`; `PROPTEST_CASES`, default 256; 2,000 run clean; dropping a bucket from the sum fails at once) | B-35 item 9, by hand |
| The same, one year by hand: a member's check, a member's card, a member's payment of unknown method, a company card charge and a company check to one contractor, then the member reimbursed | `bookspec/examples/1099-and-party-totals-for-a-members-payment.toml` | B-35 item 9; Treas. Reg. §1.6041-1(e)(1), (a)(1)(iv) |
| The CPA package's 1099 worksheet is `books.vendor_1099`'s CSV for the package's accounts (the request's, else the settings', R18) through its as_of, byte for byte; without `through` the files differ; a `through` outside the year and an unknown account are refused | `ledger-mcp` `oracle_mcp.rs` `cpa_package_holds_the_year_and_its_evidence`; `ledger-package` `tests/package.rs` `payments_to_a_vendor_come_from_the_packages_accounts`, `one_list_of_cash_accounts_per_package`, `the_1099_worksheet_stops_at_as_of` | B-33; mcp-interface.md |
| A feature is asked for before a command's first line: `books.update_party` with a field and the first creditor, in P2's books waiting for their format upgrade, is refused whole (nothing journaled, never the field without the creditor); once upgraded, as in books this version made, it journals the field, then the creditor, declaring nothing | `ledger-mcp` `tests/spec_party_totals.rs` `update_party_asks_for_the_creditor_feature_before_its_first_line` | B-27 |
| The CPA package's 1099 worksheet is `books.vendor_1099`'s CSV for the package's accounts (the request's, else the settings', R18) through its as_of, byte for byte; without `through` the files differ; a `through` outside the year and an unknown account are refused | `ledger-cli` `oracle_mcp.rs` `cpa_package_holds_the_year_and_its_evidence`; `ledger-package` `tests/package.rs` `payments_to_a_vendor_come_from_the_packages_accounts`, `one_list_of_cash_accounts_per_package`, `the_1099_worksheet_stops_at_as_of` | B-33; mcp-interface.md |
| A feature is asked for before a command's first line: `books.update_party` with a field and the first creditor, in P2's books waiting for their format upgrade, is refused whole (nothing journaled, never the field without the creditor); once upgraded, as in books this version made, it journals the field, then the creditor, declaring nothing | `ledger-cli` `tests/spec_party_totals.rs` `update_party_asks_for_the_creditor_feature_before_its_first_line` | B-27 |
| One bookspec loader for every example, the parties' and the 1099s' with them: `setup.parties`, `setup.members` (`distributions_account`), `setup.payees` (a party's vendor record), `setup.status_changes`, `setup.instruments`, events' `parties` and `instrument`, and the queries `vendor_1099`, `k1`, `party_part`, `party_totals`, `party_tin_last4`; refused: a party or payment method not in the setup, a reversal naming parties, party totals without a basis, a 1099 line leaving a figure out, a wrong direction or status | `ledger-core` `tests/bookspec.rs` (`every_bookspec_example_holds`, `a_party_not_in_the_setup_is_refused`, `a_party_parts_direction_is_checked`, `a_reversal_naming_parties_is_refused`, `party_totals_without_a_basis_are_refused`, `a_vendor_1099_line_leaving_a_figure_out_is_refused`, `a_vendor_1099_status_is_checked`, `a_payment_method_not_in_the_setup_is_refused`) | bookspec/README.md |
**For P12 (mutation targets, none run, D-33):** `parties::card_accounts`
@@ -1406,9 +1408,9 @@ a bookspec example (`bookspec/examples/`, P13, D-34).
|---|---|
| Through a day of the life's final month, depreciation is prorated by days, never plugged (repro A: December 483.87, accumulated 11,483.87) | `ledger-core` `spec_assets_payroll.rs` `defect22_depreciation_through_mid_final_month_is_prorated_not_plugged`; `spec_asset_tags.rs` `register_as_of_a_day_before_a_same_month_disposal_is_prorated` |
| A mid-month life's drift is spread over the final month's days (repro D: through Apr 13, 920.00; Apr 14, 930.00) | `defect22_final_month_drift_is_spread_by_days`; `spec_replayed_extremes.rs` `depreciation_reaches_chronos_last_month` (1,185.92 through Dec 10 of year 262142) |
| A disposed asset's schedule stops at its disposal day, held, and equals what the disposal took (repro B) | `defect22_a_disposed_assets_schedule_stops_at_its_disposal_day`, property `prop_disposed_schedule_equals_what_disposal_computed`; `ledger-mcp` `spec_assets.rs` `a_disposed_asset_lists_what_it_depreciated_through_its_disposal` |
| A disposed asset's schedule stops at its disposal day, held, and equals what the disposal took (repro B) | `defect22_a_disposed_assets_schedule_stops_at_its_disposal_day`, property `prop_disposed_schedule_equals_what_disposal_computed`; `ledger-cli` `spec_assets.rs` `a_disposed_asset_lists_what_it_depreciated_through_its_disposal` |
| A disposal in the final month posts depreciation by days (repro C: 322.58, gain 322.58) | `ledger-store` `tests/assets.rs` `defect22_a_disposal_in_the_final_month_posts_depreciation_by_days` |
| A disposal inside a month already run takes back the days after it (R2: excess 500.00, book value 6,500.00) | `defect22_a_disposal_inside_a_posted_month_takes_back_the_extra_days` (store and `ledger-mcp` `spec_assets.rs`) |
| A disposal inside a month already run takes back the days after it (R2: excess 500.00, book value 6,500.00) | `defect22_a_disposal_inside_a_posted_month_takes_back_the_extra_days` (store and `ledger-cli` `spec_assets.rs`) |
| A month skipped is caught up in the month the run is made; a later run for it posts nothing (settled in design, P3) | core `a_skipped_month_is_caught_up_in_the_month_run`, store `a_skipped_month_is_caught_up_in_the_month_run`, MCP `a_skipped_month_is_caught_up_and_said` |
| A reversed run is un-posted and runs again (feature month_reposting); re-instating a run whose month ran again is refused; a disposal, and a run its disposal took as final, can't be reversed; a reversed run before a disposal leaves nothing behind (R3) | store `a_reversed_depreciation_month_can_be_run_again`, `a_disposal_entry_cannot_be_reversed`, `a_reversed_run_before_a_disposal_leaves_nothing_behind`; core `a_reversed_schedule_month_is_due_again` |
| What the books carry is read entry by entry, each in its own month: a run reversed after its month ended isn't run twice (its month posts nothing; the reversal's month catches it up), for depreciation; a schedule month reversed after it ended is left for a human, listed with why (P3 review) | store `a_run_reversed_after_its_month_is_caught_up_in_the_reversals_month`; GUI `a_run_reversed_after_its_month_leaves_the_reversals_month_due`; core `schedules.rs` `a_schedule_month_reversed_after_it_ended_is_left_for_a_human`; store `format.rs` `a_schedule_month_posted_again_waits_for_month_reposting_and_replays` |
@@ -1420,7 +1422,7 @@ a bookspec example (`bookspec/examples/`, P13, D-34).
| The final month is C + (D − C) × k / d, rounded half up, against arithmetic written in the test (P3 review) | property `prop_the_final_month_takes_the_remainder_by_its_days` (500 cases) |
| A month that rounds to nothing posts and journals nothing (R10) | store `run_depreciation_posts_nothing_for_a_month_that_rounds_to_nothing`, MCP `run_depreciation_skips_a_month_that_rounds_to_nothing` |
| Depreciation and daily schedules round to the currency's minor unit (R1: yen, dinars) | `jpy_depreciation_posts_whole_yen`, store `jpy_depreciation_runs_post_whole_yen`, `schedules.rs` `a_jpy_daily_schedule_posts_whole_yen`, `a_kwd_daily_schedule_rounds_to_the_fils`; `money.rs` `steps_are_the_currencys_minor_unit` |
| A new schedule spreads by the day (1,200.00 from March 15: 54.84, eleven 100.00, 45.16); deferred revenue is earned by the day (290.32, 333.34, 333.33, 43.01) | `schedules.rs` `defect9_a_new_schedule_spreads_by_the_day`, `defect9_deferred_revenue_daily_is_earned_to_the_cent`; `schedules.rs` unit `a_daily_plan_spreads_by_the_day`; `ledger-mcp` `mcp_stdio.rs` `mcp_daily_schedule_over_stdio` |
| A new schedule spreads by the day (1,200.00 from March 15: 54.84, eleven 100.00, 45.16); deferred revenue is earned by the day (290.32, 333.34, 333.33, 43.01) | `schedules.rs` `defect9_a_new_schedule_spreads_by_the_day`, `defect9_deferred_revenue_daily_is_earned_to_the_cent`; `schedules.rs` unit `a_daily_plan_spreads_by_the_day`; `ledger-cli` `mcp_journeys.rs` `mcp_daily_schedule` |
| Schedules from before P3 stay equal months, forever | `defect9_legacy_schedules_stay_monthly_even`, property `prop_monthly_even_plan_is_unchanged`; every old-books fixture (`books_an_earlier_version_wrote_open_to_its_figures`) |
| An equal-months schedule starts on the 1st (D-37); a daily one's last day is in the calendar | `an_equal_months_schedule_starts_on_the_first`, `spec_bounds.rs` `a_daily_schedule_whose_last_day_passes_9999_is_refused` |
| chrono's month-end clamping is pinned for daily plans (Jan 29, 30, 31; Feb 29) | `schedules.rs` unit `daily_plans_pin_month_end_clamping` |
@@ -1588,7 +1590,7 @@ basis. Mutation runs wait for P12 (D-33); none was run.
- **Named regressions** (the brief's reproductions, anonymized):
`list_assets_book_value_is_what_the_books_carry` and
`a_reversed_run_is_not_posted` (`ledger-mcp/tests/spec_assets.rs`),
`a_reversed_run_is_not_posted` (`ledger-cli/tests/spec_assets.rs`),
`an_asset_bought_before_it_is_in_service_is_on_the_register_from_acquisition`,
`personal_use_contribution_keeps_its_723_basis_apart_from_depreciable_basis`,
`built_in_gain_is_fixed_at_contribution_not_book_cost`
@@ -1664,7 +1666,7 @@ basis. Mutation runs wait for P12 (D-33); none was run.
v9 and v10 databases under this version's name rebuilt). The old books
(`books_an_earlier_version_wrote_open_to_its_figures`) open to exactly
their figures.
- **ledger-mcp:** `spec_assets.rs` (the aliases `purchase_date`,
- **ledger-cli:** `spec_assets.rs` (the aliases `purchase_date`,
`in_service`, `tax_basis_rule`, `contributor_basis` and the old rule
names, noted, and refused when they disagree; the acquired date from the
acquisition entry; a value at contribution required; every refusal
@@ -1737,8 +1739,8 @@ rules.md says why not. Mutation runs wait for P12 (D-33).
- **Viewer** (`ledger-gui/tests/p3_seams.rs`): its capital statements,
party money, cash flow, register and 1099 worksheet, read from the
books' folder and settings file as it opens them, are the engine's.
- **Tools** (`ledger-mcp/tests/p3_seams.rs`, the real binary over
stdio): `books.cash_flow` line by line, `books.profit_and_loss`,
- **Tools** (`ledger-cli/tests/p3_seams.rs`, the books adopted into a
data root and served by a real books server): `books.cash_flow` line by line, `books.profit_and_loss`,
`books.export` members_equity and iif, `books.list_assets`,
`books.vendor_1099` through each date, `books.party` on both bases.
- **Found and fixed** (each test fails before its fix): a distribution
@@ -1870,7 +1872,7 @@ P12 (D-33); none was run.
`prop_project_set_round_trips`); `tests/format.rs`
`p3_features_wait_for_the_upgrade_and_are_declared_once` gains
`projects` and a `project.set`.
- **ledger-mcp:** `tests/spec_projects.rs`
- **ledger-cli:** `tests/spec_projects.rs`
(`add_project_then_project_returns_its_facts_and_figures`,
`a_name_only_project_journals_only_class_added`,
`update_project_checks_every_field_before_writing`,
@@ -1894,7 +1896,7 @@ P12 (D-33); none was run.
`books.projects` among the read-only calls; `spec_vault.rs`'s idempotent
list gains `books.update_project`; the unit tables (`annotations_complete`,
scopes' table) list 119 tools, `period_tools_describe_the_one_default`
checks the period tools' wording, and `mcp_stdio.rs`'s `tools/list`
checks the period tools' wording, and `mcp_journeys.rs`'s `tools/list`
shows 116; `every_tool_is_documented_and_every_documented_tool_exists`
holds with the four new tools (on the rebase onto the surface review it
caught `books.list_classes` written mid-line and a `books.set_line_class`
@@ -1990,7 +1992,7 @@ fail.
sign, and `project_figures`' to-date is `project_to_date`'s).
`project_figures` takes the net before its first month from to date
less the months (one scan fewer); the proptest pins it.
- **ledger-mcp:** `tests/spec_projects.rs`
- **ledger-cli:** `tests/spec_projects.rs`
`project_snapshot_margin_by_account_and_running_net` (the period's and
to date's margin_pct, to_date.by_account, the 12 months' net_to_date
opening on the December deposit and ending on to_date.net, a loss's
@@ -2001,7 +2003,7 @@ fail.
- **viewer and server on one book:** ledger-cli
`tests/projects_parity.rs`
`a_projects_snapshot_is_the_same_in_the_viewer_and_from_the_server`
(`wanderbooks dev-stdio`'s `books.project` against the viewer's
(the books server's `books.project` against the viewer's
`page_figures` and `Snap` on the same books: to date's revenue,
expenses, net, margin_pct, unrecovered cost and by_account, every
month's figures and net_to_date over the chart's span, and the fiscal
@@ -2104,7 +2106,7 @@ receivable or payable and the payments untagged; journaled as the line's
`a_reversed_tagged_invoice_replays_with_its_projects_at_zero`: one
`entry.reversed`, its replay tagged as live); `format.rs`
`document_line_projects_is_its_own_feature`.
- **ledger-mcp:** `tests/spec_projects.rs`
- **ledger-cli:** `tests/spec_projects.rs`
(`invoice_and_bill_lines_count_toward_their_projects`: by name, id and
any capitals; `books.get_entry`'s line `project` and `class_id`;
payments untagged;
@@ -2256,6 +2258,130 @@ fails; rounding toward zero fails both `cashflow::tests`; without the
tax-category route `operating_classes_follow_asc_230_10_45_25`, the
second test and the example fail.
## One way to serve books (D-44, 2026-10-23)
The stdio server and its binary are gone, and with them the second
harness: every suite that drove it moved to
`crates/ledger-cli/tests/` and drives a real `wanderbooks server` on a
data root of its own, reached as an agent reaches it.
- **The harness** (`tests/common/mod.rs`, `mod common;` in each file):
`Home` (a `wb-` temp home made by `wanderbooks setup --home`, its admin
socket, tokens with every scope, `adopt`), `Server` (the server on that
root with an empty environment but PATH, and HOME and XDG_CONFIG_HOME in
the home; ended by its own PID), `Wire` (one MCP session, synchronous:
a message POSTed to `/b/{book}/mcp` with the bearer token, session and
protocol headers the bridge sends; `fetch` takes a handed file by its
download and checks its size and SHA-256), `Books` and `Place` (a home,
its server and the books `acme`; `Place` starts the server again on
each start, as a new server process read the same books, and lets a
test write books into the root before the first), the async rmcp
`connect` and `call`, and `book_files` (the books' files but what every
call writes by design: the access log, `ops/`, `archive.git/`,
`exports/`, `uploads/`, SQLite's -wal and -shm). Settings a test gives
as a settings file's text are set as an agent sets them: the changes
`ledger_config::codec::import` reads from it, by `books.set_setting`.
- **Moved, every assertion kept:** `mcp_journeys.rs` (was
`mcp_stdio.rs`), `spec_mcp.rs`, `spec_vault.rs`, `spec_kept.rs`,
`spec_attachments.rs`, `spec_parties.rs`, `spec_projects.rs`,
`spec_assets.rs`, `spec_party_totals.rs`, `spec_1099.rs`,
`spec_tax_policy.rs`, `oracle_mcp.rs`, `hostile_args.rs`,
`tin_files.rs`, `http_concurrency.rs`, `p3_seams.rs`, `startup.rs`;
`projects_parity.rs` and `one_executable.rs` drive the server too.
Files the books make are checked by their download; restarts are the
server closed and started again on the same root; old books are put in
the root (or adopted with their settings file, as the owner moves old
books in); the JSON-RPC framing tests check the server's door (a 4xx,
and any error it carries -32700 or -32600; an agent's malformed lines
are the bridge's, `the_agents_input_is_read_message_by_message`).
- **Retargeted:** a held lock and newer books (`startup.rs`) are now a
book the server reports not open, with why, on its route and in its
list, while the others are served; a bad receipt threshold is refused
as it is set (`inv5_bad_threshold_refused_when_set`); a changed and a
lowered setting take effect on the next call
(`a_changed_setting_is_read_on_the_next_call`,
`a_lowered_threshold_is_enforced`); `get_receipt` above 256 KiB gives
its download (`get_receipt_gives_bytes_up_to_256_kib_and_a_download_above`);
an unknown tool is a protocol error (`jsonrpc_unknown_tool_is_a_protocol_error`,
now true of the server itself).
- **Deleted, for what they tested is gone:** startup's
`help_names_the_options`, `the_default_data_dir_is_under_home`,
`the_import_root_flag_is_refused_as_unknown`,
`the_settings_beside_the_books_are_read` and
`the_cash_roles_beside_the_books_are_read` (the stdio binary's flags and
its settings file; the rules stay covered by
`mcp_receipts_always_required_when_threshold_is_zero` and
`cash_flow_reads_its_account_roles_from_the_books_settings`);
`a_restarted_server_waits_for_the_old_one_to_let_go` (its lock wait;
the server's is `local_server.rs` `a_second_server_waits_for_the_first`);
spec_vault's `a_key_file_inside_the_books_is_refused` and
`books_in_git_must_ignore_the_vault` (a books dir's key and git; a book
in the root keeps its own: `a_book_in_the_root_keeps_its_vault_and_key_inside_its_own_folder`);
spec_kept's `an_import_that_reads_differently_is_refused`,
`an_import_needs_the_vault_key_on_the_command_line` and the settings
file's half of the others; spec_mcp's `a_broken_edit_keeps_the_last_good_settings`
(a settings file's reload); `dev_stdio_is_the_books_server` (the
command is gone, no name kept: it is an unknown mode, as any other
word is); the unit tests of `wire.rs`, of
`paths.rs` (the key-inside-the-books check's path resolution) and of
the settings file's watching in `settings.rs`.
- **The owner's three decisions (2026-10-23).** (a) `books.import_settings`
is gone: deleted spec_kept's import half of
`books_keeping_their_settings_take_no_settings_file` (now: the tool is
not listed, and a call to it is a protocol error), the import calls in
spec_mcp's every-tool walk and spec_vault's idempotent list, and the
scope rule's unit test of imports (`scopes.rs`); ledger-config's
`load_missing_path_errors` and `load_matches_parse` (`load` is gone:
adoption reads the file's text), `tests/effective_policy.rs` now on
`config.json`'s text. `books.chart_template` gives its settings as
changes, which `mcp_journeys.rs` sets by `books.set_setting`.
(b) The viewer's loose-books-folder mode is gone: deleted `live.rs`
`a_settings_file_made_later_is_followed` (replaced by
`a_setting_changed_while_it_runs_is_followed`: a journaled change is
followed, a file beside the books never read),
`vault_key_flag.rs` `the_key_named_on_the_command_line_wins` (the file
is `command_line.rs`: an unknown argument, and each gone flag, exits
2), `tin_copy.rs` `a_key_inside_the_books_is_refused` (a book keeps its
key inside its own folder), `old_books.rs`
`books_that_keep_no_settings_read_their_file_only_outside_a_root` and
`an_older_books_dirs_config_json_sets_its_receipt_rule` (now one test,
`books_that_keep_no_settings_read_no_file_beside_them`), and
`exports.rs` `the_menu_reads_an_older_books_dirs_settings_file` (now
`the_menu_reads_the_books_own_settings`). The TIN tests open the
vault where a book of the data root keeps it; `one_executable.rs`'s
windowless viewer opens empty books in a root and home of its own. (c) `wanderbooks dev-stdio` has no
stub: it is an unknown command.
- **Review of the move (2026-10-23).** The windowless viewer opened the
demo, whose build alone took over its minute under load: it opens
empty books in a data root of the test's own, with an empty
environment but PATH and a temp HOME, as `ledger-gui`'s
`command_line.rs` now runs too (neither can reach this account's
books or preferences). `old_books_name_each_closes_rule_and_true_up_at_the_next_close`
had lost the refusal it was written for: its books are now P1's
journal in P2's format (declared offline, as P2's upgrade did), so
the close names `members_cumulative` in its `format_upgrade_pending`
refusal, and closes once the owner upgrades. `hostile_args.rs`'s old
books take the fixture company's settings only where their format
keeps settings: each change must be taken, or refused as
`format_upgrade_pending` (all of them, then, and the books are served
on the defaults), or, for the fiscal year, refused under periods
already closed on the calendar year; any other answer fails when the
books are first served. spec_vault's full-TIN scans
skip only the bytes of the exports the test was handed (their
records are scanned, and nothing else may be in `exports/`). The
harness fails on a stream line or JSON body from the server that
isn't JSON, instead of dropping it. `settings_live_in_the_books_and_the_ein_leaves_masked`
reads the timezone and the new EIN back after its restart, and
`inv5_bad_threshold_refused_when_set` compares the journal itself.
- **Unit tests** of the tools build their server the one way the books
server does (`Served::open` on books in a data root of their own), with
any settings they need journaled first as `books.set_setting` journals
them (`Store::change_settings`), so no test-only serving mode or
settings are left.
- **Time:** `just server-only` runs the one executable's own suites; the
tool suites run once, in `check`. Mutation of `ledger-mcp` runs the
ledger-cli suites too, without the viewer (`just mutants-mcp`).
## Benchmarks (baseline 2026-10-13)
No budget, by decision (the owner, 2026-10-13: the books do little work
@@ -2319,7 +2445,7 @@ B-14) are the first.
|---|---|---|
| Fault injection | SKIP for the core (no I/O, clock, or network); DONE for the store's journal (full disk, cut-off append) | the first I/O or clock in `ledger-core` |
| Integration | SKIP for the core (no collaborators) | the core gains one; covered at the store and MCP layers |
| End-to-end | SKIP for the core (a library) | MCP stdio tests cover the journeys |
| End-to-end | SKIP for the core (a library) | the MCP tests through a real books server (ledger-cli) cover the journeys |
| Security properties | SKIP for the core (no roles, no secrets) | the first permission concept, or a secret-bearing field |
| Concurrency stress | SKIP for the core (no shared state); DONE for the store's writer + viewer (found the false "differs") | the first `Arc`, atomic, or lock in the core; the MCP server's HTTP transport shares one store across sessions (next) |
| Lean: apportion's sum law for any n | DONE (2026-10-14) | verify/lean/Wanderbooks/Apportion.lean: ranks are a permutation of 0..n-1 (strict total order + pigeonhole, core Lean only), so exactly u parts get a unit and one the fraction; `apportion_sum` and `apportion_sum_of_shortfall`, standard axioms only |