Commit Graph
458 Commits
Author SHA1 Message Date
swaitsandClaude Fable 5.1 335a3eaa7e merge: P4b S7, bad debts (branch S7, 4b4fecb9)
The fourth of the paused branches: Ledger::write_off and the bad_debts
control role, invoice.written_off with its evidence filed in the same
store call, books.write_off_invoice, the write-off on the timeline and
in Needs attention, Lean writeOff_within_bounds, rules.md B-41 and the
bookspec examples. Twenty-eight files conflicted with the S4.4, S5 and
S6 merges; resolved as plan 8.0.2 says, keeping both sides' appends with
their seams restored, and: the scope table at 125; S7's law numbered 37;
B-41 placed before B-42; the handbook's step 4 and recovery procedures
folded into the one invoices-and-bills section; S7's wire tests in their
own module with the confirm S4.4's contract needs; S7's copy of the
payment steps dropped (S6 replaced them with Ledger::settle) and its
write-off journaled through S6's journal_auto_lines; in the old-books
fixture S7's hand-written write-off moved to a second invoice, since
S6's hand-written payment settles invoice 1 in full.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
2026-10-28 21:47:33 -06:00
swaitsandClaude Fable 5.1 be58059830 merge: P4b S6, paying on terms (branch S6, 6c4480cd)
The third of the paused branches: Ledger::settle with discounts and the
processor's fee (kernel::settle, Kani settle_is_exact, Lean
settle_posts_a_document_step), books.receive_payment and books.pay_bill
with applications, books.bills_due, the agings' detail and tie-out, the
aging exports and the CPA package's aging CSVs, the handlers in
tools/settlements.rs. Twelve files conflicted with the S4.4 and S5
merges, resolved as plan 8.0.2 says: both routers; both test appends
(the shared match arm in arap_standing.rs spliced into both tests; the
shared `.unwrap()` in store.rs); the scope table at 124; one
invoices-and-bills handbook (S5's send step 1 with S6's steps 2, 3, 5
and procedures) registered once; docs/mcp-interface.md with S4.4's
create docs and S6's payment docs; S5's law renumbered 35 and S6's 36
(S1's is 34); the parity rows and sections of both. S6's wire tests
create their bills as S4.4's contract now requires (confirmed, the
vendor's number, a line account, the vendor's invoice uploaded) and live
in their own module beside S4.4's tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
2026-10-28 21:26:40 -06:00
swaitsandClaude Fable 5.1 3a3f03d884 merge: P4b S5, sent tracking (branch S5, ad649be3)
check / check (push) Canceled after 0s
two-user / two-user (push) Canceled after 0s
The second of the paused branches. invoice.sent, Ledger::mark_sent (with
its Lean step), books.mark_sent in its own group, the two attention
items, schema 17. Conflicts, resolved as plan 8.0.2 says: the shared
over-the-wire test file crates/ledger-cli/tests/documents.rs (S4.4's
file kept whole, S5's test appended, its create call given the
confirmed: true the S4.4 contract now needs), types.rs (both appends
kept), docs/parity.md (the review's oracle row and S5's new row).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
2026-10-28 21:21:21 -06:00
swaitsandClaude Fable 5.1 d329bd6f56 merge: P4b S4.4, the create tools preview and confirm, the party contact block (branch S4.4, 002fb9f8)
check / check (push) Canceled after 0s
two-user / two-user (push) Canceled after 0s
The first of the five paused branches (plan 8.0.2, "Resuming the paused
wave"). books.create_invoice and books.create_bill preview without
confirmed and post with it (A-1), the issued PDF filed in the same
store call, the party's full contact block (D-55) on books.update_party
and the invoice's bill-to. The only conflict was docs/parity.md, where
the 2026-10-28 review had written the S1-S4.3 notes on the A/R and A/P
rows and S4.4 its own: both are kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
2026-10-28 21:11:11 -06:00
swaitsandClaude Fable 5.1 d6388d7be7 docs: D-57 and D-58, the owner's answers to Q-5 and Q-8
check / check (push) Canceled after 0s
two-user / two-user (push) Canceled after 0s
Q-5: one format upgrade of the live books, after S7 and P4 have landed,
declaring documents_v2 and drafts_v2 together (D-52 confirmed; A-14
records it). Q-8: the review's decisions D-47 to D-54 are the owner's;
the "may revisit" clauses are closed. Both asked and answered through
the question tool on 2026-10-28. The owner also chose to have the
planning session merge the paused branches itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
2026-10-28 21:07:19 -06:00
swaitsandClaude Fable 5.1 af553a3706 docs: the 2026-10-28 drift review; the plan, status and docs brought to date
check / check (push) Canceled after 0s
two-user / two-user (push) Canceled after 0s
The planning session read plan/served.md, every design document and the
five branches the owner paused on 2026-10-27 against main (44ee4083),
with dry-run merges touching no branch. plan/2026-10-28_DRIFT_REPORT.md
lists every divergence and judges every recorded departure (all kept;
three owed back; one needs a decision: P6 made ledger-store depend on
ledger-import).

The plan: section 0 "What changed on 2026-10-28"; 8.0.1 and the unit
Status lines name S4.4, S5, S6, S7 and P6 as built but unmerged; 8.0.2
records the destination (D-56: the installed app, P11a starts now) and
the resume order with the known conflicts (the shared documents.rs
test file, the types.rs appends, P6 retaking schema 18, the law numbers
34 to 37); 8.0.3 lists the names the EM approved during the wave; P11a
gains step 0 (the macOS and Windows keyring stores, the Windows data
root and pipes, a cross-platform CI build: today the agent path runs on
Linux alone); Q-5 is due and Q-8 asks whether D-47 to D-54 stand; the
Landlock sentences in section 2 and server.toml are struck.

STATUS.md and HANDOFF.md say where things stand and how to resume;
roadmap.md and gui.md are rewritten to today. The docs lose their
stale facts: 122 tools, 2,700 tests, the parity floor at 21 with the
P4b notes the slices never wrote, the ten crates and their dependency
graph (jiff added to the core's allowed list), the cutover of
2026-10-20 recorded in capability-parity.md and parallel-run.md, the
P4b S1 law and its two Kani harnesses added to verification.md (law
34), the proof counts (51 and 9), and README, the site and install.sh
honest about macOS (the viewer runs; connecting an agent waits for
P11a's keyring store, and the installer now says so instead of failing).

just check on main at 44ee4083: exit 0 (167 suites, 2,871 passed, 0
failed, 7 ignored, the server-only build clean). No code changed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
2026-10-28 18:26:23 -06:00
swaits 44ee408312 docs: the paused team is stopped; record the branches and P6 checkpoint 2026-10-27 14:42:19 -06:00
swaits 3a1b8d8576 docs: record the owner's pause of the five in-flight units 2026-10-27 14:39:08 -06:00
swaits ad649be3d5 docs: S5 step 4 landed; attention predicates in the test record (P4b S5)
The As-built block records step 4 built after U0b, its two items and
their tests; docs/testing.md names them and what P12 must mutate (the
number exemption, the open/partial check, sends.is_empty(), the two day
comparisons with their checked date arithmetic, the last-send max);
docs/parity.md says sent tracking also feeds Needs attention.
2026-10-27 13:46:43 -06:00
swaits 485bd4ae5f feat(attention): unsent and unreminded invoices (P4b S5)
The books record that an invoice went out but do not chase it. U0b's
one attention engine now lists two items the sent record makes possible,
in the viewer's Needs attention and in books.start's next steps alike:

- "Invoice INV-0007 was never sent": an issued invoice (a number) still
  Open or Partial, with no send recorded, once today is past its issue
  date plus invoicing.unsent_days (default 2). A legacy invoice with no
  number is exempt, and so is a void or settled one.
- "Invoice INV-0007 is overdue: remind the customer": the invoice is
  overdue and its last send is older than invoicing.reminder_days
  (default 14). An invoice never sent gets the first item, not this one.

Both are Todo, open the invoice, and offer books.mark_sent with its
invoice and the tool's defaults. The viewer maps the new
Target::Invoice to its invoice page (one line; items still come only
from ledger-package), and the two lists cannot drift.

Tests: an_unsent_invoice_is_listed_until_a_send_clears_it (the overview
draws it; attention-unsent-invoice.png; a send clears it),
a_legacy_invoice_with_no_number_is_never_listed_as_unsent,
an_overdue_invoice_with_an_old_send_is_asked_to_remind, and
an_unsent_invoice_is_on_both_lists (viewer and books.start, step
books.mark_sent on invoice:1). The existing attention parity test stays
green.
2026-10-27 13:46:43 -06:00
swaits 4b4fecb98e feat(gui): the journal says a write-off in plain words (P4b S7)
R-B: a new event type gets a describe arm, so the journal's list reads
"Invoice 7 written off, 60.00" and opens the entry, rather than printing
the type at a human. every_journal_event_has_plain_language_text scans
store.rs for the types and fails on any without text.
2026-10-27 13:27:27 -06:00
swaits adc8804f61 refactor(mcp): books.mark_sent in its own sent group (P4b S5)
S4.4 is working in tools/documents.rs, so the send tool moves beside it
rather than into it: `crates/ledger-mcp/src/tools/sent.rs`, declared like
every other group and named in docs/architecture.md's layout. The two
helpers the document read tools share (`sent_out`, `last_sent`) go to
tools/helpers.rs, which the groups share; the handler and its own
argument readers stay private to `sent`.

No tool name, schema or behavior changes: books.mark_sent is where it
was on the wire, only in a file of its own.
2026-10-27 13:24:10 -06:00
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 6c4480cdc8 feat(package): aging CSVs in the export and the CPA package (P4b S6)
The CPA's first tie-out is that the aging equals the control account, so
books.export and the CPA package now write ar_aging.csv and ap_aging.csv
from one builder: one row per open document (number, party, dates, terms,
total, paid, discounted, outstanding, bucket, days overdue) and a TOTAL
line carrying the outstanding sum, the GL account and balance, and the
difference (always 0.00; B-23, B-38 INVARIANT 1). XLSX and PDF lay the
same aging out as a statement with the tie-out beneath it.

The package README states the tie-out, the gross discount method (A-8)
and the direct write-off method (A-5). The chart templates gain 6995
Purchase Discounts so no template puts another account at a control
role's code (4100 and 6060 already match). The viewer's Money out list
gains the discount-deadline column and colours a due-soon or
discount-open date; the screenshot is re-rendered.

Test: the_aging_csvs_tie_to_the_control_check in ledger-package; the
existing package file-order test and mcp_journeys see the two new files.
2026-10-27 12:59:40 -06:00
swaits 57f04dbb0c feat(mcp): payments on terms, bills due, and the agings' tie-out (P4b S6)
books.receive_payment and books.pay_bill take applications, each
document's cash with an optional discount and reason, and a customer
payment may carry the processor's fee; the result names every document it
settled and what reached the account. books.bills_due lists what to pay
and the running total by due date. The agings take detail and return the
control account's tie-out, and books.export writes ar_aging/ap_aging.

The handlers live in tools/settlements.rs (the EM's split, so S4.4's
documents.rs stays free); documents.rs keeps the documents, the void and
the control check. docs/mcp-interface.md documents every changed tool and
the new one; docs/architecture.md names the group; docs/testing.md
records the tests and P12's mutation targets.

Old names (document_id, cash_account) still work; the output keeps
remaining/status for one document. A fee's processor is named in the
description, not tagged as the entry's vendor: ledger tags are entry-wide
and tagging would move the customer's party totals and 1099-NEC.

The handbook's invoices-and-bills section gains the payment, discount,
fee and bounce-replacement procedures (S4.5 can extend it). Over the wire:
one_payment_settles_two_invoices, late_discount_needs_a_reason,
a_fee_posts_gross_to_receivables_and_net_to_the_bank,
card_paid_bill_is_not_1099_nec,
multi_document_payment_counts_once_for_1099_and_party_totals,
overpayment_refused_with_the_excess,
bills_due_orders_by_due_and_discount and the_aging_export_ties_out.

Terms cannot yet be journaled on a document (S4.3) and a bill line's
account is not an MCP field (S4.4), so bill_on_terms_with_discount,
equipment_bill_payment_is_investing_cash and the discount half of
bills_due are core tests until those land; the unit's As-built block
records this.
2026-10-27 12:59:40 -06:00
swaits 78a2385440 feat(store): journal settlements and replay their lines (P4b S6)
Store::settle now journals a settlement once, line for line, and one
payment event per application, so replay rebuilds the same tags and the
projection sees the same entry. A single legacy-shaped payment is still
journaled byte for byte; more than one document, a discount or a fee
needs documents_v2, and a payment accepted only because an earlier one
was reversed still needs document_standing.

Each event names the control line it tags (line) and, when it took one,
its discount and discount line; the fee is on the first application's
event with its line. Replay tags exactly those lines and leaves a line it
cannot match untagged, which books.control_check lists; a legacy event
without line keeps the first-untagged-control-line rule.

Tests: a_settlement_names_its_lines_and_replays (two invoices, a discount
and a fee; event indices match the plan; replay equals live),
settlement_waits_for_documents_v2 (refused with the feature remedy and no
journal line, accepted after the upgrade), and
legacy_payment_events_keep_the_positional_rule. arap_props gains a
Settle op (two documents, discount, fee), and
books_written_under_older_rules_still_open gains a hand-written payment
event of the new shape through JournalWriter (8.0.6 item 1).
2026-10-27 12:59:40 -06:00
swaits 10ee0681c1 feat(arap): settle documents with discounts and a fee (P4b S6)
One payment had to name one document and could take no discount or
processor fee, so paying several invoices from one deposit meant several
entries and the fee had nowhere to go. This adds the kernel's exact
settlement arithmetic (applications total, net deposit), a Ledger::settle
that posts one entry for a customer's or vendor's documents, and the gross
method and gross fee A-8/A-4 chose.

- kernel::settle: sum(cash)+sum(discounts)=applications, sum(cash)-fee=
  deposit, None on overflow or a negative deposit. Kani settle_is_exact
  plus the Decimal bridge.
- Ledger::settle / settlement_lines: one party and one kind per
  settlement, one application per document, each cash+discount within
  payment_room; a discount within the terms (discount_max and
  discount_by) needs no reason, a larger or later one does; a bill with
  an asset line is refused with D-49's remedy; a fee is a customer
  payment's only and posts gross.
- Rows are payment lines in application order, then discount lines, the
  fee line and the cash line, so a journaled event's line indices are
  stable; replay tags exactly the line an event names and leaves an
  unmatched line unattached (never refuses).
- receive_payment_with and pay_bill_with become one-application calls.
- Roles: sales_allowances (4100), purchase_discounts (6995) and
  payment_processing_fees (6060), constants used on demand but not added
  to control_accounts() (existing charts may use those codes for other
  things).
- Lean: settle posts through Step.document, so settle_posts_a_document_step
  and settle_unattached, no new constructor; both in Audit.lean.
- Core tests: bill_on_terms_with_discount, late_discount_needs_a_reason,
  discount_on_an_asset_bill_is_refused_with_the_remedy,
  a_fee_posts_gross_to_receivables_and_net_to_the_bank,
  a_settlement_names_its_lines_and_refuses_a_document_twice, and
  equipment_bill_payment_is_investing_cash in the cash-flow suite.
- Bookspec B-42 is stated, with its id BS-arap-6; the catalogue and index
  now record it as CONVENTION.

just check and just prove green.
2026-10-27 12:59:05 -06:00
swaits d74ee1e962 docs(plan): P4b S7 as built (P4b S7)
The unit's As-built block: where the build departed from its text, and
why (the bad_debts role, the least-owed bound over the date-sum, the
evidence rule, the handbook section it had to create, the bookspec
schema's write_off event, and where the tests live). The Build text is
untouched; the EM moves the status row.
2026-10-27 12:45:01 -06:00
swaits 176baa5250 test(arap): the role inventory gains bad_debts (P4b S7)
B-41 gives the write-off its own expense role (6990 Bad Debt Expense,
8.3.4), so the pinned inventory of control roles has thirteen keys, not
twelve; the new role's key, default code and type are checked with the
rest. No other role changes.
2026-10-27 12:44:45 -06:00
swaits 22cdfcf34b refactor(arap): rustfmt the unit's files, and the write-off's SOURCE line (P4b S7)
No behaviour: the files as rustfmt leaves them after the unit's edits
(the commits before this one were made before the formatter ran), and the
Ledger::write_off spec block cites A-5 alone for the direct write-off
method (A-6 is the hand-entry refusal, not this command).
2026-10-27 12:44:45 -06:00
swaits f31dd2640b docs: the write-off's place among the destructive tools and the tool counts (P4b S7) 2026-10-27 12:44:45 -06:00
swaits e801908a88 test(mcp): exercise the write-off in the happy path (P4b S7)
spec_mcp's inv1 walks every tool once and fails on any the books never
answered: the happy path now creates an invoice that will not be paid,
uploads the collection letter, and writes it off with the evidence in the
same call, so books.write_off_invoice's output schema is checked like
every other tool's.
2026-10-27 12:44:45 -06:00
swaits 0322778683 docs(arap): B-41, the bookspec examples, and the four registers (P4b S7)
rules.md B-41 states the write-off: the direct method the owner chose
(A-5), Dr 6990 / Cr 1200 with the receivable tagged, at most what is
owed and at most the least owed from that day on (a partial write-off as
§1.166-3(a) allows), refused on cash-basis books (§1.166-1(e)), its
evidence filed in the same call (§1.166-2(a)); bills are never written
off (ASC 405-20-40-1); and a recovery is not a new movement (D-49): the
same year, reverse the write-off and record the payment; a later year, a
plain entry to a bad-debt-recoveries revenue account, the invoice staying
written off (IRC §111).

bookspec states B-41 for the first time and gives B-38 its example: two
new examples, bad-debt-direct-write-off (two partial write-offs of one
invoice, the balances and the trial balance worked by hand) and
arap-hand-control-posting-refused (a hand line an earlier version wrote
replays, the same line written today is refused). The example schema
gains the dated write_off event, lets a refused entry say so (an entry
posts whole or not at all; older and refused never combine), and its
refusal vocabulary gains untagged_control_line, B-38's control guard.

The registers: docs/architecture.md names the new tool group;
docs/receipt-store.md lists invoice.written_off and what replay does with
it; docs/parity.md records the slice (no row changes status, the floor
stands); docs/testing.md records every test and what P12 must mutate;
docs/verification.md the two Lean theorems and what they cover.

No figure moves: no golden file changed.
2026-10-27 12:44:45 -06:00
swaits b731d7ed04 feat(arap): a write-off on the timeline, and the CPA's note (P4b S7)
The viewer's invoice page already drew a WriteOff movement; now the
fixture proves it: a written-off invoice reads Written off, owes nothing,
and its timeline's "Written off" row opens the entry (the screenshot
invoice-written-off).

Needs attention gains the one item the tax rule asks for: a write-off
made this calendar year is listed, one per invoice, as "Write-off
awaiting the CPA's note", with what was written off, the day, and the
prompt that opens the document. Whether a debt is worthless, and in
which year, is a tax judgment that stays with the CPA (plan 8.3.16); the
books flag it and keep the evidence beside it until the year ends. The
item is computed in ledger-package's one attention function, so the
viewer and books.start see it alike; view_model.rs compares the two reads
on the same books.

The handbook's invoices-and-bills section starts here (S4.5 adds the
issue, terms and payment steps around it): step 4 says when to void, to
write off, or to reverse, and the recovery procedures D-49 records — the
same year, reverse the write-off and record the payment; a later year, a
plain entry to a bad-debt-recoveries revenue account, the invoice staying
written off (IRC §111).

Tests: a_write_off_shows_on_the_timeline,
a_write_off_is_flagged_for_the_cpa.
2026-10-27 12:44:45 -06:00
swaits 0f1c7786ab feat(mcp): books.write_off_invoice (P4b S7)
One tool for the bad debt (rules.md B-41): it writes off an invoice that
will not be collected, Dr 6990 Bad Debt Expense / Cr 1200 Accounts
Receivable, the receivable line tagged with the invoice so the document
reads written_off and its figures stay derived (B-38).

The evidence is the point, and the tool takes exactly one of
upload_id (an upload, filed with the write-off) and
existing_attachment_id (one already filed, linked to the write-off's
entry and the invoice); neither or both is refused. amount defaults to
what the invoice owes and may be partial.

Without confirmed it is a preview: the engine runs on a clone (every
refusal the post would give) and the answer is the figures the write-off
would leave, entry_id 0, nothing journaled.

The tool lives in its own group file, tools/write_offs.rs (plan U0a), so
S4.4's document tools and this one land apart; it is destructive, needs
books:write to preview and books:post to post (scopes.rs), and is
documented under "Tools — A/R and A/P". The destructive-tool count, the
tool tables and the journey test's tool count each move by one.

Tests over the wire: write_off_then_recovery, write_off_needs_evidence.
2026-10-27 12:44:45 -06:00
swaits 4e71cf6320 feat(store): journal a write-off with its evidence (P4b S7)
A bad debt is deducted on evidence it is worthless (Treas. Reg.
§1.166-2), so the store will not journal one without it:
Store::write_off_invoice takes the evidence as bytes to file or as an
attachment already filed, and files it in the same call — a
receipt.attached on the write-off's entry and an attachment.linked to
the invoice (rules.md B-41). With no evidence it refuses, journaling
nothing.

The entry (Dr 6990 Bad Debt Expense / Cr 1200) and its evidence are
journaled after the engine's whole check and after the feature gate: the
new event type invoice.written_off needs documents_v2, so an older
version stops at the declaration rather than reading a written-off
invoice as still owed (B-27, D-52). Its payload is the invoice, the
entry, the day, the amount and the reason; replay tags the receivable
line WriteOff, and the projection keeps no written-off figure — it is
the tagged lines' sum (B-38).

`meta::invoice_written_off_of` reads and writes the payload; the replay
fuzzer draws the type.

8.0.6: (1) `books_written_under_older_rules_still_open` hand-writes the
new event and its entry through JournalWriter and still opens, with the
figures tailing; (2) a new fixture old-books/p4_s7_before was written by
main 4cad2cfc (the version before this unit) through the old-books-writer
harness — a numbered invoice with terms and snapshots, a partial payment,
a voided invoice and a paid bill — with its expected.json; this version
opens it and reports the same figures, the viewer included; (3) the
cut-over entry is in format_upgrade.rs's books_p2_served_cut_over_whole;
(4) documents_v2_is_its_own_feature is green and the event gate's arms
pass; (5) no figure moves: no golden changed (the previous version's
expected.json is the comparison); (6) merge does not need the owner's
go-ahead; the live books refuse the new write until he gives it.

Tests: write_off_waits_for_documents_v2, write_off_replays_and_projects,
write_off_without_evidence_is_refused; arap_props.rs gains WriteOff.
2026-10-27 12:44:45 -06:00
swaits 62f2fb058e feat(verify): prove a write-off never takes a document below zero (P4b S7)
Ledger::write_off is a new command, so it is a new Step: Step.writeOff,
whose two lines are a tagged control line (the receivable falls) and an
untagged expense line (the deduction). Being a document step in shape, it
moves nothing unattached, so control_agrees and
Reachable.control_is_documents still hold with no new proof:
Step.writeOff_unattached states its half of that.

writeOff_within_bounds is the law the command's refusal enforces: a
write-off of at most what the document is owed, read over the date-sum
docSum ... through D (B-38 fixed that sum, not standing), leaves what is
owed through the write-off's day at or above zero. through_append lets
the write-off's own two lines be read separately.

Both are listed in Audit.lean; just prove holds (standard axioms only, no
sorry).
2026-10-27 12:44:45 -06:00
swaits d5af44b9cb feat(arap): write off a bad debt in the engine (P4b S7)
An invoice that will not be collected comes off the receivables and is
deducted, the direct write-off method the owner chose (A-5; IRC
§166(a); Treas. Reg. §1.166-1(e), -2(a), -3(a); rules.md B-41).

Ledger::write_off posts one balanced entry, Dr 6990 Bad Debt Expense /
Cr 1200 Accounts Receivable, its receivable line tagged WriteOff so the
figures stay derived: what the invoice owes falls and what was written
off rises, and a reversal re-instates it without a new movement (D-49).
The amount is at most what is owed, and at most the least owed on any
day from the write-off's on: a later payment can't leave the invoice
owing less than nothing (B-38 INVARIANT 2). A partial write-off is
allowed, as §1.166-3(a) allows to the extent charged off. Cash-basis
books refuse it: an invoice never counted as income has no basis to
deduct (§1.166-1(e)). The bad_debts role (6990) is a control role, so
the account is created on demand and can be assigned like the others.

The same-year recovery D-49 records is a reversal of the write-off and
then a payment; a later year is a plain entry, and needs no movement.

Tests: half_paid_rest_written_off_is_written_off,
write_off_refused_on_cash_basis, write_off_above_outstanding_refused,
write_off_then_recovery_same_year.
2026-10-27 12:44:45 -06:00
swaits 002fb9f8fc docs(plan): S4.4 as built (P4b S4.4) 2026-10-27 12:23:35 -06:00
swaits d5b9e5cf99 feat(mcp): the create tools preview and confirm, and the party keeps its contact block (P4b S4.4)
An invoice is issued, not edited. Without `confirmed` the create tools now
preview: the posting (with the accounts' codes, which the live chart may
not hold yet), the number it will take, its due date, terms and early
discount, warnings, and for an invoice the DRAFT copy of the PDF, handed
over as a file. Nothing is written and no number is used up. With
`confirmed` the invoice posts, takes the number the preview showed, and
files the issued copy with itself in the same call: `receipt.attached` on
its entry plus the link to the invoice, its SHA-256 in the event (A-1,
A-7, B-40). The copy the customer received is therefore the copy in the
books, and `books.document`'s re-created page can never be mistaken for
it. The result also carries a `mailto` link for a human to send it; the
books never send (A-4).

A bill keeps what the plan says a bill is: the vendor's own invoice
number, the vendor's invoice filed on its entry as its evidence, and, in
books that have declared `documents_v2`, an account on every line, so no
bill can land in General Expenses by default any more (A-9, A-10, A-13).
A vendor number entered twice is refused unless the caller says why, and
the reason is journaled with the second bill (B-40 item 4). Books that
have not declared the feature keep recording bills exactly as before, so
the owner's books keep working between units (D-39, 8.0.4).

Terms are resolved in one place and in one order: the due date given,
else the terms given, else the customer's or vendor's own default, else
the books' `invoicing.default_terms`; a document given only a due date
stores no terms, so an old-shape invoice is still written byte for byte
as before (B-39, A-13).

D-55: a party carries the owner's full contact block (phone, contact
person, billing and shipping addresses, billing email). It rides the same
`party.updated` event behind `documents_v2`, and an invoice's bill-to is
snapshotted at issue with the billing address and billing email in place
of the general ones when the party has them — `PartySnapshot::from_party`
is the one place that rule lives, so a later edit cannot change what was
sent, and the next invoice takes today's details.

Tests: the wire tests are the new `crates/ledger-cli/tests/documents.rs`
(preview writes nothing, the DRAFT copy, the issued copy's SHA-256 equals
`books.get_receipt`'s, numbers run, a bill's file and terms, the duplicate
refusal, the line-account rules, the terms fallback, the `party_id` alias
and the ambiguity it refuses, the contact block's set and clear, and the
bill-to at issue); `format_upgrade.rs` gains
`documents_v2_refused_until_consent` and the cut-over test now refuses and
then makes a document carrying a new field (8.0.6 item 3); the store
tests are `the_contact_block_round_trips_and_replays` and
`an_invoice_keeps_the_billing_details_it_was_issued_with`, and
`books_written_under_older_rules_still_open` gains a hand-written
contact-block event (8.0.6 item 1).

Merge does not need the owner's go-ahead: the live books refuse a write
that needs `documents_v2` until he gives it, and take every other write as
before (8.0.6 item 6). No figure moved: S4 moves none.
2026-10-27 12:23:32 -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