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

+7 -8
View File
@@ -705,14 +705,13 @@ impl Books {
.entry_vendor(id)
.and_then(|v| ledger.get_vendor(v))
.map(|v| v.name.clone());
// The receipt policy covers what is posted by hand (journal
// entries and recurring drafts); invoices, payments, closes,
// depreciation, and schedules carry their own evidence, and a
// reversal leans on its original's.
let missing_receipt = matches!(origin, "Journal entry" | "Recurring")
&& e.reversal_of().is_none()
&& rec.is_empty()
&& policy.receipt_required(&ledger, e);
// The engine decides (plan P4, I-5): the receipt policy covers
// what is posted by hand (journal entries, recurring drafts);
// invoices, payments, closes, depreciation, and schedules carry
// their own evidence, and a reversal leans on its original's.
// The viewer asks the same function the server's gate asks.
let missing_receipt = rec.is_empty()
&& ledger_core::evidence::requirement(&ledger, e, ledger.settings()).is_required();
let cash_effect = e
.lines()
.iter()