Commit Graph
429 Commits
Author SHA1 Message Date
swaits 3ea008bb54 feat(viewer): the journal says an invoice went out (P4b S5)
R-B item 7: every journal event gets a plain-language sentence, so the
new `invoice.sent` reads "Invoice 7 sent by email" (or "reminder")
instead of showing its event name. A send has no entry to open, so the
line has no route: it is a document event, not a posting.
2026-10-27 13:24:10 -06:00
swaits 458ca19965 test(mcp): exercise books.mark_sent in the every-tool journey (P4b S5)
`inv1_every_tool_result_matches_its_output_schema` fails listing any
tool the happy-path journey never calls successfully; the new send tool
must be in it. The journey records one send on the invoice it already
created (the books never send), so its output schema is exercised on
every run like every other tool's.
2026-10-27 13:24:10 -06:00
swaits b0d4ae7f8e docs(plan): P4b S5 as built (P4b S5)
Records the departures: step 4 (the two attention items) deferred until
U0b (R-F forbids them before it); the Lean step that AGENTS.md's bar
requires over the unit's "no Lean change"; the recipients parameter
renamed for the path-name lint; the note checked for a full TIN; schema
16 taken at merge (S4.3 had not landed); and the handbook section
created before S4.5.
2026-10-27 13:24:10 -06:00
swaits 7c5a2fe3ce docs: sent tracking in the rules, parity and test records (P4b S5)
rules.md B-27 lists `documents_v2`, the one feature S4 to S7 share
(D-52), including `invoice.sent`; receipt-store.md lists the event;
parity.md gains the sent-tracking row (done: implemented, tested, and
golden-covered by the trial balance of the fixture book around a send);
testing.md records the tests and what P12 must mutate, and names the
two attention predicates as deferred with step 4 until U0b lands.
2026-10-27 13:24:10 -06:00
swaits 47c0e4396e verify(lean): Step.markSent, a send moves no figure (P4b S5)
AGENTS.md's formal-methods bar makes every new Ledger command a Lean
Step with a preservation proof, so `Ledger::mark_sent` gets one even
though the unit expected no Lean change (the As-built block records
that). `Documents.lean`'s `Books` gains the sends recorded, and
`Step.markSent` adds one on a document the lines carry. A send touches
no line, so every control balance, every document's sum and what is
unattached are unchanged through any day
(`Step.markSent_changes_no_figure`, no axioms), and `Reachable.tagged`
covers the new step.

