feat(drafts): the drafts tools, the preview digest on every confirm, the digest tool (P4)
Accounting reason: a confirm must act on the thing that was previewed. Every tool with confirmed (post_entry, reverse_entry, run_schedules, dispose_asset, reconcile, close_period, create_invoice, create_bill, write_off_invoice, purge_tin, consolidate_duplicates, and the S6 settlement tools receive_payment and pay_bill, which gain confirmed and preview on a clone of the ledger) returns preview (SHA-256 of the canonical preview) and refuses a confirm whose preview is missing or stale (stale_preview, before anything is written). A draft's digest is its own state and the settings seq; an operation's is the tool, its arguments as sent, the settings seq and the journal head seq, so another session's write forces a re-preview (D-50). New tools books.list_drafts, books.set_draft_state, books.discard_draft, books.preview_entry; reports carry as_of_status (drafts open on or before their end, unreconciled accounts, settings problems) and take strict; the CPA package is refused while a draft dated in it is open unless allow_open_drafts gives a reason, journaled as a decision; books.digest takes since_seq (what was posted since a journal line, backdated entries included: defect 6) and lists zero_net_accounts, drafts_by_state and the reconciliation split. Needs attention gains 'draft awaiting receipt' beside 'draft not posted'; the drafts handbook is rewritten. Every over-the-wire harness confirms the way an agent does. Docs: mcp-interface, rules B-43, parity, testing (with the For-P12 paragraph), architecture, and the unit's As-built block. Tests: drafts.rs (a_draft_waits_for_its_receipt_and_posts, the_gate_moved_and_the_mcp_error_is_unchanged, a_reversal_needs_no_receipt_of_its_own, discard_unblocks_the_close_over_the_wire, report_warns_on_draft_before_as_of, cpa_package_refuses_open_draft, stale_preview_refused, every_confirm_tool_takes_a_preview, digest_since_seq_is_posting_time, legacy_reversed_replays_identically, gui_and_server_gate_agree), attention_parity.rs, format_upgrade.rs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
7299cf90f2
commit
6a66a2855b
49 files changed
+2526
-202
No files matched your search
+17
-1
@@ -1479,7 +1479,23 @@ Each unit follows the 8.0.4 template. "A-n" is an owner answer numbered in 8.3.1
|
||||
5. **Files:** owns `crates/ledger-core/src/evidence.rs`, `crates/ledger-mcp/src/tools/drafts.rs`, `crates/ledger-cli/tests/drafts.rs`, `handbook/drafts-and-reversals.md`; touches `journal.rs`, `ledger.rs`, `store.rs`, every tool with `confirmed` (the sweep), `crates/ledger-gui/src/books.rs`, `crates/ledger-package/src/attention.rs`, the 8.0.3 files.
|
||||
6. **Tests and proofs:** named above; Lean `Step.discard`; no kernel change.
|
||||
7. **Done when:** (1) every tool with `confirmed` takes `preview` and refuses a stale one (`every_confirm_tool_takes_a_preview`, `stale_preview_refused`); (2) one function decides evidence and the GUI's string match is gone (`gui_and_server_gate_agree`); (3) reversals still post at once with no receipt of their own (`a_reversal_needs_no_receipt_of_its_own`); (4) the drafts tools and the digest fields exist and are documented; (5) 8.0.6 in the PR (a `figures.json` from the pre-P4 version over a fixture holding reversals and drafts: no figure moves); (6) `just check`, `just prove` green.
|
||||
8. **As built:** none yet.
|
||||
8. **As built (2026-10-29):** departures from Build, each with its reason.
|
||||
- **`crates/ledger-core/src/evidence.rs` was not new.** P6 created it (annotations, settlements); `Policy`, `Requirement`, `Origin` and `requirement` were appended to it.
|
||||
- **Schema 19 is a table, not a column.** Drafts are not rows of `entries` (they never touch the projection until posted), so `entries.draft_state` had nowhere to live: a `draft_states(entry_id, state, note)` table holds the drafts awaiting evidence (a draft being built is the default and has no row), and `entries.posted_seq` is a column. The number 19 is the next after the current last (18); the EM renumbers at merge.
|
||||
- **`Store::post_entry` stays the primitive; the gate is `Store::post_entry_checked`.** About 176 call sites in tests, fixtures, the demo and bookspec post hand entries over 25.00 with no receipt through `Store::post_entry`; a gate there would refuse all of them. The server posts a hand draft only through `post_entry_checked`, which calls `evidence::requirement` and refuses with `StoreError::ReceiptRequired`; the MCP maps it to the unchanged `receipt_required` error (`the_gate_moved_and_the_mcp_error_is_unchanged`).
|
||||
- **Payouts are not exempt.** The unit lists payouts among the exempt origins; `books.record_payout` makes an ordinary draft whose statement is attached as its receipt (its own description says so), and the viewer has always covered it, so it stays covered (no behaviour change). The exempt origins are exactly the ones the viewer's journal-event reading exempted: a close, a schedule month, a depreciation run, a disposal, a document's entry (any line tagged with a document), a payroll run or remittance, a reversal. The ledger learned the two it could not see (a disposal, a remittance) from `asset.disposed` and `payroll.taxes_remitted` (`Origin`), live and on replay.
|
||||
- **A split group counts for a draft only.** `requirement` applies the anti-splitting rule to drafts (as the post always did) and not to posted entries, whose decision was made when they posted; otherwise the viewer would flag legitimately posted neighbours.
|
||||
- **`posted_seq` is stamped for every posting, not only `entry.posted`/`entry.reversed`**: a close (`period.closed`) and a schedule month (`schedule.posted`) post an entry too. `Ledger::expect_posted_seq` (set when the store journals `entry.posted`/`entry.reversed`, before the command that posts) and `stamp_unstamped` (after each replayed line, and after the two live single-line commands) make live equal replay (`posted_seq_is_the_posting_lines_seq`).
|
||||
- **The preview digest is taken over the arguments as the caller sent them** (a thread-local set in `call_tool`, `ARGS`), not over re-serialized parameters (the params structs are `Deserialize` only); `Preview::of`/`check` and `BooksServer::preview_of` live in `tools/helpers.rs`, not `server.rs`.
|
||||
- **`confirmed` was added to `books.receive_payment` and `books.pay_bill`** (S6 deferred it): without it a preview on a clone of the ledger (entry 0), with it the post; their scope rows are now preview-then-post. `books.void_document`, `books.run_depreciation` and `books.run_recurring` have no `confirmed` today (the unit's list names them, the grep does not) and post at once as before; not changed. `InvoiceOut.preview`/`BillOut.preview` changed from `true` to the digest.
|
||||
- **The digest tool**: `since` became optional beside `since_seq`; `books.digest`'s `receipt_required` is now the engine's decision, so a document's, a close's or a depreciation run's entry no longer reads "receipt missing".
|
||||
- **Reports**: `as_of_status` and `strict` are on `books.trial_balance`, `books.profit_and_loss`, `books.balance_sheet`, `books.cash_flow` and `books.profit_and_loss_by_class`.
|
||||
- **`books.export_cpa_package` journals a decision** when `allow_open_drafts` is given (its doc said "read-only"); with open drafts and no reason it is refused with code `open_drafts`.
|
||||
- **A discarded draft's receipts stay filed** (retention, Pub. 583); a recurring draft's occurrence stays generated. Not unlinked.
|
||||
- **Harness**: every over-the-wire test harness confirms the way an agent does (`common::tools_call`, `Wire::call`, the async `call`s: the call without `confirmed`, then with the `preview`); `Wire::call_raw` sends exactly what it is given.
|
||||
- **8.0.6**: `books_written_under_older_rules_still_open` hand-writes both events; fixture `old-books/p4_before` (writer `p4_before.rs`, run against 25b52580) with its `expected.json`; `books_p2_served_cut_over_whole` refuses `books.set_draft_state` and `books.discard_draft` before consent. No figure moved: `JournalEntry` serializes a building state as nothing, so no golden changed. The statement for the PR: merge does not need the owner's go-ahead; the live books refuse the two draft writes until he gives it.
|
||||
- **Lean**: `Step.discard` with three theorems in `Audit.lean`; no kernel change, no new Kani harness.
|
||||
- **Not done in this unit**: the viewer has no screenshot of the awaiting-evidence item (the item is tested through the parity test, `a_waiting_draft_and_a_building_draft_are_two_items_on_both_lists`).
|
||||
|
||||
**Struck from P4 (D-47):** ~~reversal as a draft (`entry.reversal_drafted`, `Step.draftReversal`, `evidence: link_original | {attachment_ids} | later`, "reversal draft needs evidence", `reversal_needs_evidence`, `inherited_evidence_posts`)~~; ~~`preview_token` as an HMAC with `keys/preview.key`~~ → D-50; ~~`ledger_core::evidence::requirement` used by "every path" including the at-once tools~~ → the at-once tools are unchanged (IZ-1h); ~~"defect 2"~~ → the 2026-10-16 audit's defect 2 (a reversal posted with no evidence of its own) is closed by D-47 and D-12, not by code.
|
||||
|
||||
|
||||
Reference in new issue
Block a user