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
@@ -278,10 +278,39 @@ pub fn find_with(
|
||||
};
|
||||
let money = ledger_core::parties::money_accounts(ledger, &money_cash, &financing);
|
||||
let members = settings.company.party_members(ledger);
|
||||
let entries = entries(books, &journal, &policy);
|
||||
let entries = entries(books, &journal);
|
||||
let mut out = Vec::new();
|
||||
|
||||
// Two kinds of open draft (plan P4, R-F): one that says it waits for
|
||||
// its evidence, and one still being built.
|
||||
for e in entries.iter().filter(|e| e.status == EntryStatus::Draft) {
|
||||
if let Some(note) = &e.awaiting_evidence {
|
||||
let waits = if note.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(": {note}")
|
||||
};
|
||||
out.push(Attention {
|
||||
severity: Severity::Todo,
|
||||
kind: "draft_awaiting_evidence",
|
||||
title: format!("Draft #{} awaiting receipt: {}", e.id, e.description),
|
||||
detail: format!(
|
||||
"#{} · {} · {}. Waiting for its evidence{waits}. It appears in no report until posted.",
|
||||
e.id,
|
||||
date(e.date),
|
||||
dollars_or(e.amount)
|
||||
),
|
||||
target: Target::Entry(e.id),
|
||||
prompt: format!(
|
||||
"In wanderbooks, draft entry {} (\"{}\", {}) is waiting for its evidence{waits}. If the receipt has arrived, attach it, preview the entry (books.preview_entry) and post it; if it never will, say why and discard it (books.discard_draft).",
|
||||
e.id,
|
||||
e.description,
|
||||
dollars_or(e.amount)
|
||||
),
|
||||
tool: Some(("books.get_entry".into(), serde_json::json!({ "entry_id": e.id }))),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
out.push(Attention {
|
||||
severity: Severity::Todo,
|
||||
kind: "draft",
|
||||
@@ -1362,15 +1391,14 @@ struct Entry {
|
||||
parties: Vec<PartyId>,
|
||||
/// Covered by the receipt policy and has no receipt.
|
||||
missing_receipt: bool,
|
||||
/// A draft that says it is finished and waits for its evidence: the
|
||||
/// note it was given (plan P4).
|
||||
awaiting_evidence: Option<String>,
|
||||
}
|
||||
|
||||
/// Every entry, with the journal-derived facts the items read: its origin
|
||||
/// and whether the receipt policy covers it without a receipt.
|
||||
fn entries(
|
||||
books: &impl PackageBooks,
|
||||
journal: &[JournalLine],
|
||||
policy: &ledger_config::ReceiptPolicy,
|
||||
) -> Vec<Entry> {
|
||||
fn entries(books: &impl PackageBooks, journal: &[JournalLine]) -> Vec<Entry> {
|
||||
let ledger = books.ledger();
|
||||
let mut events: HashMap<EntryId, Vec<usize>> = HashMap::new();
|
||||
for (i, line) in journal.iter().enumerate() {
|
||||
@@ -1405,10 +1433,9 @@ fn entries(
|
||||
let evs = events.remove(&id).unwrap_or_default();
|
||||
let origin = origin(&evs, journal, closings.contains(&id));
|
||||
let amount = ledger_config::ReceiptPolicy::entry_debits(e).ok();
|
||||
let covered = matches!(origin, "Journal entry" | "Recurring")
|
||||
&& e.reversal_of().is_none()
|
||||
&& receipts.get(&id).copied().unwrap_or(0) == 0
|
||||
&& policy.receipt_required(ledger, e);
|
||||
// The engine decides (plan P4, I-5), as the server's gate does.
|
||||
let covered = receipts.get(&id).copied().unwrap_or(0) == 0
|
||||
&& ledger_core::evidence::requirement(ledger, e, ledger.settings()).is_required();
|
||||
out.push(Entry {
|
||||
id,
|
||||
date: e.date(),
|
||||
@@ -1418,6 +1445,14 @@ fn entries(
|
||||
origin,
|
||||
parties: ledger.entry_parties(id).iter().map(|p| p.party).collect(),
|
||||
missing_receipt: covered,
|
||||
awaiting_evidence: match e.draft_state() {
|
||||
ledger_core::DraftState::AwaitingEvidence { note }
|
||||
if e.status() == EntryStatus::Draft =>
|
||||
{
|
||||
Some(note.clone())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
});
|
||||
}
|
||||
out
|
||||
|
||||
Reference in new issue
Block a user