`Audit.lean` lists the theorem; rules.md B-38's model now covers the
record side of a document's life as well as its figures.
2026-10-27 13:24:10 -06:00
swaits 75d8c23d9b feat(mcp): books.mark_sent, and the send in the read tools (P4b S5)
`books.mark_sent` records the send the human or a delegate made: the
invoice (`invoice:7` or its id), the day, who sent it (the token's name
unless given), how (email by default, or mail, portal, hand, another
channel), to whom (the bill-to email, else the customer's), whether it
was the original or a reminder (original on the first send, reminder
after), and the channel's own id. It answers with every send on the
invoice, newest last. `books.documents` rows gain `last_sent` and
`books.document` shows the sends, so "did we send it, and when" is one
read away, while a bill (which we receive) has none.

The schema lint refuses a parameter named `to` (a path-name rule), so
the recipients parameter is `recipients`; the journal event and the
result keep the plan's `to`. The tool needs `books:write`: it changes
the record, never a figure.

The handbook gains the invoices-and-bills section (S4.5 had not landed
yet) with the send procedure: the books never send, the PDF goes to the
human, and each send is recorded afterwards.

Over the wire, mark_sent_records_who_and_how: two sends (the second a
reminder with a message id), the list's `last_sent`, the document's
`sends`, and the invoice still owing its total. The P2 cutover refuses
a send before consent and takes it after.
2026-10-27 13:24:10 -06:00
swaits d1fb512e4b feat(arap): record an invoice's sends as invoice.sent (P4b S5)
The books never send invoices (A-4), but they must remember that one
went out: who sent it, how, to whom, when, whether it was the original
or a reminder, and the channel's own reference. That record is a fact
of the document's life, not a figure: a send moves no money, so it is
one journal line (`invoice.sent`) and no entry, and every derived
figure (outstanding, aging, DocState) is exactly as it was.

`Ledger::mark_sent` refuses a send on a void invoice (it should never
have reached the customer), a send dated before the invoice, no
recipients, and any recipient, reference or note that would put a full
TIN or control characters into the journal. Open, partial, paid and
written-off invoices can be sent (a copy for the customer's records, a
reminder).

The event is `documents_v2`'s (D-52): an older version refuses the
type rather than dropping the record. Replay restores it and the
projection keeps it in schema 16's `invoice_sends` table, so live,
replay and the viewer agree. The new old-books fixture `p4_s5_before`
was written by eeee9658 and opens to the same figures; a hand-written
send in `books_written_under_older_rules_still_open` proves an older
journal with the new line still opens.

Tests: mark_sent_waits_for_documents_v2, sends_replay_and_project,
a_send_changes_no_figure, mark_sent_on_void_refused, Op::MarkSent in
status_is_a_function_of_journal_and_date,
an_older_version_stops_at_invoice_sent.
2026-10-27 13:24:10 -06:00
swaits ed637f0e27 docs: D-55, the owner's full party contact block; fold it into documents_v2 2026-10-27 11:04:50 -06:00
swaits 4cad2cfce2 docs: P4b S4.3 landed; the document journal holds the new fields, with two departures recorded 2026-10-27 10:37:31 -06:00
swaits d19fb464b7 docs(plan): S4.3 as built (P4b S4.3)
The store now journals the documents_v2 fields, previews without
journaling, files the issued copy and locks invoicing.first_number once a
number exists. This records the departures: the store's create commands
keep their signatures and delegate to _with variants, the issued-copy call
takes the create's arguments expanded (the store has no params struct),
schema 16 adds the document columns only (the projection has no parties
table), the over-the-wire document cut-over entry waits for S4.4, and a
refusal leaves the journal unchanged while a storage failure between the
invoice's lines and its attachment's is not rolled back.
2026-10-27 10:37:03 -06:00
swaits 0e5aebda99 feat(store): journal the document fields and file the issued PDF (P4b S4.3)
S4.1 gave documents a number, terms, line accounts and snapshots, and S4.2
gave parties an email and default terms, but the store journaled none of
them: a document carrying one was refused by a temporary guard, because a
public write that replay loses would break I-3. This slice makes the
journal carry them.

- `invoice.created` and `bill.created` write a number, terms, bill-to and
  seller snapshots, references, memo and a duplicate's reason, and each
  line its account, quantity and unit price, every key only when set. A
  document naming none is journaled byte for byte as every earlier version
  wrote it (`no_new_fields_journals_byte_for_byte`), so the owner's live
  books keep working.
- All of it is behind `documents_v2` (B-27): a write that names one waits
  for the owner's upgrade, refused with `format_upgrade_pending` and
  journaling nothing; a write that names none never waits
  (`documents_v2_waits_for_the_upgrade`).
- `invoicing.first_number` is settable only while no numbered invoice
  exists (A-2, B-40): the series has begun, and a number is never reused or
  renumbered. Refused at `set_setting`, before anything is journaled.
- The issued copy is filed in the issuing call (A-7, B-40 item 3):
  `create_invoice_issued` journals the entry, the invoice, then
  `receipt.attached` on that entry (`INV-0007.pdf`, `application/pdf`,
  "Issued invoice") and `attachment.linked` to the invoice. There is no
  `issued_pdf` field: the attachment id is allocated after the entry exists
  (CD-A6), and the link is how the copy is found and how its SHA-256 is
  checked. Every refusal happens before the first journal line; the PDF's
  bytes are stored before it, so a refused issue leaves an unreferenced,
  deduplicated blob at worst.
- A preview runs the issue on a clone and journals nothing: the posting,
  the total, the number it would take and the due date (`preview_invoice`,
  `preview_bill`). Books that haven't upgraded keep the legacy shape: the
  copy is filed as "Invoice {id}.pdf" and no number is taken (migration
  item 5).
- Schema 16 adds the document columns; a line's new fields ride
  `lines_json` as its project does. `differs_from` already compares whole
  invoices, bills and parties, so it sees every new field.

8.0.6: (1) `books_written_under_older_rules_still_open` hand-writes both
new document shapes through JournalWriter and still opens; (2) a new
fixture `old-books/p4_s43_documents` was written by main d5385a7e (S4.2,
the version before this slice) through the old-books-writer harness, with
its expected.json; this version opens it and reports the same figures;
(3) see the note on R-A step 4 in the PR (the over-the-wire document write
needs S4.4's arguments; S4.2's invoicing-setting entry already covers the
feature's cut-over); (4) `documents_v2_is_its_own_feature` is green; (5) no
figure moves: no golden changed; (6) merge does not need the owner's
go-ahead; the live books refuse the new writes until he gives it.

just check green (no kernel function, Lean theorem or Ledger command, so no
`just prove` change).
2026-10-27 10:37:03 -06:00
swaits 6be0055927 docs: P9a landed; the tunnel door is loopback-only and its live checks remain owner's 2026-10-27 09:37:46 -06:00
swaits c8541ea9fc fix(deploy): the tunnel's door binds loopback only (P9a)
Review finding: `--http` parsed whatever address it was given and bound
it. The door is the plaintext MCP service with a static bearer token;
plan 3.9 and the P9a model put a TLS proxy or tunnel in front of a
loopback listener, and no decision authorises exposing the books
unencrypted. `--http 0.0.0.0:7411` would have done exactly that.

`local::tunnel_address` now accepts only a loopback address
(127.0.0.0/8 or ::1; `127.0.0.1:7411` stays the default) and refuses
anything else with a message naming loopback and why. `server` refuses
while parsing, before it takes the lock or binds a socket; `serve`
refuses before it starts a host, so nothing is left half-open. The
bind-failure behaviour for a loopback address is unchanged (said, not
fatal, 3.9), as is the unit's hardening.

The test `the_tunnel_door_binds_loopback_only` refuses 0.0.0.0, a public
sample address and [::] through `server`, checks no socket or host was
left behind, refuses through `serve`, then starts a loopback listener and
reaches `books.start` with the bearer.

No accounting code, report or figure changes.
2026-10-27 09:35:36 -06:00
swaits 47951e5734 docs: record P9a as built, and what is still the owner's to verify
The unit's Build assumed `--http` existed (3.9 names it) and that a
headless server could mint a token with `token create`; neither was true.
The As-built block records both, plus the client-side bearer that 3.3's
`token print` cannot supply for a remote book, and the loopback-only unit
rule that blocks a remote backup push. Done-when 1 (the CI job) and 2
(clean-VM walkthrough) are explicitly left to the owner and EM; only the
local evidence is claimed.
2026-10-27 09:35:36 -06:00
swaits 1441dabcca feat(deploy): self-host recipe, the enforced isolation flag, and the tunnel's door (P9a)
The books can live on a machine of the owner's own (plan 3.8, 8.2 P9a,
D-51). Three things make that honest instead of nominal:

- `--isolation same_user|enforced` sets `server::Isolation`, carried
  through `Served` and `BooksServer` to `books.start`. Locally the books
  stay `same_user (advisory)`; under the self-host unit the server is told
  `enforced` and reports `service_user (enforced)`. The server never
  guesses, and the literal it used to report is gone.
- `--http [ADDR]` binds the same MCP router on loopback (default
  127.0.0.1:7411) beside the Unix sockets, before the ready line, for the
  TLS proxy or tunnel in front of the server. It is the same bearer door
  and the same graceful close; a failed bind is reported and is not fatal
  (3.9). `serve` carries both flags through the host, so the system unit
  can ask for them once.

`deploy/wanderbooks-server.service` is the unit: a dedicated account, a
0700 data root, `ProtectSystem=strict`, no seccomp or Landlock (D-32), and
loopback-only networking. `deploy/README.md` is the recipe: install, setup,
move the books by `books adopt` with its head check (D-31: a copied book
is the transport), mint a bearer token, put Caddy or Tailscale Funnel in
front of the loopback door, and point Claude Code and Codex at it.

The two-account CI job (`.github/workflows/two-user.yml`) makes the
deployment and runs `crates/ledger-cli/tests/two_user.rs` as another
account: `agent_uid_cannot_read_tenant` proves the tenant root and its
journal are refused, then reads, writes, uploads, files, exports and
verifies over HTTP with the bearer (in a second book, so the moved books'
head is not advanced); `self_host_adopt_same_head` compares the moved
books' head and event count with the source clone's.
`isolation_label_is_enforced_under_the_unit` runs everywhere, including
the unit-file read that keeps the recipe from drifting.

Accounting reason for the shape: nothing here changes an entry, a report
or a figure. It changes who can reach them: the move is lossless and
verifiable at the head, and the boundary is the operating system's, not a
promise in our text. No journal code, kernel function, Kani harness or
Lean step is touched, so `just prove` is not required.

No go-ahead needed: no new journal feature or event; the live books are
unaffected.
2026-10-27 09:35:36 -06:00
swaits cc4f104f06 docs: U0b landed; books.start and the viewer share one attention engine 2026-10-27 09:09:18 -06:00
swaits 26d3a9941b fix(package): books.start runs the real integrity check (U0b)
books.start passed Integrity::default() to the attention engine, so it
asserted "database matches, no file failures". On a book whose database
differs from the journal, or whose stored file is missing or corrupt, the
viewer showed the database/stored-files items and the server did not: the
two lists diverged exactly where they matter most.

Call find, which computes the check: every distinct stored blob is read
and hash-checked once (a damaged blob is still reported per attachment
filed from it), and Store::disk_check now makes the viewer's comparison
through a snapshot of its data directory instead of reporting a match.

The parity test now damages its own fixture (removes the receipt's blob,
then changes a projected account row) and asserts both lists still agree
item for item and both name the damage. On the demo books (138
attachments, 138 distinct blobs, debug build) books.start takes about
0.26 s warm, 0.29 s cold, so the real check costs no decision.
2026-10-27 06:12:55 -06:00
swaits 0cf2f8f5c5 feat(package): one attention engine over PackageBooks (U0b)
Needs attention was computed by the viewer alone, while books.start
hand-picked a handful of next steps. The human's list and the agent's
could disagree, and a new item had to be written twice (or, as
books.start showed, not at all).

Move the engine to ledger-package over PackageBooks: one function,
find/find_with(books, today, settings, integrity), reads the ledger, the
journal's event types (an entry's origin), the attachments and their
links, the statements, the reconciliations, and the database check. The
viewer's attention module now only maps Target to Route; books.start
turns the same items into its next steps (why = the item's title, top 12),
so the list the human sees and the steps the agent gets cannot drift.

PackageBooks gains settings, journal, statements, reconciliations and
disk_check, implemented for Store and Snapshot, so both views answer
alike. The W-9, reconciliation and 1099 predicates move beside the items
to keep one copy. ledger-package gains ledger-config and serde_json (the
receipt policy and the tool arguments the items carry).

Tests: view_model.rs and spec_kept.rs pass unchanged; the new
attention_parity.rs compares the viewer and books.start item for item on
the demo books and on a book written entry by entry. just check green.
2026-10-27 06:12:55 -06:00
swaits eeee9658da docs: U0a landed; the MCP handler groups are now the rule 2026-10-27 05:51:32 -06:00
swaits 45396bdce8 docs(architecture): name the ledger-mcp tool groups (U0a)
The layout paragraph the unit asks for: server.rs as the server, the
router and the request machinery; one tools/<group>.rs per tool group
with its router; tools/helpers.rs for the helpers several groups share;
server_tests.rs for the tests that span the surface and each group's own
tests in its file. plan/served.md's U0a "As built" block records the
split, the two departures (settings_tools as the module name, the test
bodies beside server.rs) and the stale 118-tool count (the surface is
122, identical to main's).
2026-10-27 04:15:47 -06:00
swaits 3cab75c494 refactor(mcp): split server.rs into a file per tool group (U0a)
server.rs had grown to 12,566 lines: every tool handler, every helper and
the request machinery in one file, so every unit that adds a tool touched
it and parallel worktrees conflicted on every merge (plan/served.md 8.2
U0a, 8.0.3). The 98 handlers still in it now live one file per group under
tools/, following the pattern tools/kept.rs and tools/projects.rs already
use (`#[path]` declaration, `use super::*`, one
`#[tool_router(vis = "pub(super)")] impl` per file):

  documents (invoices, bills, payments, voids, books.documents,
  books.document, the control check, the agings), entries (draft, line,
  post, reverse, get, search, the bank CSV import), reports (the
  statements, books.show, verify, digest, export, the CPA package, the
  P&L by class, with the export builders), assets, schedules, periods,
  reconciliation, parties (parties, vendors, customers, W-9s, the TIN
  vault, 1099s, entry parties), attachments (receipts, filed documents,
  links), settings (the handbook, books.settings, FX rates), chart,
  payroll, payouts, tax; add_class and list_classes join the existing
  projects file, since a project is a class with its facts.

The helpers only one group uses moved into that group's file; the ones
several groups share moved to tools/helpers.rs, re-exported by server.rs
so every group reaches them through `use super::*` and no group file
reaches into another's (resolve_instrument and the attachment/statement
shapers moved there too, since the payment, entries and attachment tools
all use them). server.rs keeps BooksServer, the store and its lock, the
request machinery, call_tool/get_info, tool_router (which now composes
the group routers; core_router is gone), and the shared helpers the plan
names (as_of, hand_over, resolve_vendor, parse_doc_ref, preview_lines_for).

This is a move, not a change: no accounting behaviour, tool name,
description, annotation or schema changed. A dump of
tool_router().list_all() — every tool's name, title, description, input
and output schema and annotations, in order — is identical to main's
(122 tools; the plan's "118" is stale). server.rs is 1,909 lines and has
no #[tool] handler. The server's cross-group tests moved to
server_tests.rs beside it (a #[path] module; fixtures in
server::test_support) and each group's own tests into its group file, so
server.rs stays under the 3,000-line bar. cargo test -p ledger-mcp: 99
passed; cargo test -p ledger-cli: 25 suites, 0 failed.
2026-10-27 04:15:47 -06:00
swaits d5385a7ee9 docs: P4b S4.2 landed; S4.3 owes the guard removal and the number lock 2026-10-27 03:46:51 -06:00
swaits bf91c5a7c2 test(settings): S4.1's Terms canonicalizes a short custom date (P4b S4.2)
The landed Terms::parse reads a date chrono reads and prints it padded, so
"Due 2026-1-5" is one value with "Due 2026-01-05", not a refusal. The
settings key still keeps one spelling; the test now asserts that
canonicalization instead of refusing the short form.
2026-10-27 03:44:24 -06:00
swaits 98320b3018 test(store): S4.2's shapes open old books and wait for consent (P4b S4.2)
The 8.0.6 checklist for this slice, which adds fields on party.updated
(an email and two default terms) and the invoicing settings it guards:

1. books_written_under_older_rules_still_open now hand-writes a party's
   email and terms and an invoicing setting through JournalWriter, and the
   books still open, verify and replay them.
2. A new old-books fixture, p4_s42_settings_parties, was written by the
   version before this slice (1bfce915, through the old-books-writer
   harness) with a party, a vendor, two posted entries and three settings,
   and its expected.json from that version's code. This version opens it,
   verifies it and reports its figures identically.
3. books_p2_served_cut_over_whole gains a refused invoicing setting before
   the go-ahead; the same call succeeds after it.
4. documents_v2_is_its_own_feature: books that declared every earlier
   feature stop at its declaration as newer, wait for the upgrade, and
   name exactly it in the upgrade.
No figure moves (item 5): settings and party facts enter no report.

Merge does not need the owner's go-ahead; the live books refuse the new
party and setting writes with the upgrade remedy until he gives it.
2026-10-27 03:44:24 -06:00
swaits 962b52c991 refactor(settings): the slice's contracts and notes (P4b S4.2)
invoicing.first_number's refusal (LedgerError::FirstNumberLocked) lands
as the contract the store's check raises once invoices carry numbers
(S4.3): a series that has begun is never renumbered (B-40, A-2).

Also: the settings invariants name the number prefix's bound, update_party's
doc says which facts wait for documents_v2, and the email check no longer
reads as a single-arm match. Terms is S4.1's as it landed; this slice no
longer carries a copy.
2026-10-27 03:44:24 -06:00
swaits 02cb7f0254 feat(parties): an email and default terms per side (P4b S4.2)
A customer's email is copied onto an invoice's bill-to snapshot at issue,
and a party's default terms (one as our customer, one as our vendor) are
the fallback a document's due date takes when the document itself names
none (A-13, rules.md B-39). Both are journaled as party.updated facts and
guarded by documents_v2: in books that have not declared it the write is
refused with the upgrade remedy and journaling nothing, and replay gives
the same directory back.

An email must be one plain address: one @, no spaces, no full TIN (the
party text rule already refuses nine digits). Terms are typed and already
canonical, so there is no second spelling to check. A party.updated naming
a field this version does not know is now NewerJournal, never corrupt
(B-27): an older version must say update, not damage; a known field with a
malformed value is still corrupt.
2026-10-27 03:44:24 -06:00
swaits 1fa77cff02 feat(settings): invoicing, bills and contact settings (P4b S4.2)
Eleven settings the invoice and bill workflow reads: the number prefix and
first number, default terms, payment methods with their bank details and
QR links, a footer, the unsent and reminder day counts, the bill reminder
and discount-warning day counts, and the company's email and website that
the seller snapshot prints.

Three new kinds keep them exact: a count is a positive whole number, terms
are canonicalized through Terms so "net 30" is stored once as "Net 30"
(B-39), and a payment method list admits at most eight methods whose lines
are at most 120 characters. A routing number is nine digits, so an ach or
wire method's bank-detail lines are the one text exempt from the full-TIN
check (A-3); every other text in a method is scanned.

The keys need the documents_v2 feature (D-52): an older version doesn't
know them, and the settings replay refuses an unknown key as newer. The
constant, the known-features entry and the gate test land with this slice
(its 8.0.6 work); S4.3 adds the document write call sites and must not
redeclare them.

A settings file now exports the [invoicing] and [bills] tables, a payment
method as an inline table (never a [[payment_methods]] section that would
land outside its table), and imports them back to the same values. The
unknown-key test uses fax instead of website, which is now a setting.
2026-10-27 03:44:24 -06:00
swaits 6995ebfee6 docs: P4b S4.1 landed; record the interim document-line guard and S4's departures 2026-10-27 01:57:04 -06:00
swaits 13282cf80e feat(arap): terms, numbers and line accounts in the core (P4b S4.1)
Documents could name only one revenue or general-expenses account per
project, had no number of their own, and took a due date the caller had
to work out. This slice gives the core the pieces the rest of S4 needs,
and journals nothing: an old-shape document still writes byte for byte,
so the owner's books need no upgrade for it.

- Payment terms (B-39): Net n, Net n EOM, due on receipt and a custom
  date, with the early-payment discount and its exact bound
  (kernel::discount_max, Kani + bridge). A due date past the calendar's
  end is refused, never a panic, and a discount is refused with terms
  that have no net days.
- Invoice numbers (B-40, A-2): first_number plus the count of numbered
  invoices, zero-padded to four digits and wider past 9,999, stored at
  issue, kept by a void; legacy invoices are uncounted.
- Line accounts (A-10): a document line may name its account, and the
  entry posts one line per (account, project) part. An invoice line takes
  revenue or a liability holding no role (deferred revenue); a bill line
  an expense or a non-cash asset holding no role. Roles, cash accounts
  and headers are refused.
- A line's amount is exactly its quantity times its unit price
  (kernel::line_amount, Kani + bridge): both or neither, never rounded.
- A vendor's invoice number names one standing bill per vendor, with
  case, spaces and hyphens ignored, unless a duplicate reason says why
  (B-40).
- The record fields S4.3 will journal: number, terms, bill-to and seller
  snapshots, customer reference, memo, the empty sends list S5 fills, and
  the bill's received_on, all optional and defaulted.

The store refuses a line carrying an account, quantity or unit price
before it rehearses or journals anything: those fields are journaled from
S4.3's documents_v2 on, and I-3 will not let a public write replay
without them. `document_line_fields_wait_for_documents_v2` proves the
refusal, the unchanged journal and allocators, and that an ordinary line
journals exactly as before. S4.3 removes this guard in the same change
that journals the fields and adds its byte-for-byte test.

The document commands keep their signatures; create_invoice_with and
create_bill_with take the optional fields for S4.3 and for tests. They
are the existing issue commands with more fields, not a new state
transition, so no Lean Step is added. Kani: line_amount_is_exact runs in
`just prove`; discount_max_is_bounded relates a product to a quotient and
runs in `prove-slow` (163 s), as the proration charge harnesses do.

just check green; just prove green.
2026-10-27 00:02:51 -06:00
swaits 58724bb007 docs: P13 steps 1-3 landed; record the CHOICE vocabulary and Engine::change 2026-10-26 23:23:21 -06:00
swaits 579d0eed5e fix(bookspec): the rebased P7 sentence speaks CHOICE too (P13) 2026-10-26 21:30:47 -06:00
swaits f85b03f467 docs: finish the DECISION-to-CHOICE vocabulary outside rules.md (P13) 2026-10-26 21:23:16 -06:00
swaits 1855aa938e docs: drop the rebase marker from the P13 testing section (P13) 2026-10-26 21:23:16 -06:00
swaits f91798cd6e fix(bookspec): the schema refuses a key only its query may name (P13)
`accepted` and `refused` are a command's (and `refused` a period's): the
schema now refuses either on a query that does not take it, as the loader
already did, instead of letting a malformed example reach the runner.
The no-op table walk in the same branch goes with it.
2026-10-26 21:23:16 -06:00
swaits cb4e955942 feat(bookspec): rule identifiers from B-1 to B-45 (P13)
Every rule of `docs/spec/rules.md` now has one bookspec identifier,
`BS-<area>-<n>`: eleven areas (entry, ledger, statement, period, partner,
asset, prepaid, arap, revenue, 1099, format), numbered in B-rule order
within its area. The table is `bookspec/spec/rules-index.md`, and each
stated rule carries its id under its heading in `docs/spec/rules.md`, so
a reader of either can find the other.

rules.md's DECISION status becomes bookspec's CHOICE: the specification
names the options and an engine declares its pick (`Claims::with_choice`),
which is what an example already meant by `status = "CHOICE"`. B-39 to
B-45 are allocated in `plan/served.md` 8.0.3 and not yet stated: they
carry an id and an area and no status, so a later unit fills them in
without moving anyone else's number.

The catalogue names the examples that demonstrate each rule (56 files,
each under at least one rule) and the rule each refusal reason belongs
to. A rule with no example cannot be claimed: a claim needs evidence.
`docs/architecture.md`, `docs/testing.md` (tests and the For-P12
mutation targets) and `docs/parity.md` record the new shape; the
bookspec README points at the checker, the runner and the adapter.
2026-10-26 21:23:16 -06:00
swaits b7be5a5472 feat(bookspec): the checker crate, the Engine trait and the runner (P13)
bookspec is a specification first: every rule normative and sourced,
every rule demonstrated by concrete examples, and any accounting engine
checked against them through one interface (D-34). This moves the
examples' schema out of the test that happened to parse them and makes
the interface explicit.

- `bookspec/checker` (its own workspace, inside this repository until
  the harness is stable): `Example`, `Setup`, `Event`, `Expect`, `Query`,
  `Refusal` and the TOML parser, every structure `deny_unknown_fields`,
  with `Example::from_toml` checking the cross-field shape (closed lists,
  exact decimal amounts, dates, one event kind, `reverses` counting from
  1, the shares summing to 1, commands accepted or refused and not both).
- `Engine` (`load`, `apply`, `query`, `claims`) and `Refusal { rule,
  reason }`; the refusal's rule is the bookspec identifier, so a refusal
  names the rule it belongs to. `Claims` also carries the option an
  engine picks for a CHOICE rule.
- `bookspec/runner` drives any `Engine` through the examples and reports
  per rule: pass, fail, not claimed. It compares the answer itself; an
  engine is never asked whether it agrees with itself.
- `crates/ledger-core/tests/bookspec.rs` is the wanderbooks adapter over
  `Store`: the loader parses through the checker, its own checks are
  unchanged, and the new `the_runner_reports_every_claimed_rule_green`
  runs all 56 examples through the trait. The IIF appendix is answered
  through `ledger_package::export_iif`, the code its own test checks.
- `just check` runs the bookspec workspace's fmt, clippy, rustdoc and
  tests too, so an example cannot rot (D-34, Z-17).

The plan's trait has four methods and no path for an example's
`[[changes]]` (a rename, the policy, an asset correction), which post no
entry: `change` is the fifth, required so an engine cannot skip those
examples silently. This is reported to the EM as the one departure.

One money rule holds here too: an answer's figures are exact decimal
strings, never JSON numbers, so no engine can round a cent through a
float.
2026-10-26 21:22:46 -06:00
swaits 8c956e8d74 docs: P7 landed; the burst-post test's load sensitivity recorded 2026-10-26 20:29:55 -06:00
swaits 3e65c83f96 docs: B-26's TIN separators and P7's mutation targets (P7)
rules.md B-26 now says what the scanner reads: a comma or a parenthesis
between a TIN's 3-2-4 or 2-7 groups, and any run of spaces counting
once, while an amount's comma-grouped thousands stay an amount. The
filing scan's (a) and (b) renderings gain commas. docs/testing.md gains
the P7 section and the For-P12 mutation targets (D-33), and plan/
served.md's P7 As-built block records the three departures.
2026-10-26 20:29:15 -06:00
swaits 9e67c8819d fix(tin): mask and refuse a comma-separated TIN (P7)
The scanner now finds a comma-grouped TIN, so the two places that hand
raw text out or let it in must follow: the access log's digest and its
64-character client texts masked hyphens but not commas, and a filed
file's printed-shape check refused 123-45-6789 but let 123,45,6789 in.

The access log uses the shared redactor, so it inherits the fix; the
filing scan keeps its own deliberately narrow shapes (a bank export's
routing numbers and amounts must still be filed, B-26) and gains the
comma renderings of an SSN's 3-2-4 and an EIN's 2-7. A comma-grouped
amount is still no TIN and still files.

No book, journal line or figure changes.
2026-10-26 20:28:41 -06:00
swaits c6527280e7 fix(parties): read every separator a TIN can be written with (P7)
A taxpayer identification number can be written 123,45,6789 or
(123) 45-6789 as easily as 123-45-6789, and the scanner only knew the
second: a comma-grouped number slipped through every party field, every
refusal that quotes its input and the access log's masking, which is how
a full TIN could reach a result the books promise never shows one
(rules.md B-26, I-7).

The scanner now reads a comma or a parenthesis between a TIN's 3-2-4 or
2-7 groups, and any run of spaces counts once however long it is, so
12    3456789 is an EIN. Commas and parentheses join only those groups:
an amount's thousands separators (1,234,567.89 is 1-3-3-2) still read as
an amount, and a nine-digit D-U-N-S number stays its own setting's kind
(company.duns). The old run reading is unchanged for everything it
found, which the property asserts; replay still reads a journaled line
as P2 did (B-28).

The truth table and the ASCII-equality property pinned the old separator
set, so they were updated with it: the property is now
has_full_tin_never_forgets_a_tin (what was found before is still found).
No journal, entry or figure changes: this is a text scanner only.
2026-10-26 20:28:41 -06:00
swaits 40101f197e fix(just): sweep only day-old test roots, so parallel worktrees keep theirs 2026-10-26 19:42:14 -06:00
swaits 750759075b docs: U0c landed; record the books.documents party-filter gap (2026-10-26) 2026-10-26 18:52:24 -06:00
swaits 198aa2a115 docs(plan): U0c as built (U0c)
Records the two departures. The timeline's reversed items are labelled,
not struck through, so the S2 paragraph targets the labels the viewer
draws. And the A/R and A/P section's party filter takes a customer's or
vendor's name (the handler also matches a document's own id), not the
customer or vendor id the old text and the schema promised; the schema
is a code finding, because this unit may not change code.
2026-10-26 15:59:06 -06:00
swaits feae51e49f docs(mcp): the A/R and A/P section against the shipped tools (U0c)
Checking the section against the handlers found four things to say
differently. A row's status is the DocStatus key, which is a different
list from the filter words (overdue and all filter; partial and
unattached only report); the timeline is bounded by as_of; the answer
carries pdf? when it is asked for; and the party filter matches a
customer's or vendor's name, not the customer or vendor id the old text
promised. The section head now says S4 changes the two
document-creating tools, so a reader of today's surface is not
surprised when numbers and terms arrive.
2026-10-26 15:58:28 -06:00
swaits e9febc5694 docs(testing): the P4b S1-B, S2 and S3 sections (U0c)
The 8.0.4 bar asks every unit to record what P12 must mutate (D-33), and
P4b S1 part A has its paragraph while part B, S2 and S3 shipped without
one. S1-B's targets are the payment details (a reference is one line of
at most 200 characters, carried in the entry's description; the
instrument is tagged in the same call) and the two handlers that put
B-38 on the wire (void, control check). S2's are the status filters and
the timeline labels that make a reversed payment read as reversed
instead of disappearing. S3's are the stamp arms and the re-created-copy
branch, where an invoice that has no kept copy says so.

