check / check (push) Canceled after 0s
The party page's "TIN on file" and the W-9 card's TIN copy the full, formatted number on click (right-click: digits only). The viewer reads it from the vault only at that moment, with the server's key file in force, never creates a key, refuses a key inside the books dir, and never draws more than the last four. A locked vault copies nothing and says why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
462 lines
24 KiB
Markdown
462 lines
24 KiB
Markdown
# Parties and attachments
|
|
|
|
Design for two features built together (Stephen, 2026-10-15): a directory
|
|
of the parties the company deals with, and attachments that link to any
|
|
subject. Status: design; built after asset tax basis.
|
|
|
|
## Why together
|
|
|
|
Both answer "what is this connected to?" A W-9 note, a contract, a bank
|
|
statement, a photo of a laptop: each is a file about a party, an account, a
|
|
reconciliation, an asset, a period. One subject model serves both.
|
|
|
|
## Parties
|
|
|
|
A party is anyone the company deals with. Today's vendors and customers
|
|
become parties; employees stop being name strings on payroll lines; the
|
|
Apple, Google, Anthropic, IRS, and Utah that live only in entry
|
|
descriptions get records.
|
|
|
|
### Model (ledger-core, `parties.rs`)
|
|
|
|
```text
|
|
Party {
|
|
id: PartyId, // its own id space (vendor and customer records keep theirs)
|
|
name: String, // legal or trade name, as on documents
|
|
display_name: Option<String>,
|
|
roles: BTreeSet<Role>, // set by hand: contractor, employee, member, payout_platform,
|
|
// tax_agency, government_agency, financial_institution,
|
|
// landlord (vendor and
|
|
// customer follow the records; see all_roles)
|
|
website: Option<String>,
|
|
address: Option<String>, // business address; no personal PII beyond it
|
|
notes: Option<String>,
|
|
archived: bool, // no longer dealt with; history stays
|
|
w9: Option<W9Facts>, // contractor/landlord: the TIN itself is in the vault
|
|
w4: Option<W4Facts>, // employee: never the SSN
|
|
vendor: Option<VendorId>, // its vendor record (bills, payments, 1099 form)
|
|
customer: Option<CustomerId>,// its customer record (invoices)
|
|
}
|
|
W9Facts { received: NaiveDate, entity_type: String, exempt_payee_code: Option<String>,
|
|
fatca_code: Option<String>, tin_last4: String, kept_at: String,
|
|
vault_record: Option<String> /* SHA-256 of the vault record */ }
|
|
W4Facts { received: NaiveDate, state: String, kept_at: String }
|
|
```
|
|
|
|
The 1099 form stays on the vendor record (`vendor.1099_set`); a party
|
|
reaches it through `vendor`.
|
|
|
|
Rules (to vendor as B-26 with sources):
|
|
|
|
- A full TIN or SSN is never in the journal: `tin_last4` is exactly four
|
|
digits, and any nine-digit run in any party field is refused (the engine
|
|
checks, not the agent). Full TINs live only in the vault (below).
|
|
Source: IRS truncated-TIN rules (Treas. Reg. §301.6109-4, to vendor) for
|
|
what may appear; DECISION (Stephen, 2026-10-15) for the vault.
|
|
- Roles are a set; each role's facts are only allowed with the role
|
|
(W-9 facts need contractor or landlord; W-4 facts need employee).
|
|
- Names are unique ignoring case and surrounding space among
|
|
non-archived parties.
|
|
- A 1099 needs a contractor or landlord role; 1099 totals come from the
|
|
entries tagged with the party, as today's vendor 1099 report does.
|
|
|
|
### Links
|
|
|
|
- Entries: `entry.party_set {entry_id, party_id|null}` generalizes
|
|
`entry.vendor_set` (one counterparty); `entry.parties_set` names
|
|
several, each with a share (below).
|
|
- Invoices and bills already name a customer or vendor id: those are
|
|
party ids after migration.
|
|
- Payroll lines name an employee party id (old runs keep their name
|
|
strings; new runs must name a party with the employee role).
|
|
|
|
### Journal events
|
|
|
|
`party.added {id, name, roles}`, `party.updated {id, change}` (one
|
|
`PartyField` per event, `{field, value}`: name, display_name, website,
|
|
address, notes, archived), `party.role_set {id, role, on}`,
|
|
`party.w9_set {id, facts|null}`, `party.w4_set {id, facts|null}`,
|
|
`party.tin_set {id, facts|null}` (TIN facts: type, last four, vault
|
|
record, when purged), `entry.party_set {entry_id, party_id|null}`,
|
|
`entry.parties_set {entry_id, parties: [{party_id, share}]}` (a share is
|
|
`{"amount": "30.00"}`, `{"percent": "60"}`, or `"derived"`; `[]`
|
|
clears). Replayed and projected like
|
|
the rest; every event gets viewer journal text (the GUI test enforces it).
|
|
|
|
### Shares: an entry with several parties
|
|
|
|
DECISION (Stephen, 2026-10-15): there is no "joint" party. A joint
|
|
transaction is with both members and shows on both members' pages, each
|
|
with their share; apportioning is explicit and exact when it isn't
|
|
derived. Spec: `ledger_core::shares`.
|
|
|
|
- An entry names a list of `PartyShare {party, share}`; `Share` is
|
|
`Amount(d)`, `Percent(p)`, or `Derived`. Tagging one party
|
|
(`set_entry_party`) is one derived share: all of it.
|
|
`Ledger::entry_party` means "the sole party"; `entry_parties` lists
|
|
them all.
|
|
- **What a share is of:** the entry's `entry_flow` net (what it moved
|
|
with its counterparties), or its total debits when the net is zero (a
|
|
member contributing property moves no money but is still theirs).
|
|
- **Amount:** exactly that, positive, in cents; the amounts must sum to
|
|
what the entry moved.
|
|
- **Percent:** more than 0, at most 100, summing to exactly 100. Each part
|
|
is rounded toward zero to the cent and the leftover cents (fewer than
|
|
one per party) go to the first party in order.
|
|
- **Derived:** a sole party gets all of it. Several parties must all be
|
|
members (`[[company.members]]` matched by name to a party with the
|
|
member role; core is handed them as `PartyMember`s). If the entry has
|
|
lines on a named member's capital or distributions account, each
|
|
member gets what their own lines say (and those must account for all
|
|
it moved); otherwise each gets their income share, rescaled to the
|
|
members named, rounded as percents are.
|
|
- **Posted is forever, the split included:** when an entry naming
|
|
several parties with derived shares posts (or a posted entry is tagged
|
|
so), the server records the exact amounts they resolve to
|
|
(`entry.parties_set` with amounts), so a later change to
|
|
`[[company.members]]` (a new income split, a member archived) never
|
|
re-splits posted history. A draft's derived shares follow the settings
|
|
until it posts. An archived member stays a member for their history
|
|
(`Company::party_members` falls back to the archived party of that
|
|
name). Entries posted with derived shares before this rule keep them
|
|
until re-tagged with `books.set_entry_party`.
|
|
- **Refused** (journaling nothing): a party named twice, an unknown
|
|
party, mixed kinds of share, percents not summing to 100, an amount
|
|
finer than a cent, and two parties with vendor records. A 1099 counts
|
|
a payment toward a single vendor: the vendor tag follows the one party
|
|
with a vendor record, and the 1099 worksheet counts the whole payment
|
|
toward it, so name a vendor among others only when the whole payment
|
|
was its (otherwise record its part as its own entry). Whether shares
|
|
add up depends on the settings (cash accounts, members), so core
|
|
checks only their form; the MCP server checks they resolve once the
|
|
entry has balanced lines, and `post_entry` refuses (`party_shares`)
|
|
until they do. Replay never consults settings.
|
|
- **Money:** `parties::activity_with` counts each party's resolved share;
|
|
an entry moving no money counts nothing; an entry whose shares don't
|
|
resolve counts for no one (listed as unresolved). Reversals carry
|
|
every share, so a reversal subtracts each party's part; re-tagging
|
|
either one (parties, shares, or vendor) re-tags the other, so the pair
|
|
always nets to zero.
|
|
A reversal counts on its root original's side, so reversing the
|
|
reversal reinstates the payment as paid (never a negative received).
|
|
- **One number everywhere:** `books.party`, `books.parties`, and the
|
|
viewer's party page and directory read cash as the server does
|
|
(`[books] cash_accounts`, else every asset account but receivables)
|
|
and count the whole calendar year, so a posted entry dated later this
|
|
year counts in both. The viewer shows the year up to the later of today
|
|
and its last posted entry.
|
|
- **For the agent:** a joint member transaction (a reimbursement on Due
|
|
to Shareholders, a contribution or distribution from joint funds)
|
|
names both members with no shares and lets them derive; an uneven
|
|
split gives amounts.
|
|
|
|
### A member's capital statement
|
|
|
|
A member's party page (role member, named in `[[company.members]]`,
|
|
whose capital account is in the chart) is their capital account
|
|
statement over the page's period (`Books::capital_statement`). Every
|
|
figure is the CPA package's K-1 worksheet row for that member and span
|
|
(`ledger_core::k1::k1_detail`, itself the statement of changes in
|
|
members' equity split into cash and property), so the page and the
|
|
K-1 never disagree:
|
|
|
|
- capital at the start of the period (including their share of earnings
|
|
not yet closed into capital);
|
|
- add contributions of cash (deposits, and what they paid for the
|
|
company) and of property (debits to asset accounts that are neither
|
|
cash nor receivables, at the value booked: fair market value);
|
|
- add their share of the period's net income, closed or not;
|
|
- less distributions;
|
|
- any other change in their capital, when there is one;
|
|
- capital at the end of the period (always opening plus the movements,
|
|
exactly);
|
|
- their share of profit, loss, and capital; and their part of what the
|
|
company owes the members jointly, split by the members' shares: the
|
|
balance of the `[books] financing_accounts` liabilities (the
|
|
liabilities members advance), or, only when that is not set, of the
|
|
liability accounts named "due to" / "owed to" (such as 2100 Due to
|
|
Shareholders).
|
|
|
|
Cash and property are told apart by `[books] cash_accounts`. Without it
|
|
the statement reads money as the engine does (every asset account but
|
|
receivables), so every contribution reads as cash; set `cash_accounts`
|
|
to see property apart. (The Cash page keeps its own name-based guess.)
|
|
|
|
### Migration
|
|
|
|
Old journals replay unchanged. `vendor.added` and `customer.added` keep
|
|
their meaning and, on replay as live, link their record to a party: the
|
|
live party of that name (ignoring case and surrounding space) without
|
|
such a record yet, else a new party. So a vendor and a customer of the
|
|
same name share one party; a second vendor of a taken name gets a new
|
|
party named "Name (2)" (names stay unique). Vendor and customer names
|
|
obey the party text rules (no full TIN). `entry.vendor_set` also sets the
|
|
entry's party, and `entry.party_set` to a party with a vendor record sets
|
|
the vendor tag: one counterparty. On an entry naming several parties the
|
|
vendor tag follows the parties, never the other way: `entry.vendor_set`
|
|
for the vendor of a party already named keeps every share; for any other
|
|
vendor, or clearing the tag while its party is named, it is refused. A
|
|
vendor record can't link to a party named beside another party with a
|
|
vendor record (refused: the entry would name two vendors); linking one
|
|
tags the entries naming the party that have no vendor tag yet.
|
|
|
|
### Tools
|
|
|
|
`books.add_party`, `books.update_party`, `books.party_roles`,
|
|
`books.set_w9`, `books.set_w4`, `books.parties` (list, filter by role),
|
|
`books.party` (one: facts, roles, totals this year, documents, links),
|
|
`books.set_entry_party`. (Merging two parties, `books.merge_parties`, is
|
|
not built yet: records of one name already share a party.) `books.add_vendor` and
|
|
`books.add_customer` stay as shortcuts.
|
|
|
|
### Viewer
|
|
|
|
A Parties page (by role, searchable) and a page per party: facts, roles,
|
|
year-to-date paid / owed / invoiced / 1099 amount, every entry, invoice,
|
|
bill, payroll run, documents, and history. Members link to their capital
|
|
accounts. Global search finds parties. Needs-attention: a contractor
|
|
paid without W-9 facts; a 1099 party paid over the threshold.
|
|
|
|
Money with a party (Stephen, 2026-10-15: "how much have I spent on Apple
|
|
versus how much did I get back"): paid out, received in, and net, for a
|
|
chosen period (this year, last year, any year, all time), from
|
|
`ledger_core::parties::activity`; what it was for (the non-money lines by
|
|
account); a monthly out-vs-in chart; the 1099 amount for the calendar
|
|
year. The directory shows the same columns, sortable.
|
|
|
|
## The TIN vault
|
|
|
|
Decision (Stephen, 2026-10-15): wanderbooks holds full TINs, so what it
|
|
hands the CPA is complete, but never in the journal. The vault holds
|
|
everyone's, members included, and the CPA package carries them: "In the
|
|
vault, that's the WHOLE reason we built it. And it needs to be in the
|
|
damn CPA package."
|
|
|
|
- **Where:** `vault/` in the books dir, listed in `.gitignore`. Never in
|
|
the hash-chained journal, the SQLite projection, git, an export other
|
|
than the 1099 worksheet and the CPA package (saved outside the books
|
|
dir), or anything the viewer draws. Nothing outside the vault holds
|
|
more than the last four digits.
|
|
- **Who:** any party, whatever its roles: every member (their K-1 needs
|
|
it) and every 1099 payee. The books keep the party's TIN facts (type,
|
|
last four, the vault record's hash; `party.tin_set`), which never
|
|
disagree with its W-9 facts' last four. The viewer shows "TIN on file
|
|
••••1234 (SSN)" on the party's page, never more; clicking it copies the
|
|
full number (right-click: digits only), read from the vault at that
|
|
moment with the key in force, so people can paste it into a form
|
|
(Stephen, 2026-10-16: "people still need to actually access these
|
|
things"). The viewer never creates a key: without one it says the vault
|
|
is locked.
|
|
- **What:** per party, the full TIN, its type (SSN, EIN, ITIN), and the
|
|
W-9 file itself, each encrypted at rest (XChaCha20-Poly1305, one random
|
|
nonce per record; the file format carries a version and the party id as
|
|
associated data, so a record can't be moved to another party).
|
|
- **Key:** a random 256-bit key in a key file outside the books dir
|
|
(default `~/.config/wanderbooks/vault.key`, mode 0600; `[vault] key_file`
|
|
in wanderbooks.toml overrides it), created on first use. No OS keyring
|
|
or other system service (Stephen, 2026-10-15: no external dependencies),
|
|
so the vault works anywhere wanderbooks runs. Recovery is a copy of that
|
|
file in the human's password manager; without it the vault can't be
|
|
read (the books still open and verify).
|
|
- **Journal:** `party.w9_set` records that a W-9 was received, its date,
|
|
entity type, codes, and last four, plus a hash of the vault record, so
|
|
the books prove a W-9 existed without holding it.
|
|
- **Purge:** `books.purge_tin {party, reason}` deletes the vault record
|
|
(after the retention period, or on request) and journals that it was
|
|
purged; the journal's facts stay.
|
|
- **Use:** `books.vendor_1099` with `include_tins: true` and
|
|
`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 written only to `save_to`, outside the books dir, and
|
|
the tool returns its path; it is refused without one. 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, a restored backup) still
|
|
open and verify; the 1099 worksheet and the CPA package then say which
|
|
TINs are missing (`tin_gaps`), and the package is still built.
|
|
- **Tools** (built; `docs/mcp-interface.md` "Tools — the TIN vault"):
|
|
`books.set_w9` takes `tin` + `tin_type` and `w9_file_path` (the form
|
|
goes to the vault, never the attachment store) and journals the last
|
|
four with `vault_record`; facts that disagree with the vault's TIN are
|
|
refused, and clearing facts with a vault record needs a purge first.
|
|
With a TIN it also records the party's TIN facts. `books.set_tin
|
|
{party, tin, tin_type}` stores any party's TIN (a member's SSN, say) the
|
|
same way and journals `party.tin_set`; a party whose W-9 facts name
|
|
another last four is refused (a payee's new TIN comes with its W-9).
|
|
`books.get_w9 {party, save_to}` writes the form to a new file outside the
|
|
books dir and returns only the path. `books.purge_tin {party, reason,
|
|
confirmed?}` previews, then deletes the records (no key needed) and
|
|
journals the purge as `party.w9_set` with `vault_record` cleared and
|
|
`kept_at` saying when and why, and `party.tin_set` with `vault_record`
|
|
cleared and `purged` saying when and why. `books.vendor_1099 {include_tins: true}`
|
|
is the one tool output with full TINs (with a warning; `save_csv_to`
|
|
writes the CSV outside the books dir); `books.export_cpa_package
|
|
{save_to}` writes them in its zip, outside the books dir, and returns
|
|
only the path. 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.
|
|
|
|
## Attachments
|
|
|
|
### Subjects
|
|
|
|
An attachment links to any number of subjects:
|
|
|
|
```text
|
|
Subject = Entry(id) | Account(id) | Reconciliation(id) | Asset(id) | Party(id)
|
|
| Invoice(id) | Bill(id) | Period(id) | PayrollRun(id) | Schedule(id)
|
|
| Company
|
|
```
|
|
|
|
`attachment.linked {attachment_id, subject}` and `attachment.unlinked
|
|
{attachment_id, subject, reason}` replace the single `entry_id` link (old
|
|
`receipt.attached` with an entry and `receipt.linked` replay as links to
|
|
that entry). A receipt is an attachment linked to an entry; the receipt
|
|
policy counts links to the entry, as now. A file stored once can be
|
|
linked to many subjects (no more refiling).
|
|
|
|
Tools (built; `docs/mcp-interface.md` "Tools — attachments"):
|
|
`books.link_attachment`, `books.unlink_attachment` (refused for a posted
|
|
entry's last receipt under the receipt policy, and for a standing
|
|
reconciliation's statement), `books.attachments {subject}`, `link_to` on
|
|
`books.attach_receipt` and `books.file_document` (all or nothing), and
|
|
`links` on every listed attachment. `books.file_document` takes the kinds
|
|
below as its category (`receipt` is `books.attach_receipt`'s).
|
|
|
|
### Kinds
|
|
|
|
`receipt`, `statement`, `contract`, `formation`, `legal`, `tax`,
|
|
`accounting`, `photo`, `warranty`, `import_source`, `other`. A statement
|
|
carries `{account_id, period_start, period_end, opening_balance,
|
|
closing_balance}`, checked against the account's currency.
|
|
|
|
### Reconciliations
|
|
|
|
`books.reconcile` with `confirmed: true` records a reconciliation:
|
|
`reconciliation.completed {id, account_id, statement_date,
|
|
statement_balance, cleared: [(entry_id, line)], attachment_id}`. It
|
|
refuses unless the difference is exactly zero and a statement for that
|
|
account and date is attached (the receipt rule's twin). A reconciliation
|
|
is never edited; a later one supersedes it; `reconciliation.voided
|
|
{id, reason}` exists for mistakes. While it stands, the lines it
|
|
recorded as cleared can't be un-cleared (void it first), and nothing is
|
|
journaled when it is refused (a withdrawn statement file included). A
|
|
statement recorded wrong is voided (`statement.voided {attachment_id,
|
|
reason}`, refused while a reconciliation rests on it) and recorded again
|
|
on the same file; withdrawing a statement's file retires its statement. Viewer: per-account reconciliation
|
|
history with its statement; Needs-attention: an account with activity not
|
|
reconciled through last month's end.
|
|
|
|
Tools (built; `docs/mcp-interface.md` "Tools — bank reconciliation"):
|
|
`books.record_statement`, `books.statements`, `books.reconcile` (preview
|
|
by default; `statement_attachment` + `confirmed: true` records it),
|
|
`books.reconciliations`, `books.void_reconciliation`,
|
|
`books.void_statement`; `books.show
|
|
reconciliation`; `books.digest` `unreconciled_accounts` and the
|
|
viewer's Needs attention, one rule (`ledger_store::reconcilable`): the
|
|
cash accounts (`[books] cash_accounts`, else assets named for cash), the
|
|
card accounts (liabilities named "card", before their first statement),
|
|
and other liabilities with statements, not reconciled through last
|
|
month's end). The statement of a reconciliation not voided can't be
|
|
unlinked from it.
|
|
|
|
### Import sources
|
|
|
|
Built: file the export (`books.file_document` category `import_source`)
|
|
and pass `source_attachment_id` to `books.import_bank_csv`; it is linked
|
|
to the bank account imported into. Not to the drafts it creates: a file
|
|
linked to an entry counts as its receipt, and a bank export is not one
|
|
(DECISION, 2026-10-15, building the MCP tools). Recording the row each
|
|
draft came from is not built yet.
|
|
|
|
### Projection
|
|
|
|
SQLite: `attachment_links(attachment_id, subject_kind, subject_id)`,
|
|
`statements(attachment_id, account_id, period_start, period_end, opening,
|
|
closing)`, `reconciliations(...)`, `reconciliation_lines(...)`; the old
|
|
`attachments.entry_id` column is kept and filled for entry links during
|
|
the migration so older readers still work.
|
|
|
|
## Testing
|
|
|
|
Spec blocks on every command; spec-blind suites per crate; the
|
|
refusal-leaves-books-unchanged property; replay and migration from v10
|
|
books; the GUI's every-event-has-text test; mutation on the new modules.
|
|
|
|
## After this: the completeness engine
|
|
|
|
Decided (Stephen, 2026-10-15), built after parties and attachments: one
|
|
set of completeness rules in the core (asset: tag, serial, tax basis,
|
|
acquisition link, label; entry: receipt, 1099 vendor tag; account:
|
|
reconciled through last month; month: depreciation, amortization, close;
|
|
party: W-9 before paying a contractor; settings: no problems; and the
|
|
rules this design adds), `next_steps` on every tool response naming the
|
|
exact call that completes each, `books.todo` as the one punch list behind
|
|
the digest, the viewer's Needs attention, and the session-start hook, and
|
|
enforcement where a rule is a rule (tax basis at registration). The
|
|
standard: do it now, completely.
|
|
|
|
### Counterparties
|
|
|
|
Decided (Stephen, 2026-10-15): the books should be easy to make complete
|
|
without forcing one style. `[books] parties = "off" | "suggest" |
|
|
"require"`, default `"suggest"`:
|
|
|
|
- `off`: never mention counterparties.
|
|
- `suggest`: a draft or preview without a party names the likely one
|
|
(existing parties matched against the description, the receipt's text,
|
|
the vendor), or offers to create one; `next_steps` carries the exact
|
|
call. Nothing is refused.
|
|
- `require`: posting is refused until the entry names a party or is
|
|
marked as having none (transfers, depreciation, amortization, closes
|
|
are exempt by kind).
|
|
The engine's own posting paths name their counterparty or are refused
|
|
first, so nothing posts around the rule: `books.run_payroll` names each
|
|
line's employee party on the entry (each with their net pay) and, under
|
|
`require`, refuses a line without one; `books.remit_payroll_taxes`
|
|
takes `agency` (required under `require`); `books.dispose_asset` takes
|
|
`buyer` (required under `require` for a sale with proceeds; a
|
|
retirement with no proceeds and no buyer is marked as having none).
|
|
- **Marks follow the chain:** a reversal carries its original's parties,
|
|
shares, vendor tag, and no-party mark; marking or tagging any entry of a
|
|
reversal chain marks or tags them all. Tagging a vendor (or any party)
|
|
clears a no-party mark, so an entry never names a party and carries the
|
|
mark at once.
|
|
|
|
Also: memorized matches (journaled rules like description contains
|
|
"OPENROUTER" → OpenRouter, applied by bank-CSV import and new drafts,
|
|
reviewable and removable); a backfill punch list grouping untagged
|
|
entries by likely party for one confirmation per group; 1099 nudges (a
|
|
party given contractor or landlord, or a payment that looks like
|
|
services to an individual, asks for the role and the W-9).
|
|
|
|
## After that: updates (check and seamless upgrade)
|
|
|
|
Decided (Stephen, 2026-10-15), after the completeness engine. New versions
|
|
fix accounting, tax, and legal rules, so books running an old version may
|
|
be wrong; staying current is part of keeping correct books.
|
|
|
|
- **Check:** the MCP server checks for a newer release when it starts and
|
|
once a day while running (and the viewer and `just session-start` ask
|
|
it). Releases carry notes and a severity: a fix to an accounting, tax, or
|
|
legal rule (a 1099 threshold, a depreciation rule, a refusal that was
|
|
wrong) is marked so, and says which books it may affect. `[updates]
|
|
check = "daily" | "never"` in wanderbooks.toml; the check contacts only
|
|
the release host and sends nothing about the books.
|
|
- **Surface:** the update is a completeness item (`books.todo`), in
|
|
`books.digest`, a banner in the viewer, and the session-start hook; the
|
|
MCP instructions tell the agent to raise it with the human at once, with
|
|
the release notes, and to treat a rules fix as urgent.
|
|
- **Upgrade:** one step (`wanderbooks update`, or the agent offers it):
|
|
download the release, verify its signature against a key pinned in the
|
|
installed build, then rehearse: open the books read-only with the new
|
|
build and run the full verify (replay, projection, migrations) before
|
|
switching. Swap binaries atomically, keep the previous build for
|
|
rollback, restart the viewer, and have the MCP server exit cleanly so the
|
|
client reconnects to the new one. Never downgrade; never touch the
|
|
journal except through the new build's ordinary migrations.
|
|
- **After:** the agent re-runs whatever the fix affects (for example,
|
|
`books.vendor_1099` for the year, or the register) and tells the human
|
|
what changed in their books, if anything.
|