fix: Windows reads of lock files, update paths cfg(unix), merge main again (P11c)
spec_vault swept every file under a book, lock files included; on Windows a held LockFileEx lock is mandatory and the read failed. The locks are already dedicated files (never data), so the test skips *.lock. Merged main (P13 step 6, the macOS server.log assert, P11a's AppImage identity): host.rs keeps main's five-field identity on Unix and size/last-write on Windows; the update poll and set_program_version are Unix-only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
commit
7749afc557
234 files changed
+13028
-18411
No files matched your search
+51
-7
@@ -31,9 +31,9 @@ the specification in `bookspec/` (which never depends on the engine).
|
||||
| `ledger-core` | the semantics engine: money, chart, journal, `Ledger` commands, reports, periods, assets, schedules, parties, A/R and A/P, payroll, projects, settings and conventions, the kernel and its Kani harnesses | nothing |
|
||||
| `ledger-vault` | the TIN vault: XChaCha20-Poly1305 records under the book's key | nothing |
|
||||
| `ledger-config` | the `wanderbooks.toml` codec (adoption in, `books.export_settings` out) and the receipt policy until P4 moves it into the core | `ledger-core` |
|
||||
| `ledger-store` | the source of truth: the hash-chained JSONL journal, its format gate, the SQLite projection (schema 18; a file per schema), the blob store, backups, the access log, `Snapshot` for readers | `ledger-core`, `ledger-reader` (P6, D-61) |
|
||||
| `ledger-store` | the source of truth: the hash-chained JSONL journal, its format gate, the SQLite projection (schema 18; a file per schema), the blob store, backups, the access log, `describe.rs` (the journal in words, for `books.journal_history`), `Snapshot` for readers | `ledger-core`, `ledger-reader` (P6, D-61) |
|
||||
| `ledger-reader` | the one reader: a PDF's text and pages, an XLSX's cells as exact decimal strings, plain text (P6; a plain library, in-process, D-61); `ledger-import` re-exports it as `extract` | none (`hayro`, `zip`, `quick-xml`) |
|
||||
| `ledger-import` | formats: QuickBooks CSV in; IIF, XLSX, Typst PDFs (statements, the invoice, labels) out | `ledger-core`, `ledger-reader` |
|
||||
| `ledger-import` | formats: QuickBooks CSV in; IIF, XLSX, Typst PDFs (statements, the invoice, labels, the audit workpaper) out | `ledger-core`, `ledger-reader` |
|
||||
| `ledger-package` | the shared read-only view over `Store` or `Snapshot` (`PackageBooks`): the CPA package, the 1099 worksheet, the invoice document, Needs attention | `ledger-core`, `ledger-store`, `ledger-config`, `ledger-import`, `ledger-vault` |
|
||||
| `ledger-mcp` | the books server: the MCP tools (122), resources, the handbook, scopes, the door and the access log, backups' scheduling | all of the above |
|
||||
| `ledger-host` | the host and its leases, the agent bridge (`wanderbooks mcp`), tokens in 0600 files under the data root (D-63), the owner's commands, uploads and downloads, `update` and `rollback` (P11a) | `ledger-store`, `ledger-mcp`, `ledger-config` |
|
||||
@@ -59,13 +59,26 @@ closure. The reader now lives in the small library crate `ledger-reader`;
|
||||
re-exports it as `extract`), the store no longer builds the typesetter,
|
||||
and `ledger-import` still never depends on the store.
|
||||
|
||||
Audits (P8, rules.md B-45) are split by what each layer can see. The pure
|
||||
checks over a ledger are `ledger_core::audit` (`statements`, with the facts
|
||||
a ledger cannot know handed in by the caller: the receipts filed, the
|
||||
statement of cash flows the role settings give, the settings problems, the
|
||||
undecided 1099 vendors); what only the store can see (the chain, the
|
||||
database against a replay, the stored files, the access log, duplicate
|
||||
files) and the audit and attestation events are `ledger_store::audit` (a
|
||||
child module of `store`, because it reads the store's private state); the
|
||||
workpaper's PDF is `ledger_import::workpaper` with its Typst template in
|
||||
`ledger-import`'s `templates/` beside the other documents; the backups'
|
||||
standing is read by the server, which alone knows the backups
|
||||
(`BackupStanding`), and the tools are `ledger-mcp`'s `tools/audit.rs`.
|
||||
|
||||
`ledger-core` itself (`crates/ledger-core/src/`): `money`, `account`,
|
||||
`journal`, `ledger` (the command surface and replay), `kernel` (every
|
||||
money arithmetic, generic over `Amount`) and `proofs` (its Kani
|
||||
harnesses), `reports`, `cashflow`, `analysis`, `periods`, `assets`,
|
||||
`schedules`, `recurring`, `payouts`, `payroll`, `form1099`, `tax`,
|
||||
`classes`, `parties`, `arap`, `settings`, `conventions`, `calendar`,
|
||||
`shares`, `tax_capital`, `rules`, `error`; its tests in `tests/`
|
||||
`shares`, `tax_capital`, `rules`, `audit` (the audit programme's pure checks, P8), `error`; its tests in `tests/`
|
||||
(`invariants.rs`, `properties.rs`, `spec_*.rs`, `kernel_bridge.rs`,
|
||||
`model.rs`, `bookspec.rs`, `golden/`).
|
||||
|
||||
@@ -365,9 +378,21 @@ downgrading is theirs). Anything else (an AppImage, whose file is named by
|
||||
without the agent noticing. The format gate stays the server's (3.9):
|
||||
`rollback` refuses while a book declares a format or feature the kept
|
||||
version does not know;
|
||||
- an AppImage is the exception to the restart: the host runs from the old
|
||||
image's mount, whose path does not change, so it keeps running the old
|
||||
version until the books are next closed and opened (docs/packaging.md).
|
||||
- an AppImage runs the program from a mount that goes away with the image,
|
||||
so the host learns the image's own path from `APPIMAGE` (the one variable
|
||||
`start.rs` adds to its scrubbed environment, only when it names an
|
||||
absolute file; the host reads it about itself and never tells an agent),
|
||||
runs and restarts from that file, and tells a replaced program by its
|
||||
inode, size and last write, like any other;
|
||||
- updates are also **staged** (step 4): while the books are open the host
|
||||
looks at the feed at most once a day (`update_check` knob; the look is
|
||||
noted in `updates/checked`), and only downloads and verifies a newer
|
||||
release into `<root>/updates/staged/`. It never swaps. The staged program
|
||||
is applied by an opener just before it starts a host (no host runs, so no
|
||||
call is cut off), after a second check against the key, or by
|
||||
`wanderbooks update`, the owner's say-so, which reuses the staged copy. A
|
||||
local release is named in `<root>/updates/feed` (a `file://` address, then
|
||||
the key file), the same rule as `--trusted-key`.
|
||||
|
||||
`deploy/packaging/` holds the Linux packaging: `packager.toml`
|
||||
(cargo-packager: `.deb`, AppImage, the pacman tarball with its PKGBUILD),
|
||||
@@ -375,4 +400,23 @@ the rpm table in `crates/ledger-cli/Cargo.toml`, `aur/PKGBUILD` (the AUR
|
||||
package, which repacks the release's tarball), `build-linux.sh`,
|
||||
`repo-indexes.sh` (the apt and dnf indexes), `local-release.sh` (a
|
||||
throwaway-keyed feed on this machine) and `test-linux-packages.sh`.
|
||||
`.github/workflows/release.yml` builds them on a version tag.
|
||||
`.github/workflows/release.yml` builds them on a version tag. The macOS
|
||||
bundle is `packager-macos.toml`, `Info.plist` and `build-macos.sh` (an
|
||||
unsigned `.dmg` from CI's `macos` job; signing and notarization are step 5).
|
||||
|
||||
## The result envelope and the guards (`ledger-mcp`, P5)
|
||||
|
||||
Every tool result is given the same fields in one place: `call_tool` runs the
|
||||
tool, then `next_steps::apply` adds `next_steps`, `guards`, `warnings` and
|
||||
(on a tool that is not read-only) `head`, in the structured content and in
|
||||
the text block alike, never overwriting a field the tool wrote. Next steps
|
||||
come from the tool's own result (`next_steps::tool_steps`: the confirm after
|
||||
a preview, what a post, a draft, a statement or a close leads to) and, after
|
||||
a change to an entry or an invoice, from the viewer's own Needs-attention
|
||||
items about it (`ledger_package::find_with`). `guards.rs` is the registry of
|
||||
named refusals, each with the tool that remedies it; a refusal whose text
|
||||
carries a guard gets it as `structuredContent.guards`. The prompts
|
||||
(`tools/prompts.rs`, text in `prompts/*.md`) and the resource templates are
|
||||
served by the same `ServerHandler`; `books.start` is loaded up front by
|
||||
clients that defer tools (`_meta`). `ledger-store`'s `describe.rs` is the
|
||||
one place that turns a journal line into a sentence for an agent.
|
||||
+228
-31
@@ -12,12 +12,16 @@ locked to one assistant.
|
||||
|
||||
| Crate | Role | Spec |
|
||||
|---|---|---|
|
||||
| `ledger-core` | the engine: money, entries, the laws; every rule the tools enforce | `docs/spec/rules.md`, `docs/verification.md` |
|
||||
| `ledger-package` | the read-only views shared with the viewer: reports, aging, the CPA package, Needs attention (the one list `books.start` and the viewer both draw) | `docs/architecture.md` |
|
||||
| `ledger-store` | the hash-chained journal (the source of truth), the SQLite projection, the blob store | `docs/receipt-store.md` |
|
||||
| `ledger-mcp` | the books server: the tools, resources and handbook over store + core | this document |
|
||||
| `ledger-host` | the agent bridge (`wanderbooks mcp`), the host and its leases, tokens | `plan/served.md` 3.3, 3.9 |
|
||||
|
||||
(The overview sections of this document date from the first MCP phase; P5
|
||||
rewrites them. The per-tool sections are held to the code by a test.)
|
||||
(P5 rewrote the overview sections for the surface as it is: the result
|
||||
envelope, `books.start` v2, the prompts, the resource templates and the
|
||||
journal in words, below. The per-tool sections are held to the code by a
|
||||
test: `every_tool_is_documented_and_every_documented_tool_exists`.)
|
||||
|
||||
Rust, `rmcp` SDK (the Rust MCP SDK). The MCP crate holds no accounting
|
||||
logic: it translates tool calls into `Ledger` commands and formats results.
|
||||
@@ -106,15 +110,37 @@ Conventions, decisions, open items and approvals live in the books too
|
||||
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 },
|
||||
- `books.start { focus?, agent? }` (read-only, at most 8 KB; `_meta`
|
||||
`anthropic/alwaysLoad: true`, so a client that defers tools loads it up
|
||||
front) → `{ book: { id, url, name,
|
||||
timezone, today, fiscal_year, base_currency, basis }, you?: { principal,
|
||||
kind, scopes, client_id? }, head: { seq, hash },
|
||||
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? }],
|
||||
truncated?, handbook_sections, format_upgrade?: {
|
||||
guard, format, adds, how }, next_steps: [{ tool, args?, why }],
|
||||
isolation }`. The connect text names it as the first call.
|
||||
drafts: { building, awaiting_evidence: [{ entry_id, date, amount, note
|
||||
}] }, reconciliation: { tracked, never_reconciled, behind } (account
|
||||
codes), backup: { last_at, through_seq, stale }, prompts: [{ name, args
|
||||
}], needs_attention: { items: [{ id, severity, kind, title, tool?, args?
|
||||
}], counts: { kind: n } }, truncated?, handbook_sections,
|
||||
format_upgrade?: { guard, format, adds, how }, next_steps: [{ tool |
|
||||
prompt, args?, why, required }], isolation }`. The connect text names it
|
||||
as the first call. `book.url` is the book's path on this server
|
||||
(`/b/ID/mcp`), never a path on a disk. `you` is the token's own facts
|
||||
(its name, the kind `agent`, its scopes) and the client program's name; it is
|
||||
absent on the `books://start` resource, which no token's call made.
|
||||
`agent: { model?, reasoning_effort?, harness? }` is what the agent says
|
||||
about itself: self-reported, never checked, written to the call's access
|
||||
record (`books.access_log` shows it as `agent`) and used for nothing
|
||||
else. `drafts` lists the first ten drafts waiting on their evidence;
|
||||
`reconciliation` the accounts reconciled to statements and which are
|
||||
behind or never reconciled; `needs_attention` the twelve most urgent
|
||||
items (problems first) with every item counted by kind: the same list
|
||||
the viewer draws, from one function (`ledger_package::attention`). If
|
||||
the answer would pass 8 KiB it shortens, in this order: convention
|
||||
summaries, attention items (to four), waiting drafts (to three), then
|
||||
the lists below, each saying how many more in `truncated`.
|
||||
`format_upgrade` is there only for books waiting for a format upgrade
|
||||
(below); its next step comes first, before anything else, at every
|
||||
start until it is done: `books.handbook {section: "format-upgrade"}`,
|
||||
@@ -128,8 +154,9 @@ settings-and-conventions section says how to use them.
|
||||
`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); the
|
||||
onboarding step (`books.handbook {section: "contract"}`, when the chart
|
||||
is empty or `focus` is `onboarding`); `books.open_items` (an item due
|
||||
onboarding steps (the `onboard` prompt, `books.set_setting`,
|
||||
`books.chart_template` and `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,
|
||||
failing, paused or failing its drill); then Needs attention, by
|
||||
severity, at most 12, each one `{tool, args?, why}` whose `why` is the
|
||||
@@ -138,11 +165,11 @@ settings-and-conventions section says how to use them.
|
||||
server's own damage check and are not repeated); then the focus's step
|
||||
(`close`:
|
||||
`books.conventions {kind: "close_step"}`; `audit`: `books.verify`); then
|
||||
always `books.handbook {section: "contract"}` and `books.digest`.
|
||||
`books.draft_entry` (the books are set up), `books.handbook {section:
|
||||
"contract"}` and `books.digest`. Attention steps are `required` when the
|
||||
item is a problem.
|
||||
`focus` is `daily` (the default), `onboarding`, `close` or `audit`;
|
||||
anything else is refused. 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`.
|
||||
anything else is refused. Also `books://start`.
|
||||
**The format upgrade** (plan 3.9, D-39, rules.md B-27). Books an older
|
||||
wanderbooks wrote are in an older journal format: they are served on it,
|
||||
and every tool that needs nothing new works as before. A tool whose
|
||||
@@ -173,6 +200,16 @@ as it is made.
|
||||
`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).
|
||||
**Correcting a source** (P5, journal feature `source_revisions`):
|
||||
`books.set_convention` with `reason` and the text exactly as it stands
|
||||
changes only who decided it, when and on what (`decided_by`,
|
||||
`decided_on`, `citation`, `note`), journaled as `convention.source_revised
|
||||
{id, source, reason}`; the revision number and the words do not move.
|
||||
Without `reason` the same call is refused ("already says exactly that;
|
||||
to correct only who decided it, when or on what, add reason"), and with
|
||||
`reason` and different words it is refused ("changes more than its
|
||||
source"). Books that have not had the owner's upgrade refuse it with
|
||||
`format_upgrade_pending`.
|
||||
- `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
|
||||
@@ -190,8 +227,14 @@ as it is made.
|
||||
decision without them journals the bytes it always did, and one with
|
||||
them needs the journal feature `projects`. `books.decisions` gives each
|
||||
decision's `projects: [{ id, name }]` (left out when none); `project`
|
||||
filters to the decisions naming one.
|
||||
- `books.raise_item { title, detail_md?, due?, decided_by, ... }` → `{ id }`,
|
||||
filters to the decisions naming one. `books.record_decision { revises:
|
||||
ID, reason, decided_by: [..], decided_on, citation? }` corrects only
|
||||
that decision's source (who decided it, when, on what; its words stay,
|
||||
and `subject` and `decision_md`, if given, must be the ones it has):
|
||||
`decision.source_revised {id, source, reason}`, returning `{ id }`.
|
||||
- `books.raise_item { title, detail_md?, due?, decided_by, ... }` → `{ id }`
|
||||
(`books.raise_item { revises: ID, reason, decided_by, ... }` corrects
|
||||
only that item's source: `open_item.source_revised`),
|
||||
`books.resolve_item { id, resolution, decided_by, ... }`,
|
||||
`books.open_items { include_resolved? }`.
|
||||
- `books.approve_entry { entry_id, approver, note?, approved_on? }`: the
|
||||
@@ -350,8 +393,8 @@ books' files; no tool title or description, connect text or handbook
|
||||
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.
|
||||
Tool descriptions are at most 700 characters (`tool_descriptions_fit_700_characters`
|
||||
allows none longer; the long text lives in the handbook).
|
||||
|
||||
**Annotations.** Every tool has a `title` and a `readOnlyHint`; every tool
|
||||
that changes anything also has a `destructiveHint`, true for these 31:
|
||||
@@ -443,9 +486,44 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
|
||||
`books.run_schedules`, `books.dispose_asset`, `books.reconcile`,
|
||||
`books.close_period`, `books.create_invoice`, `books.create_bill`,
|
||||
`books.receive_payment`, `books.pay_bill`, `books.write_off_invoice`,
|
||||
`books.purge_tin`, `books.consolidate_duplicates` and
|
||||
`books.discard_draft`. (`books.void_document`, `books.run_depreciation`
|
||||
`books.purge_tin`, `books.consolidate_duplicates`,
|
||||
`books.discard_draft` and `books.audit`. (`books.void_document`, `books.run_depreciation`
|
||||
and `books.run_recurring` post at once and have no `confirmed`.)
|
||||
- **The result envelope (P5, plan 4.6).** Every tool result, whatever the
|
||||
tool, is given the same fields in one place (`call_tool`,
|
||||
`next_steps.rs::apply`), after the tool has answered; a tool's own field of
|
||||
the same name is kept, never overwritten:
|
||||
`next_steps: [{ tool | prompt | command, args?, why, required }]` (what to
|
||||
do next: after a preview, the same call with `confirmed: true` and the
|
||||
returned `preview`; after a post, `books.show` of the entry, plus the
|
||||
Needs-attention items about that entry or invoice, the viewer's own,
|
||||
`required` when they are problems; after a close, `books.verify`; after
|
||||
`books.export_cpa_package`, a `command` step carrying the download
|
||||
command; every other tool that can change the books ends with
|
||||
`books.digest`), `guards: [{ code, message, remedy_tool, remedy_args? }]`
|
||||
(empty on success; on a refusal that is a guard, the refusal's
|
||||
`structuredContent` is `{ guards: [..] }` and its text is unchanged),
|
||||
`warnings: [..]` (a tool's own `warnings` is that field), and, on every
|
||||
tool that is not read-only, `head: { seq, hash }`, the journal's head
|
||||
after the call's last write (or as it stands, when the call wrote
|
||||
nothing). A tool that can change the books always has `head` and
|
||||
`next_steps`; a read has `next_steps` only when something follows it, so
|
||||
a resource and its tool say the same (`books://settings` is
|
||||
`books.settings`). `guards` is on a refusal; a success that has no
|
||||
warnings has no `warnings` key. A tool that can change the books always answers with all four;
|
||||
a read has `next_steps` only when something follows it and no `guards`
|
||||
or `warnings` of the envelope's, so a resource and its tool say the same
|
||||
(`books://settings` is `books.settings`). The guard codes are the enum in `guards.rs`, each with its
|
||||
remedy tool: `stale_preview` (the tool again, previewed),
|
||||
`format_upgrade_pending` (`books.start`), `receipt_required`
|
||||
(`books.attach_receipt`), `approval_required` (`books.approve_entry`),
|
||||
`party_shares` and `party_required` (`books.set_entry_party`),
|
||||
`open_drafts` (`books.list_drafts`), `untagged_control_line`
|
||||
(`books.control_check`), `duplicate_vendor_ref` (`books.search_entries`)
|
||||
and `first_number_locked` (`books.settings`). Tests:
|
||||
`every_mutating_result_has_head_and_next_steps`,
|
||||
`the_envelope_keeps_what_a_tool_said_and_carries_a_refusal_guard`,
|
||||
`every_guard_names_an_existing_tool`.
|
||||
- **Errors are typed strings** mirroring `LedgerError` variants, with the
|
||||
offending values included (e.g. unbalanced totals).
|
||||
|
||||
@@ -1958,7 +2036,17 @@ posting.
|
||||
## Resources (read-only)
|
||||
|
||||
Hand-rolled on `ServerHandler` (rmcp 3.x has no resource macros):
|
||||
`list_resources` advertises, `read_resource` renders.
|
||||
`list_resources` advertises, `list_resource_templates` advertises the
|
||||
templates below (P5), `read_resource` renders.
|
||||
|
||||
- Templates: `books://entries/{id}` (as `books.get_entry`),
|
||||
`books://attachments/{id}` (as below), `books://journal/{seq}` (that line
|
||||
in words: `books.journal_history`'s answer for it),
|
||||
`books://reports/{kind}{?start,end,as_of}` (`kind` is `trial-balance`,
|
||||
`balance-sheet` or `profit-and-loss`; `as_of` for the first two, `start`
|
||||
and `end` for the third; omitted: today, or the fiscal year to date) and
|
||||
`books://digest{?since,since_seq,as_of}` (`since_seq` wins over `since`;
|
||||
bare: this month so far).
|
||||
|
||||
- `books://start`, `books://settings`, `books://conventions`
|
||||
- `books://chart`
|
||||
@@ -2118,8 +2206,8 @@ snapshots; see plan 8.2 S4.
|
||||
receivable line tagged with the invoice and the receivable side one line
|
||||
per (account, project) part.
|
||||
- `customer` is the customer's id, its party's id or its name (a name
|
||||
two customers answer to is refused); `party_id` is the old spelling of
|
||||
the same thing (a customer id), kept until P5.
|
||||
two customers answer to is refused); it is required (the old `party_id`
|
||||
spelling is gone, P5).
|
||||
- The due date: `due_date` as given (and it overrides `terms`); else
|
||||
`terms` ("Net 30", "Net 15 EOM", "2/10 net 30", "Due on receipt",
|
||||
"Due 2026-11-15", rules.md B-39); else the customer's
|
||||
@@ -2170,8 +2258,8 @@ snapshots; see plan 8.2 S4.
|
||||
sha256 }, warnings?, notes? }`.
|
||||
Confirmed it posts Dr each line's account / Cr `2000` Accounts Payable,
|
||||
the payable line tagged with the bill and one debit line per (account,
|
||||
project) part; `vendor` is a vendor id, its party's id or its name, and
|
||||
`party_id` the old spelling (a vendor id).
|
||||
project) part; `vendor` is a vendor id, its party's id or its name
|
||||
(required; the old `party_id` spelling is gone, P5).
|
||||
- `vendor_ref` is the vendor's own invoice number: required in books
|
||||
that have declared `documents_v2` (B-40). The same vendor and number
|
||||
twice is refused unless `duplicate_reason` says why, which is
|
||||
@@ -2194,8 +2282,8 @@ snapshots; see plan 8.2 S4.
|
||||
- `warnings` names an expense account it created with no tax category;
|
||||
`notes` one per finished or abandoned project a line names (none for
|
||||
on-spec: a cost of on-spec work is an expense like any other).
|
||||
- `books.receive_payment { date, deposit_to | cash_account, instrument?,
|
||||
reference?, invoice | document_id, amount? | applications: [{ invoice,
|
||||
- `books.receive_payment { date, deposit_to, instrument?,
|
||||
reference?, invoice, amount? | applications: [{ invoice,
|
||||
amount, discount?, discount_reason? }], fee?: { amount, account?,
|
||||
vendor? }, paid_in_full_marking?, confirmed?, preview? }` → `{ preview?,
|
||||
entry_id, documents: [{ ref, number?, outstanding, status }], deposited,
|
||||
@@ -2217,11 +2305,11 @@ snapshots; see plan 8.2 S4.
|
||||
repeat the single document's figures. An overpayment is refused with the
|
||||
excess and the remedy (a plain entry to 2300 Customer Deposits).
|
||||
`paid_in_full_marking: true` adds a UCC §3-311 warning and records
|
||||
nothing else. `document_id` and `cash_account` are the older names of
|
||||
`invoice`/`bill` and `deposit_to`/`paid_from`. The books record
|
||||
nothing else. (The older names `document_id` and `cash_account` are
|
||||
gone, P5.) The books record
|
||||
payments; they never collect them (D-45).
|
||||
- `books.pay_bill { date, paid_from | cash_account, instrument?,
|
||||
reference?, bill | document_id, amount? | applications: [{ bill, amount,
|
||||
- `books.pay_bill { date, paid_from, instrument?,
|
||||
reference?, bill, amount? | applications: [{ bill, amount,
|
||||
discount?, discount_reason? }], confirmed?, preview? }` → as `receive_payment`, per bill and
|
||||
one vendor (B-42): Dr A/P each bill's cash and discount, Cr the payer
|
||||
account the cash, Cr purchase discounts the discounts. A discount on a
|
||||
@@ -2466,7 +2554,7 @@ until the owner upgrades them.
|
||||
|
||||
## Tools — CPA handoff
|
||||
|
||||
- `books.export_cpa_package { as_of?, cash_accounts?, financing_accounts?,
|
||||
- `books.export_cpa_package { as_of?, without_audit_reason?, cash_accounts?, financing_accounts?,
|
||||
investing_accounts?, include_receipts?, include_tins?,
|
||||
allow_open_drafts?: { reason } }` → `{ file,
|
||||
tins_included, tin_gaps, warnings?, note }`: the zip,
|
||||
@@ -2604,3 +2692,112 @@ state and discard tools wait for the books' format upgrade).
|
||||
drafts_open_on_or_before: [draft], unreconciled: [...], settings_problems:
|
||||
[...] }` for the report's end date, and take `strict: true` to be refused,
|
||||
naming what is open, instead of warned.
|
||||
|
||||
## Tools — the journal in words (P5)
|
||||
|
||||
- `books.journal_history { after_seq?, types?, subject?, limit? }` → `{
|
||||
lines: [{ seq, ts, type, summary, subject?, entry_id? }], more, head_seq,
|
||||
tail_repairs: [{ at, bytes }] }`. The journal in order, a sentence for
|
||||
each line (`summary`: what it did, to what, the short names a person
|
||||
typed; never a setting's value, never a full TIN), the line's own
|
||||
`subject` (`entry:12`, `invoice:3`, `bill:3`, `account:10`, `party:5`,
|
||||
`asset:2`) and the entry it belongs to. `types` takes an exact type
|
||||
(`entry.posted`) or a group ending in a dot (`entry.`); `subject` keeps
|
||||
the lines about one thing (a line is about the subject its type names and
|
||||
every one its payload refers to); `limit` is 50 by default, 200 at most;
|
||||
`more` says to ask again with the last `seq` as `after_seq`. Read-only,
|
||||
`books:read`. It is how a figure is traced to what produced it and the
|
||||
only way to read the journal; `tail_repairs` lists torn tails set aside
|
||||
when the books opened (when, how many bytes). The sentences come from
|
||||
`ledger-store`'s `describe.rs`.
|
||||
|
||||
## Prompts (P5)
|
||||
|
||||
The server advertises the prompts capability (also in the bridge's cold
|
||||
`initialize`). `prompts/list` and `prompts/get` need a token that may read.
|
||||
Each prompt's text is a file in `ledger-mcp/prompts/`, with the company's
|
||||
name and the arguments filled in; it names only tools that exist (a test
|
||||
checks). An argument is one token (letters, digits, `-`, `_`, `.`, `:`; at
|
||||
most 40 characters) of the shape the prompt asks for, so an argument can
|
||||
never rewrite the text; an unknown prompt, a missing, extra or malformed
|
||||
argument is refused.
|
||||
|
||||
| Prompt | Arguments | Drives |
|
||||
|---|---|---|
|
||||
| `onboard` | none | the company's facts, the chart, the policies, the opening balances, the trial-balance check |
|
||||
| `reconcile` | `account` (a code), `month` (YYYY-MM) | the statement, `books.record_statement`, `books.mark_cleared`, `books.reconcile` |
|
||||
| `month_end_close` | `month` (YYYY-MM) | the company's own close steps, reconciliations, drafts, schedules, checks, `books.close_period` |
|
||||
| `cpa_handoff` | `year` (YYYY) | `books.verify`, the periods, 1099 work, `books.export_cpa_package` |
|
||||
| `annual_compliance` | `year` (YYYY) | the year's filings, 1099s, assets, open items |
|
||||
| `audit` | `scope` (`month`, `annual`, `cpa_handoff` or `full`) | `books.verify`, `books.control_check`, the digest, evidence against entries (P8 fills in its own tools) |
|
||||
|
||||
`migrate`, `process_inbox` and `receipts_from_email` are not built (D-46,
|
||||
D-28, D-17).
|
||||
## Tools — audit and attestations (P8)
|
||||
|
||||
An audit is the books read as an auditor reads them, through a day, in one
|
||||
run: the same books give the same findings and the same workpaper. An
|
||||
attestation is a claim by a checker that it looked at something. The audit
|
||||
finds; the attestations stand behind it; the CPA package waits for both
|
||||
(rules.md B-45; decision D-9; the journal feature `audits`: the audit run
|
||||
and the attestations wait for the books' format upgrade, refused with
|
||||
`format_upgrade_pending` until then).
|
||||
|
||||
- `books.audit { scope, through?, confirmed?, preview? }` → `{ status:
|
||||
"preview" | "audited" | "already_run", preview, head, checks: [{ code,
|
||||
severity, predicate }], run?: { id, through, through_seq, scope,
|
||||
program_version, tb_hash, findings_sha256, workpaper_attachment,
|
||||
workpaper_pdf, at_current_head }, findings: [{ severity, code, subject,
|
||||
message }], summary }`. `scope` is `month`, `annual`, `cpa_handoff` or
|
||||
`full`; `through` defaults to today. Without `confirmed` it lists the
|
||||
checks and changes nothing; with `confirmed` and the `preview` digest it
|
||||
runs them, files the workpaper as two documents (category `workpaper`:
|
||||
JSON, and a PDF whose bytes depend only on the findings) and journals
|
||||
`audit.run`. The checks are the statements (trial balance, the
|
||||
accounting equation, the profit and loss against equity, the cash flow's
|
||||
footing), the A/R and A/P control accounts, closes, schedules and
|
||||
depreciation, the asset register, each posted entry against the receipt
|
||||
policy, open drafts, tax categories, 1099 decisions and the settings;
|
||||
then the journal's chain, the database against a replay, the stored
|
||||
files, the access log, duplicate files and the backups. The same books
|
||||
(the figure head), day, scope and programme return the run that exists
|
||||
(`already_run`) and file and journal nothing. "The same books" means no
|
||||
entry, document, close, settings or role change since: an audit, an
|
||||
attestation or a filed receipt does not change them. Needs books:write to
|
||||
preview, books:post to run.
|
||||
- `books.workpaper { id }` → `{ run, workpaper, pdf? }`: the workpaper as
|
||||
filed (what was checked, through which day and state, every finding) and
|
||||
its PDF handed over (`pdf` is a download command; kept 15 minutes).
|
||||
Read-only.
|
||||
- `books.attest_receipt { attachment_id, entry_id, amount, date, method?,
|
||||
note? }` → `{ attestation, bar, why_not_counted? }`. The checker's
|
||||
reading of the receipt's total and date, compared by the books with the
|
||||
posted entry's amounts (its debits, or any one line) and date; a
|
||||
difference is recorded with `disagrees: true` and never counts. The
|
||||
receipt must be filed on or linked to the entry.
|
||||
- `books.review_entry { entry_id, method?, note? }` → the same shape: a
|
||||
posted entry reviewed whole.
|
||||
- `books.attest_period { period, method?, note? }` → the same shape.
|
||||
`period` is an id or a name from `books.list_periods`. Refused unless an
|
||||
audit through the period's end stands at the books' current head; the
|
||||
attestation names that run.
|
||||
- `books.attestations { through?, kind? }` → `{ bar, attestations: [{ seq,
|
||||
kind, by, at, attachment_id?, entry_id?, period_id?, audit_run_id?,
|
||||
amount_checked?, date_checked?, disagrees, method, note?, counted }],
|
||||
counted }`. `kind` is `receipt`, `entry` or `period`. `counted` is read
|
||||
against `policy.attestation_bar` as it stands when asked and never
|
||||
stored: a bar change changes what counts, earlier claims too.
|
||||
|
||||
`method` is `{ model?, reasoning_effort?, adversarial?, verifier_agents? }`
|
||||
and is the client's own claim: the books record it and cannot verify it.
|
||||
`by` is the token's name. The bar (`policy.attestation_bar`, default
|
||||
`effort=high verifiers=2 adversarial=true`) holds every attestation to the
|
||||
effort, and a period attested on a year-level audit (`annual`,
|
||||
`cpa_handoff`, `full`) to the adversarial check and the verifier count too.
|
||||
|
||||
`books.export_cpa_package` gains `without_audit_reason?`: on books that
|
||||
declared `audits`, with `policy.cpa_package_requires_audit` at `refuse` (the
|
||||
default once the books hold an audit run; before one it warns), the package is refused (`audit_required`) unless an audit with
|
||||
scope `cpa_handoff` or `full` stands at the current head with a period
|
||||
attested on it at the bar; a reason overrides the refusal and is journaled
|
||||
as a decision. Under `warn` the package is made and its `warnings` say so.
|
||||
+50
-16
@@ -16,8 +16,8 @@ not a tool, so there is nothing for a recipe to call.
|
||||
| `.rpm` | `cargo-generate-rpm` (table in `crates/ledger-cli/Cargo.toml`) | same paths, marker `rpm`; 36 MB | dnf repository (`repo-indexes.sh dnf`) | unsigned until step 5 (`--signing-key` takes the PGP key then) |
|
||||
| AUR `PKGBUILD` | `cargo packager` makes the tarball (`pacman` format) and a PKGBUILD for it; ours is `deploy/packaging/aur/PKGBUILD`, which repacks the release's tarball (package `wanderbooks-bin`) | same paths, marker `aur`; built with `makepkg` | pacman (the AUR keeps old PKGBUILDs) | n/a (pacman trusts the AUR's checksums) |
|
||||
| AppImage | `cargo packager` (`appimage`); 44 MB | one file the owner puts anywhere and runs; needs no admin password | `wanderbooks update` (`$APPIMAGE` is the file replaced) against the manifest; minisign | minisign by the release key (step 5) |
|
||||
| `.dmg` | `cargo packager` (`dmg`) | (trial) needs a Mac | `wanderbooks update` | Developer ID and notarization (step 5) |
|
||||
| NSIS `.exe` | `cargo packager` (`nsis`) | (trial) needs Windows, and native Windows is unit P11c (D-62) | `wanderbooks update` | the Windows identity (step 5) |
|
||||
| `.dmg` | `cargo packager` (`dmg`), `deploy/packaging/build-macos.sh`; **built in CI, unsigned; the trial on a Mac is the owner's** | `Wanderbooks.app` dragged to Applications; no admin password (the trial will say) | `wanderbooks update` (it replaces the program inside the `.app`: see the note below) | **unsigned**; Developer ID and notarization are step 5 |
|
||||
| NSIS `.exe` | `cargo packager` (`nsis`) | **not built**: native Windows is unit P11c (D-62) | `wanderbooks update` | the Windows identity (step 5) |
|
||||
|
||||
`just package-linux` builds the first four (`deploy/packaging/build-linux.sh`,
|
||||
about a minute after the release build, itself 8 to 12 minutes on a
|
||||
@@ -64,21 +64,18 @@ What the trial found, each of which is built around now:
|
||||
manifest's `signature` is that `.sig` as written, and `update` reads the
|
||||
public key in either form. Checked end to end by hand (below), and by
|
||||
`a_public_key_is_read_in_every_form_minisign_and_the_packager_write_it`.
|
||||
7. **The AppImage updates itself but the books it holds do not restart
|
||||
onto the new image.** `update` replaces `$APPIMAGE`, and an AppImage run
|
||||
from a shell (`Wanderbooks.AppImage update`, or the one that was running)
|
||||
proves the swap and the rollback: after `update`, the AppImage on disk
|
||||
is the new file, the old one is kept, and `rollback` restores it. But the
|
||||
host that is open runs from the old image's mount
|
||||
7. **The AppImage updates itself, and (since step 4) the books it holds
|
||||
restart onto the new image.** `update` replaces `$APPIMAGE`. The host
|
||||
that is open used to run from the old image's mount
|
||||
(`/tmp/.mount_*/usr/bin/wanderbooks`), a path that does not change, so
|
||||
`host.rs` never sees a replaced file and the open books keep the old
|
||||
version until they are closed and opened again (`wanderbooks close`).
|
||||
Fixing it means the host learning the image's path (its environment is
|
||||
scrubbed on purpose; `APPIMAGE` would join the allow-list of `start.rs`,
|
||||
or the path would come from the mount table) and starting its successor
|
||||
from it: a decision about what the host may trust, left to the EM (the
|
||||
unit's report names it). Package installs and a hand-placed executable
|
||||
restart as designed (`bridge_survives_update`).
|
||||
it never saw a replaced file. The host now reads `APPIMAGE` (it is in
|
||||
the allow-list of `start.rs`'s scrubbed environment, only when it names
|
||||
an absolute file; a path the host reads about itself and never tells an
|
||||
agent), runs and restarts from that file, and tells a replaced program
|
||||
by its inode, size and last write. Tested without FUSE by
|
||||
`a_replaced_appimage_restarts_the_books` (the programs run from one
|
||||
copy while `APPIMAGE` names another, as a mount does); not run against
|
||||
a real image mount in this wave.
|
||||
8. **Onboarding with no terminal does not exist yet.** The unit's Done-when
|
||||
(1) says an AppImage "opens, onboards and posts an entry with no
|
||||
terminal". The AppImage opens (a bare launch is the viewer), but the viewer
|
||||
@@ -118,6 +115,43 @@ package manager. Not tested: Ubuntu, openSUSE, a package-manager
|
||||
*downgrade* (the repositories keep every release by construction), and the
|
||||
`.rpm` and `.deb` under their real signatures (step 5).
|
||||
|
||||
## The macOS bundle (step 6, built in CI, unsigned)
|
||||
|
||||
`deploy/packaging/packager-macos.toml` (cargo-packager: the `dmg` format,
|
||||
which makes the `.app` first; the icon is the 512 px PNG, made into an
|
||||
`.icns` by the packager; `minimum-system-version` 11.0), `Info.plist`
|
||||
(merged into the generated one: one viewer at a time, no discrete GPU),
|
||||
`build-macos.sh` (the same shape as the Linux script, in portable shell
|
||||
because macOS has BSD `sed`) and `just package-macos`. The `macos` job of
|
||||
`.github/workflows/release.yml` runs on `macos-latest` (Apple silicon: the
|
||||
manifest key is `macos-aarch64`; an Intel build is not made), uploads the
|
||||
`.dmg` as a run artifact (named `macos-dmg-unsigned`) and, on a tag,
|
||||
attaches it to the release. **It is not signed or notarized** (step 5: the
|
||||
owner's Developer ID and Q-4), so Gatekeeper refuses it on a Mac that
|
||||
downloaded it; the owner opens it with right-click, Open. What was checked
|
||||
here, on Linux: the config parses under cargo-packager 0.11.8's strict
|
||||
schema (unknown keys are errors) and the script's `sed` is the portable
|
||||
subset. **Not checked: that the `.dmg` builds, mounts or runs**: that needs
|
||||
the CI run (Actions, release, Run workflow, this branch) and then a Mac.
|
||||
Open items for the trial on a Mac: whether `wanderbooks update` replacing
|
||||
the program inside a signed `.app` breaks its signature (it will: step 5
|
||||
must decide, probably by updating a whole `.app`); the translocated-app
|
||||
check (`translocated_app_refuses_to_connect`) is not built.
|
||||
|
||||
## Updates staged by the host (step 4)
|
||||
|
||||
The host looks at the feed at most once a day while the books are open
|
||||
(`update_check`), and downloads and verifies a newer release into
|
||||
`<root>/updates/staged/` with a `meta.json` (version, sha256, signature,
|
||||
notes). It never swaps. The next opener that starts a host applies it
|
||||
first; `wanderbooks update` applies it too and reuses the staged copy;
|
||||
`update --check` says "already downloaded". A local release is named by
|
||||
`<root>/updates/feed` (a `file://` address, then the key file). Without a
|
||||
compiled-in release key and without that file, the look does nothing but
|
||||
log why. Not built: the in-app "restart now to update" choice and the
|
||||
viewer's notice, which need an admin-socket intent and a place in the
|
||||
viewer (see the unit's As-built block).
|
||||
|
||||
## What `update` and `rollback` need from a release
|
||||
|
||||
`manifest.json` at `releases/latest/download/` (D-26):
|
||||
|
||||
@@ -223,3 +223,27 @@ backdated entries included: defect 6), `zero_net_accounts`,
|
||||
`drafts_by_state` and the reconciliation split; reports gained
|
||||
`as_of_status` and `strict`; the CPA package is refused while a draft
|
||||
dated in it is open (4.8).
|
||||
|
||||
### P5: the agent surface (2026-10-29)
|
||||
|
||||
No semantic feature moved, so no status changed and the floor stands. P5
|
||||
changes how an agent is told what to do, not how a figure is computed: the
|
||||
result envelope on every tool result (`next_steps`, `guards`, `warnings`,
|
||||
`head`), `books.start` v2, the prompts, the resource templates,
|
||||
`books.journal_history` (the journal in words) and source corrections
|
||||
(`source_revisions`: `convention.source_revised`, `decision.source_revised`,
|
||||
`open_item.source_revised`). One new row, `partial` until a hand-verified
|
||||
golden covers it:
|
||||
|
||||
| Area | Counterpart (a cross-check, not the spec) | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| Audit trail in words (`books.journal_history`, the `describe` sentences) | QBO "Audit log", Xero "History and notes" | partial | tests `journal_history_traces_what_the_books_did`, `every_event_type_has_a_sentence`; the viewer keeps its own sentences (the shared describer is the server's) |
|
||||
### P8: audit and attestations (2026-10-29)
|
||||
|
||||
Three new rows, none above changed status, and the floor stands.
|
||||
|
||||
| Area | Counterpart (a cross-check, not the spec) | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| Audit run: one programme of checks over the statements, the control accounts, closes, schedules, assets, evidence, drafts, tax categories, 1099 decisions, settings and the store's integrity (`books.audit`, `books.workpaper`) | An auditor's workpaper; QBO "Audit log"/"Review" and Xero's "Reports review" are a cross-check only | partial | rules.md B-45; `ledger_core::audit`, `ledger_store::audit`; tests `audit_detects_each_seeded_defect` (every code seeded), `audit_is_deterministic`, `workpaper_pdf_is_deterministic`; the fixture `p8_before` was written by the version before; `partial` until a hand-verified golden covers a workpaper |
|
||||
| Attestations of receipts, entries and periods, weighed against `policy.attestation_bar` when asked (`books.attest_receipt`, `books.review_entry`, `books.attest_period`, `books.attestations`) | An accountant's sign-off on a month | partial | `below_bar_not_counted`, `amount_mismatch_never_counts`, `stale_head_period_attestation_refused`; the method is the client's own claim, and the books say so |
|
||||
| The CPA package waits for a counted audit (`policy.cpa_package_requires_audit`) | A close checklist before handoff | partial | `cpa_package_refuses_without_a_counted_audit_unless_a_reason_is_given`; behind the feature `audits` |
|
||||
+29
-2
@@ -64,7 +64,16 @@ apply is safe — on startup the applier replays every journal line with
|
||||
`attachment.annotated`, `entry.settles` (P6), `entry.draft_state_set`
|
||||
(a draft building or awaiting its evidence: a note on the draft, no
|
||||
figure) and `entry.draft_discarded` (a draft thrown away, with why; the
|
||||
draft's earlier lines stay in the journal) (P4, feature `drafts_v2`)
|
||||
draft's earlier lines stay in the journal) (P4, feature `drafts_v2`),
|
||||
and `convention.source_revised {id, source, reason}`,
|
||||
`decision.source_revised {id, source: {decided_by: [..], decided_on,
|
||||
citation?}, reason}` and `open_item.source_revised {id, source, reason}`
|
||||
(P5, feature `source_revisions`: who decided a convention, a decision or
|
||||
an open item, when and on what, corrected without touching its words;
|
||||
replay sets the source and nothing else, and the projection updates the
|
||||
row's existing source columns, so no schema changed)
|
||||
`audit.run`, `receipt.attested`, `entry.reviewed` and `period.attested`
|
||||
(P8, feature `audits`: claims about the books, no figure)
|
||||
(plus the A/R, A/P, asset,
|
||||
period, payroll, and class events listed on `JournalLine`).
|
||||
- `payload`: the full domain facts (account fields, entry with lines, blob
|
||||
@@ -167,6 +176,20 @@ a backup is then proven where it lands, by a drill (docs/mcp-interface.md,
|
||||
read off a file (the closed field list; a later annotation replaces the
|
||||
earlier record), and `entry.settles {entry_id, settles[]}` says which
|
||||
entries a payment pays back (the only source of "reimbursed").
|
||||
- **Audits and attestations are records, not figures** (P8, rules.md B-45,
|
||||
feature `audits`): `audit.run {id, through, through_seq, scope,
|
||||
program_version, tb_hash, findings_sha256, workpaper_attachment?,
|
||||
workpaper_pdf?}` says a programme of checks was run through a day at the
|
||||
books' figure head `through_seq` (the last line that bears on a figure;
|
||||
audits, attestations and filings of evidence do not move it), and which
|
||||
two filed documents (category `workpaper`) hold its findings;
|
||||
`receipt.attested {attachment_id, entry_id, by, at, method,
|
||||
amount_checked, date_checked, disagrees, note?}`, `entry.reviewed
|
||||
{entry_id, by, at, method, note?}` and `period.attested {period_id, by,
|
||||
at, method, audit_run_id}` are a named client's claim that it checked.
|
||||
`method` ({model?, reasoning_effort?, adversarial?, verifier_agents?}) is
|
||||
that client's own word. Whether a claim counts is never journaled: it is
|
||||
derived against `policy.attestation_bar` when asked.
|
||||
- The store is policy-free: it records attachments faithfully. Whether a
|
||||
posting *requires* a receipt is decided by the MCP layer's receipt policy
|
||||
(`docs/mcp-interface.md`), which reads the `attachments` table.
|
||||
@@ -192,7 +215,11 @@ Suspected corruption is fixed by rebuild, never by forensics.
|
||||
as above; `extractions(attachment_id, extractor, version, text_sha256,
|
||||
pages, status, seq)` and `annotations(attachment_id, receipt_number,
|
||||
payee, amount, date, instrument, category, source, reimbursable_to,
|
||||
note, seq)` (P6; amounts TEXT, never REAL); `events(seq, type, payload,
|
||||
note, seq)` (P6; amounts TEXT, never REAL); `audits(id, through,
|
||||
through_seq, scope, program_version, tb_hash, findings_sha256,
|
||||
workpaper_attachment, workpaper_pdf, seq)` and `attestations(seq, kind,
|
||||
attachment_id, entry_id, period_id, audit_run_id, by_who, at_ts, method,
|
||||
amount_checked, date_checked, disagrees, note)` (P8, schema 20); `events(seq, type, payload,
|
||||
created_at)` — the applier's output,
|
||||
a queryable mirror of the journal stream and the feed the future GUI
|
||||
tails. (Poll `WHERE seq > ?`; later SSE over the HTTP transport. This is
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# The re-audit, through the MCP alone
|
||||
|
||||
The owner's acceptance of the whole programme (plan/served.md, P10). An
|
||||
agent audits a company's books using only the books tools, the books' own
|
||||
filed documents and the viewer's Export menu. Nothing here reads the data
|
||||
root, a database file or the journal file. The engineering mirror of this
|
||||
runbook is `crates/ledger-cli/tests/defects.rs` (every tool named below
|
||||
exists; every defect has a named regression test); the run itself is the
|
||||
owner's, on the owner's books.
|
||||
|
||||
Money is a decimal string everywhere. Every tool named is a tool in
|
||||
`books.start`'s list; if one is missing, stop: the runbook is stale, not
|
||||
the books.
|
||||
|
||||
## Before you start
|
||||
|
||||
- Two roles. The **auditor** runs `books.audit`, reads, and proposes fixes.
|
||||
The **verifiers** (at least two agents, not the auditor, not whoever made
|
||||
the entry) attest. The human confirms every correction.
|
||||
- The bar is `policy.attestation_bar` (default `effort=high verifiers=2
|
||||
adversarial=true`, D-9). Read it with `books.settings`. A verifier's
|
||||
`method` is its own claim, said truthfully: `{ model, reasoning_effort,
|
||||
adversarial, verifier_agents }`. A reading that differs from the entry is
|
||||
recorded with `disagrees: true` and never counts; never bend a reading to
|
||||
fit.
|
||||
- Context discipline. A receipt is read as text first
|
||||
(`books.get_receipt`, the default `meta` view is under 8 KiB, `view:
|
||||
"text"` gives the whole text), as one page picture (`view: "page_image"`,
|
||||
`page`, `max_px`) only when the text is empty or looks wrong, and never as
|
||||
`bytes`. Record each verifier's own token count for receipts; the exit
|
||||
bar is under 100K tokens per verifier.
|
||||
- Start with `books.start` (rules, conventions, open items, its `next_steps`)
|
||||
and `books.handbook` section `attestations-and-audit`. The `audit` prompt
|
||||
(`scope`: `month`, `annual`, `cpa_handoff` or `full`) is the same
|
||||
procedure for a client that offers prompts.
|
||||
- Audits and attestations need the books' `audits` feature (the owner's
|
||||
format upgrade). Without it the tools refuse with `format_upgrade_pending`:
|
||||
do the upgrade first, as its own step, with the owner.
|
||||
|
||||
## The two cases
|
||||
|
||||
- **T1, desktop only.** Claude Code and Codex on the owner's machine, through
|
||||
`wanderbooks connect`. Separation between the agent and the data root is
|
||||
advisory. The evidence that it held is the access log
|
||||
(`books.access_log`, `books:admin`) and the agents' transcripts, which never
|
||||
name the data root or a path into it.
|
||||
- **T2, a self-host machine running** (D-30, D-31; `deploy/README.md`). The
|
||||
same steps, plus claude.ai through the tunnel once P9b exists. Separation
|
||||
is enforced by the machine's account split. Until P9b, T2 is T1 with the
|
||||
server on its own machine.
|
||||
|
||||
The steps are identical; only the evidence in step 8 differs.
|
||||
|
||||
## Step 1. The first audit and its fixes
|
||||
|
||||
1. `books.audit { scope: "full", through: <the last month end> }` without
|
||||
`confirmed`: it lists the checks and changes nothing. Show the human.
|
||||
Then run it with `confirmed: true` and the `preview` digest. Do the same
|
||||
for `{ scope: "cpa_handoff", through: <the fiscal year end> }`.
|
||||
2. `books.workpaper { id }` for each run: the findings and the PDF as filed.
|
||||
3. Fix every finding through journaled corrections, never an edit:
|
||||
- a wrong posted entry: `books.reverse_entry { entry_id, date,
|
||||
description }`, previewed, then `confirmed: true`; then the right entry
|
||||
through `books.draft_entry`, `books.add_line`, `books.post_entry`
|
||||
(preview, then `confirmed`);
|
||||
- a reversal with no receipt of its own (an old one, `legacy_reversal_exempt`):
|
||||
its evidence is its original's (D-12, D-47). Link the original's receipt
|
||||
to the reversal with `books.link_attachment { attachment_id, subject }`;
|
||||
`books.list_receipts { entry_id }` finds the receipt;
|
||||
- a duplicate file: `books.consolidate_duplicates` (preview, then
|
||||
`confirmed`); `books.find_attachment` to look one up by hash or number;
|
||||
- a receipt's facts: `books.annotate_attachment`;
|
||||
- an open draft: post it, or `books.discard_draft { reason }`;
|
||||
- an undecided 1099 vendor: `books.set_vendor_1099`;
|
||||
- a missing tax category: `books.set_tax_category`;
|
||||
- a judgment the books should remember: `books.record_decision`.
|
||||
Nothing is fixed without the human's confirmation.
|
||||
4. Audit again at the new head. An audit is idempotent on the same books
|
||||
(`already_run`), so a new run means the books changed. Repeat until the
|
||||
`full` and `cpa_handoff` runs have no error findings; keep the final two
|
||||
run ids. Each journaled fix can be traced with `books.journal_history
|
||||
{ subject }`.
|
||||
|
||||
## Step 2. Every receipt attested
|
||||
|
||||
For every receipt on a posted entry (`books.list_receipts`, entry by entry,
|
||||
or `books.search_entries` then `books.get_entry`):
|
||||
|
||||
1. Verifier A reads `books.get_receipt` (text; a page image only if needed)
|
||||
and calls `books.attest_receipt { attachment_id, entry_id, amount, date,
|
||||
method, note? }` with the total and the date as read off the receipt.
|
||||
2. Verifier B does the same independently, without seeing A's reading.
|
||||
3. `books.attestations { kind: "receipt" }` shows each claim with
|
||||
`counted`. A claim below the bar, or with `disagrees: true`, does not
|
||||
count; the disagreement is a finding: reopen step 1 for that entry.
|
||||
|
||||
## Step 3. Every entry reviewed, every period attested
|
||||
|
||||
1. `books.review_entry { entry_id, method }` for every posted entry, by a
|
||||
verifier that did not make it.
|
||||
2. `books.attest_period { period, method }` for each month since the
|
||||
company's formation (`books.list_periods`), each against a standing
|
||||
audit at the current head (`scope: "month"` or `"full"`), then once for
|
||||
the fiscal year against the `cpa_handoff` run. A period attestation is
|
||||
refused when the books changed since the audit: audit again, then attest.
|
||||
3. `books.attestations` with no filter: every row has `counted: true` and
|
||||
none has `disagrees`.
|
||||
|
||||
## Step 4. The defect regressions
|
||||
|
||||
The 2026-10-16 audit found 20 defects; P0 found two more (21, 22) and P3
|
||||
two (23, 24). Each has a named regression test in the workspace;
|
||||
`crates/ledger-cli/tests/defects.rs` holds the list `(defect, test, file)`
|
||||
and proves each name exists. Show them green, from a checkout of the build
|
||||
under audit:
|
||||
|
||||
```
|
||||
cargo test -p ledger-cli --test defects
|
||||
```
|
||||
|
||||
then each test named in that list, by name, for example
|
||||
`cargo test -p ledger-core --test spec_cash_flow
|
||||
defect1_two_member_llc_first_year_regression`. Two defects have no code
|
||||
fix to test: defect 2 (reversals without their own evidence) is closed by
|
||||
D-12 and D-47 (its test shows the rule now) and defect 20 (the migration
|
||||
scripts) was retired at the cutover (no tool exists; the test checks none
|
||||
came back). Record the test run's output in the sign-off decision (step 7).
|
||||
|
||||
## Step 5. The Sheet comparison
|
||||
|
||||
The old Google Sheet and the books were frozen as of 2026-10-11 and
|
||||
compared a final time before the cutover (P2, `verify-sheet`); that run is
|
||||
filed as a document. There is no comparison tool and this
|
||||
runbook builds none.
|
||||
|
||||
1. `books.find_attachment { text: "verify-sheet" }` finds the filed run
|
||||
(its extracted text names the tool); `books.get_receipt { attachment_id }`
|
||||
reads it (text view; its `links` and `sha256` identify it). Cite it by
|
||||
attachment id and sha256. If it is not found, stop: the comparison was
|
||||
not filed at the cutover and the owner decides what stands in for it.
|
||||
2. `books.record_decision { subject: "Sheet comparison as of 2026-10-11",
|
||||
decision_md, decided_by: [<the owner>], decided_on, evidence_attachment:
|
||||
<that workpaper's id> }` stating that the frozen Sheet and the books
|
||||
agreed then, and nothing newer is claimed.
|
||||
|
||||
## Step 6. The CPA package from both doors
|
||||
|
||||
1. The agent's door: `books.export_cpa_package { as_of: <the year end>,
|
||||
include_tins: true }` (TINs are included by default, D-29). Without a
|
||||
counted audit it is refused (`audit_required`); never use
|
||||
`without_audit_reason` for the re-audit, the point is that it is not
|
||||
needed. Save the zip with the download command in its `file`.
|
||||
2. The viewer's door: `wanderbooks gui <book>`, the Export menu, the CPA
|
||||
package as of the same day. It lands in the Downloads folder.
|
||||
3. Compare the two zips file by file: byte-identical except for timestamps
|
||||
(the README's generated-at line and the zip entries' times). A difference
|
||||
in any figure is a defect: stop and report it.
|
||||
|
||||
## Step 7. Verify, back up, sign off
|
||||
|
||||
1. `books.verify { blobs: true }`: `status: "ok"` (not `healed`: a healed
|
||||
result means a torn tail was set aside; note it and look at
|
||||
`books.journal_history`'s `tail_repairs`).
|
||||
2. `books.backup`: every target not `stale`, `last_error` empty, and
|
||||
`last_drill_at` set with no `drill_error`. If not, `books.back_up_now`
|
||||
(an admin action) and read it again. The drill is the restore test; a
|
||||
backup that was never restored is not a backup.
|
||||
3. The owner's sign-off: `books.record_decision { subject: "Re-audit
|
||||
sign-off", decision_md, decided_by: [<the owner>], decided_on }` naming
|
||||
the final `cpa_handoff` and `full` run ids, the journal head
|
||||
(`books.verify`'s `head`), the defects run, and the CPA package's sha256.
|
||||
|
||||
## Step 8. The exit evidence
|
||||
|
||||
The re-audit passes only when all of these are true. The owner checks them;
|
||||
the agent presents the proof for each.
|
||||
|
||||
| Exit bar | Proof |
|
||||
|---|---|
|
||||
| Zero failing workpaper checks | the final `full` and `cpa_handoff` workpapers (`books.workpaper`) |
|
||||
| Cash flow difference 0.00 | `books.cash_flow` for the year; no `cash_flow_unfooted` finding |
|
||||
| Every receipt and posted entry has a counted attestation, none disagrees | `books.attestations` (step 3.3) |
|
||||
| Every action under the right principal | `books.access_log`: the auditor's token and each verifier's token appear on their own calls; no action by an admin token that an agent token did |
|
||||
| No transcript names the data root | grep each agent's transcript for the root path before sign-off (T1); T2: the tenant root is not readable by the agent's account |
|
||||
| No result carried a full TIN to a client that should not see one | `books.access_log` shows `include_tins` only on `books.export_cpa_package` and `books.get_w9`/`books.vendor_1099` calls by the owner's agent; the agents' exports may carry them (D-29), nothing else |
|
||||
| Receipt reading stayed under 100K tokens per verifier | each verifier's own token count for its receipt calls (the harness's count, written into the attestation `note`) |
|
||||
| The sign-off is journaled | `books.decisions` shows it |
|
||||
|
||||
When all are true the books are re-audited through the MCP alone.
|
||||
+100
-2
@@ -13,7 +13,7 @@ Foundation").
|
||||
|
||||
Spec blocks in the code cite these as `SOURCE: rules.md B-n`. Every rule
|
||||
also has a bookspec identifier, `**bookspec:** BS-<area>-<n>`: the table
|
||||
from B-1 to B-45 is `bookspec/spec/rules-index.md` (P13, D-34).
|
||||
from B-1 to B-45 is `spec/rules-index.md` in the bookspec repository, https://git.swaits.com/swaits/bookspec (P13, D-34).
|
||||
|
||||
---
|
||||
|
||||
@@ -1761,7 +1761,15 @@ saying what it duplicates, B-44). P4's is `drafts_v2` (an
|
||||
`entry.draft_state_set` or `entry.draft_discarded`, both new event types:
|
||||
an older version would refuse them, or, reading around them, keep a
|
||||
discarded draft that holds a period open and call a waiting draft one
|
||||
being built, B-43). A command asks for
|
||||
being built, B-43). P5's is `source_revisions` (a
|
||||
`convention.source_revised`, `decision.source_revised` or
|
||||
`open_item.source_revised`, new event types: an older version would
|
||||
refuse them, or show the old source as the record's authority). A command asks for
|
||||
being built, B-43). P8's is `audits` (an `audit.run`, `receipt.attested`,
|
||||
`entry.reviewed` or `period.attested`, all new event types, and the
|
||||
settings `policy.attestation_bar` and `policy.cpa_package_requires_audit`:
|
||||
an older version would refuse the types and the keys, or drop the claims a
|
||||
handoff rests on, B-45). A command asks for
|
||||
the feature it needs (`Store::ensure_feature`) before its first journaled
|
||||
line, and only when the line it is about to write uses it, so a write
|
||||
refused for waiting on the upgrade journals nothing, never an entry
|
||||
@@ -3582,3 +3590,93 @@ withdraw_attachment_duplicate}` and `Store::attach`'s dedup; feature
|
||||
`page_image_is_a_png_of_the_page`,
|
||||
`consolidation_never_removes_an_entrys_only_evidence`); fuzz targets
|
||||
`fuzz_extract_pdf` and `fuzz_extract_xlsx`.
|
||||
|
||||
## B-45 An audit is deterministic over the books; an attestation is a claim by a named client
|
||||
|
||||
**bookspec:** `BS-ledger-13`
|
||||
|
||||
**Status:** CONVENTION (the SOURCE block below; Pub. 583 and Rev. Proc. 98-25
|
||||
for the audit trail); CHOICE D-9 (owner-adopted recommendation,
|
||||
2026-10-16: the attestation bar, and the CPA package waits for a counted
|
||||
audit unless given a reason).
|
||||
|
||||
```text
|
||||
GOAL: the books can be audited by a programme that gives the same
|
||||
findings for the same books, and a claim that someone checked rests
|
||||
on the books as they were when the audit ran, says how it was made,
|
||||
and is weighed against the owner's bar when asked.
|
||||
SOURCE: IRS Pub. 583 (records that support the entries); Rev. Proc. 98-25
|
||||
§5.01 (the relationship between the records and the books must be
|
||||
demonstrable: an audit trail); CHOICE D-9 (the bar: reasoning
|
||||
effort at least high; for a year, adversarial verification by at
|
||||
least two independent agents; no model allow-list; the CPA package
|
||||
refuses without a counted audit unless given a reason); CHOICE
|
||||
D-53 (an audit is a run, not a job); CONVENTION (the figure head,
|
||||
below; a client's method is its own claim).
|
||||
INVARIANTS:
|
||||
1. An audit is a pure function of the journal's figures: its findings
|
||||
(`ledger_core::audit::statements`, the store's integrity checks) are
|
||||
a list in a fixed order, and the same books, day, scope and
|
||||
programme give the same findings, the same trial balance hash and
|
||||
the same workpaper bytes. A check that cannot be made is itself a
|
||||
finding, never a silent pass.
|
||||
2. The figure head is the seq of the last journal line that bears on a
|
||||
figure. Audit runs, attestations, filings of evidence (receipts,
|
||||
documents, statements, reconciliations, extraction, annotations,
|
||||
links), decisions and open items, and the format declaration do not
|
||||
move it; an entry, a draft, a document, a close, a schedule, an
|
||||
asset, a settings change or a role change does. A run is idempotent
|
||||
on (through, head, scope, programme): asking again returns it and
|
||||
journals nothing.
|
||||
3. A period is attested only on a run at the current head whose day is
|
||||
on or after the period's end.
|
||||
4. An attestation is a claim by the token that made it (`by`); its
|
||||
method (model, reasoning effort, adversarial, verifier agents) is
|
||||
the client's own, recorded and never verified. A receipt attestation
|
||||
whose amount or date differs from the entry's is recorded with
|
||||
`disagrees` and never counts.
|
||||
5. `counted` is derived against `policy.attestation_bar` as it stands
|
||||
when asked and never stored: a bar change changes what counts, past
|
||||
claims too. Every attestation needs the reasoning effort; one on a
|
||||
year-level run (annual, cpa_handoff, full) needs the adversarial
|
||||
check and the verifier count too.
|
||||
6. The CPA package, on books that declared `audits` and under
|
||||
`policy.cpa_package_requires_audit = refuse` (unset: refuse once the
|
||||
books hold an audit run, warn before), needs a run of scope
|
||||
cpa_handoff or full at the current head with a period attested on it
|
||||
that counts; otherwise it is refused, unless a reason is given,
|
||||
which is journaled as a decision.
|
||||
7. Nothing here moves a figure. Every refusal leaves the journal
|
||||
unchanged; every write waits for the feature `audits`.
|
||||
SCENARIOS:
|
||||
1. The same books audited twice through one day: one run, one
|
||||
workpaper; the second call files and journals nothing.
|
||||
2. An entry posted after the audit: the period cannot be attested until
|
||||
the audit is run again.
|
||||
3. A receipt read as 41.00 against a 40.00 entry: recorded, `disagrees`,
|
||||
not counted, however hard the checker says it thought.
|
||||
4. The bar lowered afterwards: earlier attestations that now meet it
|
||||
count.
|
||||
5. A CPA package with no counted audit: refused (`audit_required`);
|
||||
with a reason, made, and the reason is a decision in the journal.
|
||||
6. Books that never declared `audits`: the package is as it was; the
|
||||
audit run and the attestations are refused with
|
||||
`format_upgrade_pending`.
|
||||
```
|
||||
|
||||
**Enforced:** `ledger_core::audit` (`statements`, `Finding`, `CODES`,
|
||||
`AttestationBar::counts`, `Scope`, `Method`), `ledger_store::audit`
|
||||
(`Store::figure_head`, `integrity_findings`, `record_audit_run`,
|
||||
`attest_receipt`, `review_entry`, `attest_period`, `run_for_period`,
|
||||
`attestations`, `PROGRAM_VERSION`), `ledger_import::workpaper_pdf`,
|
||||
ledger-mcp `tools/audit.rs` and the gate in `books.export_cpa_package`,
|
||||
journal feature `audits`.
|
||||
**Checked:** `ledger-core` `tests/audit.rs`
|
||||
(`audit_detects_each_seeded_defect`, `below_bar_not_counted`,
|
||||
`amount_mismatch_never_counts`); `ledger-store` `tests/audit.rs`
|
||||
(`audit_writes_wait_for_audits`, `a_run_is_idempotent_on_the_figure_head`,
|
||||
`stale_head_period_attestation_refused`, `integrity_detects_each_seeded_defect`,
|
||||
`books_the_version_before_p8_wrote_wait_for_the_upgrade`); `ledger-import`
|
||||
`workpaper_pdf_is_deterministic`; `ledger-cli` `tests/audit.rs`
|
||||
(`audit_is_deterministic`, `cpa_package_refuses_without_a_counted_audit_unless_a_reason_is_given`,
|
||||
`audits_wait_for_the_upgrade`).
|
||||
+242
@@ -3769,6 +3769,25 @@ it (the same program with a tail, so its hash differs). All in
|
||||
file now on disk (not a deleted one). The host's own restart laws stay
|
||||
`open_while_held.rs`'s (`a_restart_is_not_a_close`).
|
||||
|
||||
Step 3's gap and step 4 (same file):
|
||||
|
||||
- `a_replaced_appimage_restarts_the_books`: the programs run from one copy
|
||||
(a mount's) while `APPIMAGE` names another (the image); `update`
|
||||
replaces only the image; the host restarts onto it (its `/proc/PID/exe`
|
||||
is the image), the mount's copy is untouched, the session writes on.
|
||||
- `the_host_stages_a_daily_update_and_the_next_start_applies_it`: with a
|
||||
local feed named in `updates/feed`, the host stages 9.0.0 by itself;
|
||||
the program, the host and the session are unchanged meanwhile; after
|
||||
`close`, the next opener applies it (the old program is kept, the staged
|
||||
folder gone) and the books are as they were.
|
||||
- `a_daily_update_that_is_not_ours_is_not_staged`: a signature by another
|
||||
key, then a wrong sha256, each looked at and neither staged.
|
||||
- `update_installs_the_copy_the_host_staged_without_downloading_again`: the
|
||||
release's file is deleted; `update` installs from the staged copy.
|
||||
- `a_staged_update_that_was_tampered_with_is_dropped_not_applied`: a staged
|
||||
file edited together with its recorded checksum fails the signature; the
|
||||
next start drops it and runs the old program.
|
||||
|
||||
Unit tests in `update.rs`: `versions_are_three_numbers_and_order_by_number`,
|
||||
`a_package_marker_beside_the_program_makes_it_package_managed`,
|
||||
`a_manifest_address_is_resolved_beside_the_manifest`,
|
||||
@@ -3803,3 +3822,226 @@ covers the feature one; a case with a higher format number is a gap to
|
||||
add), `swap_in`'s temporary-then-rename (`update_is_atomic`), `healthy`
|
||||
(`failed_update_rolls_back`), `install_kind` (the marker test), and
|
||||
`parse_version`'s rejections.
|
||||
|
||||
## P5: the agent surface (2026-10-29)
|
||||
|
||||
Every tool result carries the envelope (`next_steps`, `guards`, `warnings`,
|
||||
`head`), `books.start` is v2, the prompts and resource templates are
|
||||
served, `books.journal_history` reads the journal in words, and a
|
||||
convention, decision or open item can have its source corrected. Journal
|
||||
feature `source_revisions`; events `convention.source_revised`,
|
||||
`decision.source_revised`, `open_item.source_revised`; no schema change
|
||||
(the projection updates the rows' existing source columns).
|
||||
|
||||
- **`ledger-mcp` `src/server_tests.rs`:**
|
||||
`every_mutating_result_has_head_and_next_steps` (every tool of the
|
||||
router is given the envelope; the non-read-only ones have `head` equal to
|
||||
the journal's and a step each with `why` and `required`; the text block
|
||||
says what the structure says),
|
||||
`the_envelope_keeps_what_a_tool_said_and_carries_a_refusal_guard`,
|
||||
`books_start_is_always_loaded`, `tool_descriptions_fit_700_characters`
|
||||
(the allowance list is gone: every description is at most 700
|
||||
characters). `guards.rs`: `every_guard_names_an_existing_tool`,
|
||||
`a_refusal_becomes_its_guard`. `next_steps.rs`:
|
||||
`a_preview_gets_its_confirm_step`, `every_step_a_tool_names_exists`.
|
||||
`tools/prompts.rs`: `the_prompts_name_only_tools_that_exist`,
|
||||
`an_argument_is_one_token_of_the_right_shape`.
|
||||
- **`ledger-store`:** `tests/source_revisions.rs`:
|
||||
`a_source_only_revision_is_journaled` (the three events, words and
|
||||
revision unchanged, live equals replay equals a rebuilt database),
|
||||
`a_source_revision_must_change_only_the_source` (other words, the same
|
||||
source, no reason, unknown record: refused, journal unchanged),
|
||||
`source_revisions_wait_for_the_upgrade` (refused before the go-ahead with
|
||||
`format_upgrade_pending`, journaling nothing). `format.rs`:
|
||||
`source_revisions_is_its_own_feature`. `describe.rs`:
|
||||
`every_event_type_has_a_sentence`, `settings_show_their_keys_never_their_values`,
|
||||
`a_full_tin_in_a_description_is_masked`,
|
||||
`history_filters_in_order_and_says_when_more_remain`.
|
||||
- **8.0.6:** `books_written_under_older_rules_still_open` hand-writes the
|
||||
three events; the fixture `old-books/p5_before` (writer
|
||||
`old-books-writer/src/bin/p5_before.rs`, run against b97234ac, the
|
||||
revision before P5: a convention revised once, two decisions, two open
|
||||
items, a draft) opens to that version's figures, so no figure moved;
|
||||
`books_p2_served_cut_over_whole` refuses `books.raise_item` with `revises`
|
||||
before consent and takes it after.
|
||||
- **`ledger-cli` `tests/agent_surface.rs`:**
|
||||
`journal_history_traces_what_the_books_did`,
|
||||
`a_source_only_revision_over_the_wire`, `the_prompts_list_and_render`
|
||||
(each prompt rendered equals its file with the blanks filled; an
|
||||
argument that is not one token of the right shape, a missing or extra
|
||||
one, and a struck prompt are refused),
|
||||
`the_resource_templates_are_advertised_and_read`,
|
||||
`books_start_v2_is_whole_and_fits` (at most 8 KiB on the P2 fixture; the
|
||||
agent's self-report in the access log only),
|
||||
`a_new_book_is_told_to_onboard`,
|
||||
`a_mutating_call_answers_with_the_head_and_a_refusal_names_its_remedy`.
|
||||
- **`ledger-cli` `tests/agent_evals.rs`** (transcripts in `tests/agent_evals/`):
|
||||
`agent_evals_follow_only_next_steps` plays six scripted transcripts (no
|
||||
model): onboarding to a first post, a receipt-backed post, a draft that
|
||||
waits for its receipt and posts when it arrives, a reversal, a
|
||||
reconciliation, a month-end close. After `books.start` a transcript may
|
||||
call only a tool (or fetch only a prompt) the previous result's
|
||||
`next_steps` named, with the arguments the step gave and the ones the
|
||||
agent decides. `setup` calls (the chart, the books an agent finds) are
|
||||
not the agent's moves.
|
||||
- **The sweep:** the `party_id`, `document_id` and `cash_account` aliases of
|
||||
`books.create_invoice`, `books.create_bill`, `books.receive_payment` and
|
||||
`books.pay_bill` are gone (S4, S6 said "until P5"); the over-the-wire
|
||||
tests that used them name `customer`/`vendor`, `invoice`/`bill` and
|
||||
`deposit_to`/`paid_from`.
|
||||
|
||||
**For P12 (mutation targets):** `next_steps::apply`'s three branches (an
|
||||
error, a result without an object, the normal path) and the `mutating`
|
||||
test that gives `head`; `tool_steps`'s confirm branch (`!confirmed` and a
|
||||
`preview`), the `receipt_missing` pair before it, and each tool's arm;
|
||||
`attention_steps`'s subject match (entry against invoice) and its
|
||||
`Problem` severity for `required`; `subject_of`'s precedence;
|
||||
`guards::from_refusal`'s structured and worded branches and
|
||||
`Guard::remedy_tool`'s table; `prompts::checked`'s token test and each
|
||||
`Check` shape (month length and calendar, year length, the word list);
|
||||
`prompt_get`'s extra-argument and missing-argument refusals;
|
||||
`describe::subjects`'s per-prefix id choice, `describe`'s `settings.set`
|
||||
key list and TIN mask, `history`'s `>` on `after_seq`, its `limit` cut and
|
||||
`more`; `check_convention_source`'s three equalities (title/body/structure,
|
||||
source, kind), `check_decision_source` and `check_open_item_source`'s
|
||||
equal-source refusals and the empty-reason refusals; the three
|
||||
`apply_*_source` arms (a mutant that also bumps `revision` or `seq` shows
|
||||
in `a_source_only_revision_is_journaled`); `books.start`'s fit order and
|
||||
`reconciliation` split (`reconciled_through.is_some()`).
|
||||
## P8: audit and attestations (2026-10-29)
|
||||
|
||||
An audit run is a deterministic programme of checks over the books through
|
||||
a day, filed as a JSON and a PDF workpaper; attestations are claims by a
|
||||
named client that it checked a receipt, an entry or a period, weighed
|
||||
against `policy.attestation_bar` when asked. Journal feature `audits`;
|
||||
events `audit.run`, `receipt.attested`, `entry.reviewed`, `period.attested`
|
||||
(no figure moves); schema 20 (taken as the next after the last, renumbered
|
||||
at merge): the `audits` and `attestations` tables; settings
|
||||
`policy.attestation_bar` (a new `Kind::AttestationBar`) and
|
||||
`policy.cpa_package_requires_audit`. No kernel change, no Kani harness, no
|
||||
Lean theorem: the audit calls kernel functions and adds none, and the laws
|
||||
it checks are already theorems.
|
||||
|
||||
- **`ledger-core` `tests/audit.rs`:** `audit_detects_each_seeded_defect`
|
||||
(the code table `audit::CODES` has a row per check and the test iterates
|
||||
it: a seeding fixture for each, exactly that code found, its severity the
|
||||
row's; the checks a correct engine never lets happen are seeded through
|
||||
the predicate each check is built on, `check_trial_balance`,
|
||||
`check_equation`, `check_articulation`, `check_cash_flow`,
|
||||
`check_control`, `check_split`, `check_register`),
|
||||
`a_whole_book_gives_no_failure_or_warning`,
|
||||
`findings_come_in_one_order`, `below_bar_not_counted` (the bar's effort,
|
||||
the year-level adversarial check and verifier count, a lowered bar, the
|
||||
parse), `amount_mismatch_never_counts`.
|
||||
- **`ledger-store` `tests/audit.rs`:** `audit_writes_wait_for_audits`
|
||||
(R-A: every write refused with `FormatUpgradePending`, nothing
|
||||
journaled, taken after `upgrade_format`, replay equal to live),
|
||||
`a_run_is_idempotent_on_the_figure_head` (a filing and a run do not move
|
||||
the head, an entry does, asking again journals nothing),
|
||||
`amount_mismatch_never_counts` (a wrong amount and a wrong date are
|
||||
recorded as disagreeing; a receipt not on the entry, a draft and an
|
||||
unknown entry are refused), `stale_head_period_attestation_refused`
|
||||
(no audit, an audit that stops short of the period, a standing one, an
|
||||
entry since), `integrity_detects_each_seeded_defect` (every code of
|
||||
`ledger_store::audit::CODES`: a missing and a damaged stored file, a
|
||||
missing extraction text, a duplicate file written by an older version,
|
||||
a damaged access log, a database that differs from a replay, a damaged
|
||||
journal, a stale backup, a failed drill),
|
||||
`books_the_version_before_p8_wrote_wait_for_the_upgrade` (8.0.6 item 2).
|
||||
`tests/store.rs::books_written_under_older_rules_still_open` hand-writes
|
||||
the four events (8.0.6 item 1); `tests/format.rs` and
|
||||
`format.rs::audits_is_its_own_feature` (8.0.6 item 4). The four event
|
||||
names are now this version's, so the tests that need an event type no
|
||||
version has written use `ledger.not_yet_written` instead of
|
||||
`period.attested` (`tests/format.rs`, `ledger-gui/tests/newer_journal.rs`).
|
||||
- **8.0.6:** the fixture `old-books/p8_before` (writer
|
||||
`old-books-writer/src/bin/p8_before.rs`, run against b97234ac) holds
|
||||
receipted entries, a 40.00 purchase with no receipt, a small one, a
|
||||
reversal, an invoice part paid, an open draft and a closed month;
|
||||
`books_an_earlier_version_wrote_open_to_its_figures` opens it to that
|
||||
version's figures, so no figure moved and no golden changed;
|
||||
`books_p2_served_cut_over_whole` refuses `books.audit` and
|
||||
`books.review_entry` before consent and takes them after.
|
||||
- **`ledger-import` `workpaper.rs`:** `workpaper_pdf_is_deterministic`,
|
||||
`a_different_finding_is_a_different_document`.
|
||||
- **`ledger-cli` `tests/audit.rs`:** `audit_is_deterministic` (preview,
|
||||
run, the same run and findings again with nothing journaled, the
|
||||
workpaper and its PDF hash equal twice, a new entry makes a new audit),
|
||||
`attestations_count_against_the_bar_when_asked` (a disagreeing reading,
|
||||
a medium effort, no claim, a lowered bar read retroactively),
|
||||
`a_period_is_attested_on_a_standing_audit`,
|
||||
`cpa_package_refuses_without_a_counted_audit_unless_a_reason_is_given`
|
||||
(refused, a reason journaled as a decision, an audit alone not enough, a
|
||||
year attested below the whole bar not enough, the whole bar, an entry
|
||||
since, `warn`), `audits_wait_for_the_upgrade` (the fixture over the
|
||||
wire; the package is not held up by an audit it cannot ask for).
|
||||
- **`ledger-cli` `tests/attention_parity.rs`:**
|
||||
`audit_items_are_on_both_lists_once_audits_are_declared`.
|
||||
|
||||
**For P12 (mutation targets):** `audit::statements`' order of checks and
|
||||
`sort` (severity, then code, subject, message); each `check_*` predicate's
|
||||
comparison (`check_trial_balance`'s `balanced`, `check_articulation`'s
|
||||
`!=`, `check_split`'s `> unit`, `check_register`'s two conditions,
|
||||
`check_cash_flow`'s three arms, `check_control`'s `difference`, unattached
|
||||
and `Overpaid` arms); `evidence`'s three arms (Required, the reversal
|
||||
exemption and the policy test that lists it, the plain exemption) and its
|
||||
receipt-set lookup; `closed_net`'s sign for revenue and expense;
|
||||
`AttestationBar::counts` (each of the four conditions, `>=`, the `full`
|
||||
branch, the `disagrees` early return) and `parse`; `Scope::full_bar` and
|
||||
`serves_handoff`; `Store::figure_head` (the `NOT_FIGURES` list: a type
|
||||
wrongly in it lets a stale audit stand, a type wrongly out of it makes an
|
||||
audit stale at its own filing), `find_audit_run`'s four equalities,
|
||||
`record_audit_run`'s idempotence return, `attest_receipt`'s amount rule
|
||||
(the debit total or any one line) and date equality, `run_for_period`'s
|
||||
`through_seq == head` and `through >= period.end`;
|
||||
`integrity_findings`'s eight arms; `backup_findings`' three conditions; the
|
||||
export gate (`cpa_audit_standing`'s four conditions and its feature test,
|
||||
the `refuse`/`warn` split, the reason's trim); `audit_items` in Needs
|
||||
attention (the declared test, the attested set, the last-ended fiscal
|
||||
year, the three year conditions).
|
||||
|
||||
## P10: the re-audit runbook's mirror
|
||||
|
||||
`docs/runbooks/re-audit.md` is the owner's acceptance, run on his books
|
||||
through the MCP alone. What CI can check of it is in
|
||||
`crates/ledger-cli/tests/defects.rs`:
|
||||
|
||||
- `every_defect_reproduction_is_a_named_test`: a table of `(defect, test
|
||||
name, file)` for the 2026-10-16 audit's defects 1 to 19, P0's 21 and 22
|
||||
and P3's 23 and 24 (23 rows); each named function exists in its file, is
|
||||
a `#[test]` (or inside `proptest!`) and is not `#[ignore]`d. Defect 2 is
|
||||
closed by D-12 and D-47, so its row is the rule as it now stands
|
||||
(`a_reversal_needs_no_receipt_of_its_own`).
|
||||
- `defect_20_no_migration_tool_exists`: defect 20 (the migration scripts)
|
||||
was retired at the cutover; no tool of that kind is in the scopes table.
|
||||
- `the_re_audit_runbook_names_only_what_exists`: every `books.<name>` in
|
||||
the runbook is in `crates/ledger-mcp/src/scopes.rs`, the commands it
|
||||
names are dispatched by `main.rs`, and its eight steps are present.
|
||||
|
||||
The mirror proves the tests exist, not that they pass; the runbook's step 4
|
||||
runs them by name. No CI job was added: the test runs in the existing
|
||||
`check` job's `cargo test --workspace`. The run itself (a counted
|
||||
attestation on every receipt and entry, the two CPA packages equal, the
|
||||
sign-off) is the owner's and has no CI equivalent.
|
||||
|
||||
**For P12 (mutation targets):** the `DEFECTS` table's file/name lookup (a
|
||||
`starts_with` on `fn <name>(`; the `#[test]`/`#[ignore]` window of eight
|
||||
lines above it) and the scopes-line scan in `defect_20_*`. A mutant that
|
||||
widens the window or drops the `ignore` check survives only if no ignored
|
||||
or untested name is in the table; the table is the oracle.
|
||||
|
||||
## P13 step 6: the specification in its own repository (2026-10-29)
|
||||
|
||||
The specification (text, examples, checker, runner, Lean, GnuCash adapter)
|
||||
lives at https://git.swaits.com/swaits/bookspec (CC BY 4.0 for `spec/` and `examples/`, MIT OR Apache-2.0 for the
|
||||
code) and is pinned by commit in `crates/ledger-core/Cargo.toml` and
|
||||
`crates/ledger-package/Cargo.toml`. `just check` runs the examples exactly as
|
||||
before: `cargo test --workspace` includes `ledger-core` `tests/bookspec.rs`
|
||||
(`every_bookspec_example_holds`, `the_runner_reports_every_claimed_rule_green`),
|
||||
which reads them through `bookspec_checker::examples()`, and `ledger-package`
|
||||
`tests/package.rs` reads the IIF example the same way. Two checks that need
|
||||
this repository's files live in `tests/bookspec.rs`: `every_vendored_pin_is_verbatim`
|
||||
(each vendored pin in the spec is verbatim in `docs/spec/sources`) and
|
||||
`rules_md_carries_every_id`. The examples named `bookspec/examples/...` in the
|
||||
older sections above are files of that repository. The checker's, the
|
||||
runner's and the derivation tests run in its own CI.
|
||||
Reference in new issue
Block a user