P1: the books keep how they are kept
check / check (push) Canceled after 0s

Settings, conventions (posting templates, approval rules, policies,
close steps, calendar, mappings, notes), decisions, open items and
entry approvals are journal events (journal format 2). Books that keep
their settings never read wanderbooks.toml or config.json; the server
and the viewer take the vault key from --vault-key-file instead.
books.import_settings moves a settings file in once, exactly as the
server reads it. Approval rules, when enforced, gate every posting
path. Replay refuses what it can't read as newer, never as corrupt.

Lean: a record changes no figure. Mutation on the whole diff: every
survivor killed by a test but two equivalent (docs/testing.md). The
Company page says a late open item is overdue in words, not colour
alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-17 19:27:48 -06:00
1 parent ee1a372179
commit b292a505b1
71 files changed
+12249 -461

No files matched your search

+154 -48
View File
@@ -23,45 +23,87 @@ layer.
## Company settings
`ledger-mcp` reads the books dir's `wanderbooks.toml` (default: next to
`--data-dir`; override with `--books-toml PATH`): `[company] name` heads
every report, `[company] fiscal_year_start` ("MM-DD", default "01-01";
the year's end is derived, never stored) defines fiscal-year-to-date for the
CPA package and the balance sheet's net income line, `[[company.members]]`
(`name`, `share` as "50%" or "0.5", `capital_account` code; shares must sum
to 100%) drives the statement of members' equity, `[books]
accounting_basis` ("accrual" or "cash", default accrual) labels report
footers, and `[books] receipt_threshold` sets the receipt policy unless
`config.json` sets `receipt_policy`. Members may name a
`distribution_account`, and `files_jointly_with` (spouses filing one joint
return; the CPA package's README notes their two K-1s go on one Form 1040,
and k1-members.csv ends with a members' total, not a K-1, that ties to
Schedule L); single-owner businesses may set `[books] close_into`
(the equity account a close puts net income in; default Retained Earnings)
and `close_sweep` (draw, distribution, or dividend accounts closed into it).
The cash flow statement's account roles are `[books] cash_accounts` (what
it reconciles to), `financing_accounts` (loans, and money the owners
advanced, such as a "Due to owners" liability: ASC 230-10-45-14(b)), and
`investing_accounts` (loans the company makes, investments it buys: ASC
230-10-20); the tools and the viewer both read them, and a call may pass
its own (docs/spec/rules.md B-14).
The books keep their own settings (journal format 2, plan/served.md P1):
each change is a `settings.set` line with who decided it and when, replayed
into `ledger_core::settings::Settings` by the server and the viewer alike
(rules.md B-29). Every key is in `ledger_core::settings::KEYS`, with what it
takes and means: the company's facts (`company.name`, `.entity_type`,
`.tax_classification`, `.tax_form`, `.fiscal_year_start` ("MM-01"; the
year's end is derived, never stored), `.timezone` (what "today" means for
the books), `.formation_state`, `.formed`, `.state_entity_number`, `.ein`
(the one identity number a setting keeps, shown masked: "**-***5893"),
`.irs_name_control`, `.duns`, `.address`, `.registered_agent`,
`.partnership_representative`, `.cpa`, `.members` (name, share as "50%" or
"0.5", capital account, optional distributions account and
`files_jointly_with`; shares sum to exactly 1)); the books'
(`books.accounting_basis`, `.cash_accounts`, `.financing_accounts` (loans,
and money the owners advanced: ASC 230-10-45-14(b)), `.investing_accounts`
(ASC 230-10-20), `.close_into` (single owner only), `.close_sweep`,
`.asset_tag_prefix`); and policy (`policy.evidence.threshold` (receipts
strictly above it; "0.00": every entry), `.required_accounts`,
`.exempt_accounts`, `policy.parties` (off, suggest, require),
`policy.approval_rules` (off, the default, or enforce)). A value is checked
by its key and kept in one spelling (a time zone as the database spells it,
an amount without leading zeros, a return as `schedule_c`, `1065`, `1120s`
or `1120`); a change is all or nothing, and the settings after it must hold
together (members and `close_into` never both, a classification that names
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`.
The server reloads `wanderbooks.toml` (and `config.json`'s receipt policy)
whenever either changes, compared by content at the start of each request,
so an edit takes effect on the next call with no restart. A broken edit
keeps the last good settings in force and is reported until fixed; the
data dir and base currency are fixed at startup (`ledger-mcp/src/settings.rs`).
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`).
- `books.settings {}` → `{ books_toml, books_toml_exists, config_json_present,
reload_error, loads, company, fiscal_year_start, tax_form, basis,
receipt_rule, members, cash_accounts, financing_accounts,
investing_accounts, close_into, close_sweep, problems }`: the settings in
force and where they come from. `problems` lists settings that disagree
with the chart (a code not in it, or of the wrong kind), each saying what
to change; `books.digest` repeats them, with a failed reload, as
`settings_problems`. The agent edits the file when the human gives a
company fact, a refusal names a setting, or a problem is listed, then
checks `books.settings`.
- `books.settings {}` → `{ source ("books" or "file"), 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,
close_sweep, problems }`: the settings in force, every key the books know
with its value (null: unset, its default applies) and who set it, and
`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.
- `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".
- `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`).
The accounts invoices, bills, payroll, and closes post to are not settings:
they are control roles in the journal (rules.md B-23).
@@ -75,6 +117,66 @@ they are control roles in the journal (rules.md B-23).
another role, and moving a balance sheet role while its account holds a
balance. Journaled as `role.assigned`.
## Tools — how the books are kept
Conventions, decisions, open items and approvals live in the books too
(`ledger_core::conventions`, rules.md B-29), each with who decided it and
when; `books.start` lists them, and the handbook's
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: {
evidence, parties, approval_rules }, settings_summary: [{ key, value,
set_by, seq }], conventions: [{ id, kind, title, summary }], open_items:
[{ id, title, due }], truncated, handbook_sections, next_steps: [{ tool,
args?, why }] }`. The connect text names it as the first call. Lists cut
to fit say so in `truncated` ("conventions: 4 more (books.conventions)").
`focus` adds steps: `onboarding` the onboarding step, `close` the
company's close steps, `audit` `books.verify`. Not built yet (P5):
`book.id`, `book.url`, `head.last_handoff`, the `agent` input, and
loading it up front under tool search. Also `books://start`.
- `books.conventions { kind?, include_retired? }` → every convention with
its full text, structure, revision and source (`books://conventions`).
`books.set_convention { id, kind, title, body_md, structured?,
decided_by, ... }` sets or revises one (kinds: `posting_template`,
structured `{ description?, lines: [{ account, side, share }] }`, each
side's shares summing to 1, on accounts in the chart that take postings
or on slots (`"{expense}"`) each draft names;
`approval_rule`, structured `{ approver, accounts?, amount_over? }`; and
`policy`, `close_step`, `calendar`, `mapping`, `note`, in words). An id is
a slug, revised in place (same kind), never reused once retired:
`books.retire_convention { id, reason, decided_by, ... }` (destructive).
- `books.draft_from_template { template_id, date, amount, accounts?,
description?, party?, no_party_reason? }` → `{ entry_id, description,
lines }`: a draft whose lines split the amount (at most a line's) by the
template's shares, each side summing to it exactly, odd cents by largest
remainder; `accounts` names each slot's account (`{"expense": "6300"}`),
every slot and nothing else. A refused draft journals nothing (a disk
failing midway could leave a partial draft, as for any draft built line
by line).
- `books.record_decision { subject, decision_md, decided_by: [..],
decided_on, evidence_attachment?, supersedes?, citation? }` → `{ id }`;
`books.decisions { include_superseded? }`. A decision supersedes at most
one live decision; its evidence is a document still in the books.
- `books.raise_item { title, detail_md?, due?, decided_by, ... }` → `{ id }`,
`books.resolve_item { id, resolution, decided_by, ... }`,
`books.open_items { include_resolved? }`.
- `books.approve_entry { entry_id, approver, note?, approved_on? }`: the
approver approved the draft as it stands (its fingerprint: date,
description, every line and class; a line added or a class changed
afterwards voids it). Under `policy.approval_rules = enforce`,
`books.post_entry`'s preview lists `approvals_missing` and a confirmed
post is refused with `approval_required` until each covering rule's
approver approved (the store refuses it too), and a tool that posts at
once (an invoice, a bill, a payment, payroll, a remittance, a disposal)
is refused when a rule covers the entry it would post: draft it instead.
Reversals, closes, depreciation and amortization are exempt. A rule
names accounts that take postings; an account a rule names can't be
renumbered until the rule is revised, and `books.renumber_account` lists
the conventions that name the old code. Under `off` (the default) rules
block nothing.
## Transports
- **stdio** (default): `ledger-mcp --data-dir PATH`. For Claude Code,
@@ -99,9 +201,10 @@ re-read first, so a client connecting after an edit 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
(`books.handbook`), the rule that these tools are the only way to the books
(never local files), decimal-string amounts, and the
draft → lines → preview → confirmed post flow. The second paragraph adds
(`books.start` first each session, and `books.handbook` for every
procedure), the rule that these tools are the only way to the books (never
local files), decimal-string amounts, and the draft → lines → preview →
confirmed post flow. The second paragraph adds
that every entry balances and uses real accounts from `books.chart`; the
parties rule in force (`[books] parties`: under "require" every entry
the agent posts names its party or a no-party reason, since closes,
@@ -115,9 +218,10 @@ the books); that `books.create_invoice`, `books.create_bill`,
`books.receive_payment`, `books.pay_bill`, `books.run_payroll` and
`books.remit_payroll_taxes` post at once without a receipt check; TINs
only into the vault; show the numbers, and after posting check
`books.digest` and offer it to the human; and the
one local file the agent may edit, the settings file `books.settings`
names (until settings move into the journal, P1). When a long rule (many
`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
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
@@ -164,8 +268,9 @@ 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 25:
`books.post_entry`, `books.reverse_entry`, `books.unlink_receipt`,
that changes anything also has a `destructiveHint`, true for these 28:
`books.post_entry`, `books.reverse_entry`, `books.import_settings`,
`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`,
@@ -174,10 +279,10 @@ that changes anything also has a `destructiveHint`, true for these 25:
`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 five that a repeat with the same arguments leaves
`idempotentHint` on the seven 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` and
`books.set_party_roles` (a setter such as `books.update_party`,
`books.run_depreciation`, `books.run_schedules`, `books.run_recurring`,
`books.set_party_roles`, `books.set_setting` and `books.import_settings` (a setter such as `books.update_party`,
`books.set_fx_rate` or `books.set_tin` journals a line, and `books.set_tin`
seals a new vault record, every time, so none carries it;
`idempotent_tools_change_nothing_when_repeated` calls each candidate twice
@@ -923,6 +1028,7 @@ posting.
Hand-rolled on `ServerHandler` (rmcp 3.x has no resource macros):
`list_resources` advertises, `read_resource` renders.
- `books://start`, `books://settings`, `books://conventions`
- `books://chart`
- `books://reports/trial-balance`, `books://reports/balance-sheet`,
`books://reports/profit-and-loss`