The defect found merging S7 and P6: under P6's dedup the notice's
bytes resolve to the record already on file, and the one-receipt-one-
entry rule refused it only after the write-off had journaled its
entry. The store now checks the deduplicated record before the journal
line (`6c26160e`'s companion fix in `write_off_invoice`); this test
pins it: the refusal names the record and its entry, the head and the
journal are unchanged, no entry posts, the invoice stays open, and the
books verify. Also formats the B-27 test `ci-fixes` brought in.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
P6's branch paused with its gate interrupted, so five tests that
predate plan P6 step 4 (the same bytes filed twice are one attachment,
linked to each subject) still encoded the invariant it replaced, and
one golden carried counts from before extraction records existed.
- store: `blob_dedup_and_hash_verification` expects one attachment
row under the first filing's name, not two. The `attach_receipt`
spec block's INVARIANT 2/3 now say what P6 built; the stale "each
filing is its own attachment" was the one wording left.
- store: the power-loss replay copies the blob store too. The books
on disk were always journal, database and blobs; verify now reads
an extraction's text blob (P6 step 2), so a copy without `blobs/`
cannot verify.
- store: the refusals property allows `AttachReceipt` and
`FileDocument` to journal nothing when the bytes are already on
file: the record they return is the record they were.
- viewer: each statement the reconciliation helper files has its own
bytes (a wrong statement is voided and a new one comes); identical
bytes would be one record, and a record is one statement.
- viewer: the tall sidebar's shapes golden is re-blessed. The fixture
books now carry extraction records, so the Overview attention
count, the event count and the verified-file count the sidebar
prints moved; no layout changed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
Under P6's dedup (one record per bytes) the adapter's constant letter
made the example's second write-off of the same invoice reuse the first
write-off's record, which a receipt's one-entry rule (B-38) refuses, and
the write-off now refuses that before anything is journaled (the S7/P6
integration fix). The example's two write-offs have two notices, which
is what it describes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
The last of the paused branches, from its "not for merge" checkpoint:
extraction in ledger-import (text, pages, page images; capped, caught),
journaled as attachment.text_extracted; dedup by hash with
books.find_attachment and books.consolidate_duplicates; annotations and
books.set_entry_settles; the company record categories; the two fuzz
targets; feature attachment_text. Nineteen files conflicted with the
four P4b merges, resolved as plan 8.0.2 says: P6 retakes schema 18 (it
had taken 16, which S4.3 holds; S5 holds 17); the tool table is 129 and
the destructive count 31; both sides' appends kept with their seams
restored; one B-27 paragraph naming every feature; in the old-books
fixture P6's hand-written reimbursement is entry 7 (its ids collided
with S4.3's invoice); the replay fuzzer's list gains S7's
invoice.written_off; S4.3's issued-PDF test expects the extraction
record P6 journals right after a filing.
One integration defect between S7 and P6, found by the arap property
test and fixed here: under P6's dedup, a write-off whose evidence bytes
were filed already (the same collection letter for a second write-off of
the same invoice) was refused only after its entry had been journaled,
at the final link to the invoice ("already linked"). The write-off now
resolves the dedup before anything is journaled and keeps a link the
record already has to the invoice instead of refusing it; an explicit
existing_attachment_id already on an entry is still refused (S7's
one-receipt-once rule). P6's gate never ran before the pause; it runs on
main after this merge, and its ledger-store -> ledger-import dependency
is the open review point (plan 8.0.1).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
The S7 merge kept both branches' doc comments for the Books structure,
S5's (which names the sends) stranded above an unrelated theorem and
S7's on the structure; Lean refused the stray one. S5's text stays, on
the structure it describes. Kani's 52 harnesses verified in the same
run; only Lean's build stopped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
S7 added the event type (R-B step 6: the fuzzer's list must name every
type the store replays, the_replay_fuzzer_writes_every_event_type) and
the merge lost the line; the S7 gate on main caught it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
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
The weekly run's fuzz_settings_replay job failed to build since P3 gave
each projection schema its own file: the target still named
ledger_store::DB_FILE, which became db_file(). The fuzz crate is outside
the workspace, so just check never compiled it; every target builds
again (cargo fuzz build, 2026-10-29).
The same run's fuzz_store_replay crash (the writer refusing a journal
with an unknown convention kind as newer while the viewer said something
else) is not reproduced yet: the hand-made case, kept here as
an_unknown_convention_kind_is_newer_for_the_writer_and_the_viewer_alike,
passes, so the divergence needs the fuzzer's exact journal, which the
weekly job now keeps as an artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
The 2026-10-26 weekly run found a fuzz_store_replay crash (the writer
and the viewer disagreeing on a journal with an unknown convention kind)
and uploaded nothing, so the input is being hunted again locally. A
failing fuzz job now uploads fuzz/artifacts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
The owner, 2026-10-29: mutation testing is for local runs, not CI. The
weekly workflow's mutation shards (core, store, import, config, MCP) are
removed; they failed every week on the survivors and timeouts D-33
deferred to P12, and their red hid the run's real findings (a
fuzz_store_replay crash and a fuzz_settings_replay build break, both
being fixed). just mutants-* stays for the desktop; P12 runs the pass
whole. Recorded as D-59; the docs that said the shards keep running are
corrected.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
Every run since P9a landed failed at the first adoption with
"/tmp/fixture: Permission denied": the copies were owned by the runner
(cp -a keeps the checkout's ownership) and the server account, which
adopts them, could not take the source's lock. Both copies are now owned
by the server account; the agent account keeps read access to the
source's journal, which the test compares the moved books' head with.
P9a's Done-when 1 (the job's first green run) is checked on this push.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
S5 and S6 each raised the journey test's expected count from 122 to 123
on the same line, so the two merges folded into one bump; with
books.mark_sent and books.bills_due both present the table is 124. The
S7 merge raises it to 125 and P6's to 129.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
The last of the paused branches, from its "not for merge" checkpoint:
extraction in ledger-import (text, pages, page images; capped, caught),
journaled as attachment.text_extracted; dedup by hash with
books.find_attachment and books.consolidate_duplicates; annotations and
books.set_entry_settles; the company record categories; the two fuzz
targets; feature attachment_text. Nineteen files conflicted with the
four P4b merges, resolved as plan 8.0.2 says: P6 retakes schema 18 (it
had taken 16, which S4.3 holds; S5 holds 17); the tool table is 129 and
the destructive count 31; both sides' appends kept with their seams
restored; one B-27 paragraph naming every feature; in the old-books
fixture P6's hand-written reimbursement is entry 7 (its ids collided
with S4.3's invoice); the replay fuzzer's list gains S7's
invoice.written_off; S4.3's issued-PDF test expects the extraction
record P6 journals right after a filing.
One integration defect between S7 and P6, found by the arap property
test and fixed here: under P6's dedup, a write-off whose evidence bytes
were filed already (the same collection letter for a second write-off of
the same invoice) was refused only after its entry had been journaled,
at the final link to the invoice ("already linked"). The write-off now
resolves the dedup before anything is journaled and keeps a link the
record already has to the invoice instead of refusing it; an explicit
existing_attachment_id already on an entry is still refused (S7's
one-receipt-once rule). P6's gate never ran before the pause; it runs on
main after this merge, and its ledger-store -> ledger-import dependency
is the open review point (plan 8.0.1).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
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
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
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
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
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
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
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.
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.
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.
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.
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.
`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.
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.
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.
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.
`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.
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.
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.
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.
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).
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.
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.
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.
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).
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.
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.
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.
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.
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.
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).
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.