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
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-28 21:21:21 -06:00
commit 3a3f03d884
44 files changed
+1847 -30

No files matched your search

+1
View File
@@ -169,6 +169,7 @@ groups are:
- `documents` — invoices, bills, payments, voids, `books.documents`,
`books.document`, the control check and the agings;
- `sent` — `books.mark_sent`, the record that an invoice went out (P4b S5);
- `entries` — `books.draft_entry`, `books.add_line`, `books.post_entry`,
`books.reverse_entry`, `books.get_entry`, `books.search_entries`, the
bank CSV import;
+21 -3
View File
@@ -2107,9 +2107,24 @@ snapshots; see plan 8.2 S4.
number and reads `void`. Refused while a payment stands on it (the
refusal names the payment entry to reverse first), when void already,
before its date or in a closed period (P4b S1, B-38).
- `books.mark_sent { invoice, on?, by?, via?, recipients?: [string],
kind?, reference?, note? }` → `{ invoice, sends: [{ on, by, via, to, kind,
reference?, note? }] }`. Records that an invoice went out (P4b S5,
A-4): the books never send, so the owner or a delegate sends and this
records who (`by`: the token's name unless given), how (`via`: email
the default, or mail, portal, hand, another channel's name), to whom
(`recipients`: the invoice's bill-to email, else the customer's, else
required), when (`on`: today in the company's time zone), original or
reminder (`kind`: original on the first send, reminder after), and the
channel's own id (`reference`: a message id). `invoice` is
`invoice:N` or `N`. Several sends are allowed; none moves a figure or
the due date. Refused on a void invoice, dated before it, with no
recipients, or while the books wait for `documents_v2`
(`format_upgrade_pending`). `sends` is every send on the invoice, in
order, the new one last.
- `books.documents { kind, status?, party?, due_by?, as_of? }` → `{ as_of,
kind, documents: [{ ref, party, date, due_date, total, outstanding,
status, overdue_days }], outstanding_total }`. Invoices (`kind`
status, overdue_days, last_sent? }], outstanding_total }`. Invoices (`kind`
`invoices`) or bills (`bills`), due date first, every figure derived
from the document's lines that stand (B-38). `status` filters: `open`
(default: anything still owed, partly paid and overdue included),
@@ -2117,11 +2132,14 @@ snapshots; see plan 8.2 S4.
own `status` is one of `open`, `partial`, `paid`, `written_off`,
`void`, `overpaid` or `unattached` (`DocStatus::key()`). `party` (a
customer's or vendor's name, ignoring case) and `due_by` (due on or
before that day) narrow the list (P4b S2).
before that day) narrow the list (P4b S2). `last_sent` is the day the
invoice last went out, when a send is recorded (P4b S5); absent for an
invoice never sent and for every bill.
- `books.document { ref, as_of?, pdf? }` → `{ ref, party, date, due_date,
entry_id, lines: [{ description, amount, project? }], total, paid,
discounted, written_off, outstanding, status, overdue_days, timeline:
[{ date, entry_id, kind, amount, reversal, stands }], pdf? }`. `ref` is
[{ date, entry_id, kind, amount, reversal, stands }], sends: [{ on, by,
via, to, kind, reference?, note? }], pdf? }`. `ref` is
`invoice:N` or `bill:N`; `status` as `books.documents`' rows; the
timeline is every ledger line tagged with the document, dated on or
before the day, in posting order, each saying whether its entry still
+16
View File
@@ -45,6 +45,7 @@ No other statuses.
| 1099 vendor tracking + year-end worksheet | Vendors → Track payments for 1099; 1099 report | partial | hand-verified fixture (bill payments, card exclusion, reversal, year boundary); no golden file yet |
| Prepaid expense + deferred revenue amortization | Amortization schedules (Xero/QBO add-ons) | partial | hand-verified fixture, to the cent; no golden file yet |
| Automated cross-implementation diff harness (GnuCash first) | — (`just oracle`) | partial | live: `tests/oracle_gnucash.rs` runs 200 random books through GnuCash 5.10 in a pinned container, 15,800 exact comparisons, weekly in CI (docs/oracle.md Layer 5); open: fixture books diffed as `snapshot()` output, and the machine-readable catalog with CI enforcing this floor |
| Invoice sent tracking (who, how, to whom, when, original or reminder, the channel's reference) | Business → Invoices (Xero's "Sent"; QBO's email) | done | P4b S5; records, never sends (A-4); a send changes no figure; `invoice.sent` under `documents_v2` (D-52) |
Workflow/UI parity (QuickBooks/Xero reference) gets its own catalog when the
egui phase starts; semantics come first.
@@ -135,3 +136,18 @@ door on loopback for Caddy or a tunnel. `crates/ledger-cli/tests/two_user.rs`
proves the account boundary and the move's head in CI
(`.github/workflows/two-user.yml`); the recipe is `deploy/README.md`.
The cutover rule in `docs/capability-parity.md` is unchanged.
### P4b S5: sent tracking (2026-10-27)
The catalog gains one row: an invoice's recorded sends. The books never
send (A-4): `books.mark_sent` records who sent an invoice, how, to whom,
when, whether it was the original or a reminder, and the channel's own
reference, for an owner or a delegate such as an agent. The record is
`invoice.sent`, under `documents_v2` (D-52), and it moves no figure, so
the A/R and A/P rows' figures are unchanged. The floor never decreases:
the row is `done`, tested by `sends_replay_and_project`,
`a_send_changes_no_figure` and `mark_sent_records_who_and_how`. Sent
tracking also feeds Needs attention: an issued invoice nobody marked sent
(past `invoicing.unsent_days`) and an overdue invoice not reminded within
`invoicing.reminder_days` are one list in the viewer and in `books.start`,
each offering `books.mark_sent` (step 4; U0b's one engine).
+4 -2
View File
@@ -58,8 +58,10 @@ apply is safe — on startup the applier replays every journal line with
- `ts`: UTC, RFC 3339. Recorded, never trusted for ordering (`seq` orders).
- `type`: `account.added`, `entry.posted`, `entry.reversed`, `invoice.created`,
`bill.created`, `receipt.attached`, `receipt.linked`, `receipt.unlinked`,
`document.filed`, `attachment.withdrawn` (plus the A/R, A/P, asset,
period, payroll, and class events listed on `JournalLine`).
`document.filed`, `attachment.withdrawn`, `invoice.sent` (a recorded
send: a document's record, never a ledger line or a figure; P4b S5)
(plus the A/R, A/P, asset, period, payroll, and class events listed on
`JournalLine`).
- `payload`: the full domain facts (account fields, entry with lines, blob
sha, ...). Everything the applier needs; nothing it must look up elsewhere.
An `invoice.created` or `bill.created` written after the `documents_v2`
+5 -2
View File
@@ -1746,8 +1746,11 @@ person, a billing or shipping address, a billing email (D-55); an
`invoice.created` or `bill.created`
carrying a number, terms, a bill-to or seller snapshot, a customer
reference or memo, a vendor reference, a receipt date or a duplicate's
reason, or a line naming its account, quantity or unit price (S4.3); and
S5 to S7's sends, settlements and write-offs. An older version would
reason, or a line naming its account, quantity or unit price (S4.3);
S5's `invoice.sent` (who sent an invoice, how, to whom and when: a record
that moves no figure); S6's payments with `line`, `discount`,
`discount_line`, `discount_reason`, `fee` or `fee_line`; and S7's
`invoice.written_off`. An older version would
restore the document without them silently: every invoice line would post
to the revenue role and every bill line to general expenses, and every new
event type would be refused. A command asks for
+64
View File
@@ -3050,3 +3050,67 @@ and `BooksServer::new`'s copy (a dropped field must fail
`isolation_label_is_enforced_under_the_unit`); the host's `server_flags`
extension and `parse_args`' passthrough of `--isolation` and its optional
`--http` value.
## P4b S5: sent tracking (2026-10-27)
The books record that an invoice went out; they never send it (A-4). A
send is `invoice.sent`, under `documents_v2` (D-52), and moves no figure:
`Ledger::mark_sent` pushes a `Sent` onto the invoice, the store journals
one line and no entry, replay restores it and the projection keeps it in
`schema` v17's `invoice_sends` (S4.3 took 16).
- **`ledger-store` `tests/arap_standing.rs`:** `mark_sent_waits_for_documents_v2`
(books with no features: refused, nothing journaled; after `upgrade`,
taken, and the reopen equals what was live), `sends_replay_and_project`
(two sends: the journal's `invoice.sent` carries the whole record in
order, `verify` passes, the reopened invoice holds both),
`a_send_changes_no_figure` (on the `p4-arap-before` fixture: the report
snapshot, the invoice's `DocState` and every derived cache are equal
before and after), `mark_sent_on_void_refused` (`DocumentVoid`,
nothing journaled; a send dated before the void is taken).
- **`ledger-store` `tests/arap_props.rs`:** `status_is_a_function_of_journal_and_date`
gains `Op::MarkSent` (random sends on invoices, skipped on bills): every
figure, the control check and both agings still agree across live,
replay and snapshot.
- **`ledger-store` `tests/format.rs`:** `an_older_version_stops_at_invoice_sent`
(`invoice.sent` is this version's, a reader without `documents_v2` waits,
and the books declare the feature before the first send line).
- **8.0.6:** `books_written_under_older_rules_still_open` hand-writes an
`invoice.sent` and still opens; the fixture `old-books/p4_s5_before`
was written by eeee9658 (the revision before this unit) and opens to
the same figures; `books_p2_served_cut_over_whole` refuses a send before
consent and takes it after.
- **`ledger-cli` `tests/documents.rs`:** `mark_sent_records_who_and_how`
(two sends over the wire: the token's name, email and original by
default, then a reminder with a message id; `books.documents` shows
`last_sent`, `books.document` both sends, and the invoice still owes its
total).
- **`ledger-mcp/src/handbook.rs`:** the new `invoices-and-bills` section
renders, and its id is unique (the existing handbook tests).
- **Step 4, Attention (R-F):** `ledger-gui/tests/invoices.rs`:
`an_unsent_invoice_is_listed_until_a_send_clears_it` (an issued invoice
past `invoicing.unsent_days` is one to-do opening the invoice; the
overview draws it, `attention-unsent-invoice.png`; a send clears it),
`a_legacy_invoice_with_no_number_is_never_listed_as_unsent` (exempt),
`an_overdue_invoice_with_an_old_send_is_asked_to_remind` (due Mar 1,
last sent Feb 20: 14 days overdue, target the invoice, prompt names
`books.mark_sent`). `ledger-cli/tests/attention_parity.rs`:
`an_unsent_invoice_is_on_both_lists` (the viewer's list and `books.start`
both carry the item, and the step is `books.mark_sent` on `invoice:1`).
`the_viewer_and_books_start_see_the_same_attention` stays green.
**For P12 (mutation targets):** `Ledger::mark_sent`'s refusals (unknown
invoice, a send dated before it, `DocumentVoid`, empty `to`, and each of
the three `check_text` calls; the note's check is stricter than the plan's
list, to keep a full TIN out of the journal); `SentVia::parse`'s arms and
its `other:` prefix; `SendKind::parse`'s two arms; `sent_via`'s arms and
its empty fallback; `send_kind`'s error arm; the handler's `kind` default
(`original` when `sends` is empty, `reminder` after), its `to` fallback
(bill-to, then party, then refused) and its `by` fallback; `invoice_id`'s
`invoice:` strip; `last_sent`'s `max`; the replay reader's
`newer_content` arms for an unknown channel or kind; and the two attention
predicates in `ledger-package::attention`: `invoice.number.is_some()`
(the legacy exemption), `Open | Partial`, `sends.is_empty()`, the
`checked_add_days` `unsent_days` comparison (`>`), `overdue_days > 0`, the
`sends.max()` last send, and the `reminder_days` comparison (`>`), plus
each item's target (`Target::Invoice`) and `books.mark_sent` step args.
+18
View File
@@ -542,6 +542,24 @@ come from the kernel:
No Lean model: these are two arithmetic laws, not a new `Ledger`
command (S4.1 adds fields to the existing document commands).
### Sent tracking (P4b S5)
34. **A send changes no figure.** `Ledger::mark_sent` is a command, so it
is a `Step` in the model (AGENTS.md's formal-methods bar; the unit's
"no Lean change" phrase was overruled by it, and says so in its As
built block). `Documents.lean`'s `Books` gains `sends`, and
`Step.markSent` records one on a document the lines carry, touching
no line. Every control balance, every document's sum and what is
unattached are therefore the same before and after, through any day,
and `Reachable.tagged` (the control lines are all documents') covers
the new step; the engine's side is the same statement, checked by
`a_send_changes_no_figure` on the fixture book.
- Lean `Documents.Step.markSent_changes_no_figure` (no axioms) ·
`Documents.Reachable.tagged`, `Documents.Reachable.control_is_documents`
cover the new step · tests `sends_replay_and_project`,
`a_send_changes_no_figure`, `mark_sent_on_void_refused`,
`mark_sent_waits_for_documents_v2`
## What the proofs found
The engine already had 190+ tests, property tests, mutation testing,