feat(evidence): one function decides whether an entry needs evidence (P4)

Accounting reason: the receipt rule (Pub. 583, Rev. Proc. 98-25: the
books must be demonstrably supported by the records) was decided in
three places that could disagree: the server's post, the digest, and a
string match on a journal-event origin in the viewer. ledger_core::
evidence::requirement is now the one decision: Required (with why) or
Exempt (with the source). A reversal needs no receipt of its own (its
evidence is its original's, D-47, D-12); neither do a closing entry, a
schedule month's or a depreciation run's posting, a disposal, a
document's entry, a payroll run or its remittance. The ledger learned
the two origins it could not see (a disposal, a remittance) from the
events that already name their entry, live and on replay. A split group
counts for a draft only: a posted entry's decision was made when it
posted.

The policy moved with it (evidence::Policy; ReceiptPolicy in
ledger-config keeps the words and delegates). Store::post_entry_checked
refuses a covered draft with no receipt (StoreError::ReceiptRequired,
which the MCP maps to the unchanged receipt_required error);
Store::post_entry stays the primitive that commands and fixtures post
through, since ~176 call sites post hand entries with no receipt. The
viewer's missing_receipt calls requirement; its string match is gone.

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:17 -06:00
1 parent 28d46332e4
commit 7299cf90f2
6 files changed
+451 -132

No files matched your search

+18
View File
@@ -35,6 +35,24 @@ pub enum StoreError {
#[error("asset {0} already has depreciation posted; correct it with entries instead")]
AssetDepreciationPosted(u64),
/// The receipt policy covers the entry and it has no receipt (plan P4,
/// I-5; rules.md B-43): refused before anything is journaled.
#[error(
"entry {entry_id} needs a receipt before it posts: {reason} ({policy}); attach one with books.attach_receipt"
)]
ReceiptRequired {
/// The entry that needs the receipt.
entry_id: u64,
/// Its total debits, `None` when they pass exact arithmetic.
debits: Option<rust_decimal::Decimal>,
/// The policy that triggered, in words.
policy: String,
/// Why this entry is covered.
reason: String,
/// The posted entries it appears to split (anti-splitting).
split_group: Vec<u64>,
},
/// Filesystem error.
#[error("io error: {0}")]
Io(#[from] std::io::Error),