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:
swaitsandClaude Fable 5.1 committed 2026-10-29 13:55:38 -06:00
1 parent 7299cf90f2
commit 6a66a2855b
49 files changed
+2526 -202

No files matched your search

+16 -5
View File
@@ -56,7 +56,7 @@ const C: Needs = PostWhenConfirmed;
const K: Needs = PostForApprovalRules;
/// Every tool and what it needs (INVARIANT 1).
pub const TABLE: [(&str, Needs); 129] = [
pub const TABLE: [(&str, Needs); 133] = [
("books.handbook", R),
("books.start", R),
// Settings are committed at once: the settings commit of the plan.
@@ -163,8 +163,8 @@ pub const TABLE: [(&str, Needs); 129] = [
// These preview, then post when confirmed (P4b S4.4, A-1).
("books.create_invoice", C),
("books.create_bill", C),
("books.receive_payment", P),
("books.pay_bill", P),
("books.receive_payment", C),
("books.pay_bill", C),
("books.void_document", P),
// Records a send: changes the books' record, never a figure.
("books.mark_sent", W),
@@ -196,6 +196,11 @@ pub const TABLE: [(&str, Needs); 129] = [
("books.annotate_attachment", W),
("books.set_entry_settles", W),
("books.consolidate_duplicates", C),
// P4: drafts (plan P4; a discard previews, then acts).
("books.list_drafts", R),
("books.set_draft_state", W),
("books.discard_draft", C),
("books.preview_entry", R),
];
/// What `tool` needs, given its arguments; None for a tool this server
@@ -381,12 +386,18 @@ mod tests {
);
assert_eq!(
needs(
"books.receive_payment",
"books.void_document",
args(json!({"confirmed": false})).as_ref()
),
Some(Scope::Post),
"a tool that posts at once needs books:post whatever it is given"
);
// P4: a payment previews, then posts when confirmed.
assert_eq!(needs("books.receive_payment", None), Some(Scope::Write));
assert_eq!(
needs("books.pay_bill", args(json!({"confirmed": true})).as_ref()),
Some(Scope::Post)
);
assert_eq!(needs("books.access_log", None), Some(Scope::Admin));
assert_eq!(needs("books.nonesuch", None), None);
// INVARIANT 4.
@@ -435,7 +446,7 @@ mod tests {
shown("books.create_invoice", &write),
"it may preview an invoice"
);
assert!(!shown("books.receive_payment", &write));
assert!(!shown("books.void_document", &write));
let post = set(&[Scope::Post]);
assert!(shown("books.post_entry", &post));
assert!(!shown("books.chart", &post));