The S2 section records what shipped against the plan's As-built words:
the timeline labels reversed items ("Payment reversed", "Payment
(reversed later)"); the viewer draws no strikethrough.
2026-10-26 15:58:27 -06:00
swaits 1bfce9157a docs: land the 2026-10-26 plan review and the team hand-off (plan/HANDOFF.md, served.md section 8) 2026-10-26 15:20:00 -06:00
swaitsandClaude Opus 5.5 c1f9001cfe feat: the invoice PDF, with stamps and each payment (P4b S3)
check / check (push) Canceled after 0s
ledger-import invoice_pdf (templates/invoice.typ): seller, bill-to,
number, dates, terms, lines, total, each settled line and the balance
due; money formatted in Rust (the template does no arithmetic), text as
data, deterministic bytes. Stamps as a rubber stamp would mark it, in
red: PAID (with the day it was paid in full: the invoice is then the
customer's receipt), PARTIALLY PAID, PAST DUE, VOID; DRAFT in gray for
S4's preview. ledger-package invoice_document builds it from the books
as of a day: the derived figures (B-38), each payment that stands on its
own line with its date, payment method and reference; re-created copies
say so (the issued copy is kept from S4 on and never changes).
books.document pdf hands it over as a download; the viewer's invoice
page saves or opens it. The owner asked for the paid stamp with the
payment's date and details (2026-10-24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-24 18:20:55 -06:00
swaitsandClaude Opus 5.5 b7b7f17163 feat(viewer): Invoices & Bills, a document's page, overview tiles (P4b S2)
check / check (push) Canceled after 0s
One sidebar item, Invoices & Bills (the owner's choice, 2026-10-24),
with Money in (invoices) and Money out (bills) tabs: what is owed and
overdue, and the list tied to its receivables or payables account
(Agrees, or what to look at); filters Open, Overdue, Paid, Void, All;
rows by due date, an overdue one saying how many days late, each named
in full for assistive tech. A document's page: total, received or paid,
still owed, status; its lines with their projects; its timeline, a
payment later reversed marked so, each row opening its entry. The
overview adds Owed to you and You owe on bills once the books have
documents. Every figure is the document's derived state (rules.md B-38).
The sidebar's rows (and the rail's) give way to 22 points, not 24, so
13 links still fit the owner's 1479x519 window unscrolled; the
sidebar's tests compute heights from the link count; the 1440x920
golden is re-blessed for the new link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-24 15:33:04 -06:00
swaitsandClaude Opus 5.5 6f0fd97db0 test: the burst-close test closes once every client has posted
check / check (push) Canceled after 0s
closing_during_a_burst_of_posts_loses_nothing slept a fixed 1.5 s before
closing and asserted every client had posted; under load one hadn't (a
false failure of just check on 7d2bf0b). It now closes once each client
has posted at least once (a minute's deadline), then 300 ms more so the
close still lands mid-burst; what it checks is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-24 13:57:58 -06:00
swaitsandClaude Opus 5.5 7d2bf0b75f feat(mcp): books.documents and books.document (P4b S2, read tools)
check / check (push) Canceled after 0s
books.documents lists invoices or bills, due date first, with what each
still owes and its status, all derived from its ledger lines that stand
(B-38): status open (default), overdue, paid, void, written_off,
overpaid or all; party and due_by narrow it (what to pay this week).
books.document shows one (ref invoice:N or bill:N) as of a day: lines
with projects, total, paid, discounted, written off, outstanding,
status, days overdue, and its timeline of every tagged ledger line, each
saying whether its entry still stands. The over-the-wire test
documents_list_and_timeline_over_the_wire covers a reversed payment;
the happy-path journey covers both output schemas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-24 13:55:16 -06:00