feat(drafts): a draft has a state and can be discarded (P4)

Accounting reason: an entry that is finished but waits on its receipt is
a different thing from one still being built, and a draft that will
never post must have a way out, or the period that holds it open can
never close (the Lean hypothesis hdrafts). JournalEntry gains
DraftState { Building, AwaitingEvidence { note } }; Ledger gains
set_draft_state (a note: no line, balance or report moves),
discard_draft (a draft only; a posted entry is never removed, B-8; ids
are never reused; what was recorded about the draft goes with it) and
posted_seq (the journal line that posted an entry, for the digest's
since_seq). Lean Step.discard with its preservation proof and three
theorems (a discard moves no figure, removes only its draft, and
unblocks a close), listed in Audit.lean.

Tests: discard_unblocks_close (ledger-core invariants).

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 75882c5e07
commit be8bcf340e
8 files changed
+347 -1

No files matched your search

+68
View File
@@ -23,6 +23,55 @@ pub enum EntryStatus {
Posted,
}
/// Where a draft entry stands (plan P4, `drafts_v2`): still being built,
/// or finished and waiting on the document that supports it (rules.md
/// B-43). Meaningful while the entry is a draft; a posted entry keeps
/// the state it had and nothing reads it.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum DraftState {
/// Lines are still being added. What every draft starts as.
#[default]
Building,
/// The lines are final and the entry waits for its receipt or other
/// evidence; `note` says what is awaited (empty: nothing said).
AwaitingEvidence {
/// What the draft waits on, in the words of whoever set the state.
note: String,
},
}
impl DraftState {
/// Its key as the journal, the tools and the viewer name it:
/// `building` or `awaiting_evidence`.
pub fn key(&self) -> &'static str {
match self {
DraftState::Building => "building",
DraftState::AwaitingEvidence { .. } => "awaiting_evidence",
}
}
/// [`Self::key`] the other way, with the note an awaiting state
/// carries; `None` for a state this version doesn't know (a newer
/// version's).
pub fn parse(key: &str, note: &str) -> Option<DraftState> {
match key {
"building" => Some(DraftState::Building),
"awaiting_evidence" => Some(DraftState::AwaitingEvidence {
note: note.to_string(),
}),
_ => None,
}
}
/// The note an awaiting state carries; empty for building.
pub fn note(&self) -> &str {
match self {
DraftState::Building => "",
DraftState::AwaitingEvidence { note } => note,
}
}
}
/// One debit or credit line of a journal entry.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct JournalLine {
@@ -59,6 +108,10 @@ pub struct JournalEntry {
lines: Vec<JournalLine>,
status: EntryStatus,
reversal_of: Option<EntryId>,
/// Where the draft stands (`entry.draft_state_set`); meaningful while
/// `status` is `Draft`.
#[serde(default)]
draft_state: DraftState,
}
impl JournalEntry {
@@ -72,6 +125,7 @@ impl JournalEntry {
lines: Vec::new(),
status: EntryStatus::Draft,
reversal_of: None,
draft_state: DraftState::Building,
}
}
@@ -100,6 +154,12 @@ impl JournalEntry {
self.status
}
/// Where a draft stands (plan P4): building, or awaiting evidence.
/// Meaningful while the entry is a draft.
pub fn draft_state(&self) -> &DraftState {
&self.draft_state
}
/// If this entry reverses another, the reversed entry's id.
pub fn reversal_of(&self) -> Option<EntryId> {
self.reversal_of
@@ -116,9 +176,17 @@ impl JournalEntry {
self.lines[index].class_id = class_id;
}
/// Set the draft's state. Only while drafted; only via
/// [`crate::Ledger::set_draft_state`].
pub(crate) fn set_draft_state(&mut self, state: DraftState) {
self.draft_state = state;
}
/// Mark posted. Only via [`crate::Ledger`], after [`Self::validate`].
pub(crate) fn mark_posted(&mut self) {
self.status = EntryStatus::Posted;
// A posted entry waits on nothing: its state is the canonical one.
self.draft_state = DraftState::Building;
}
/// Link this entry as the reversal of another. Only via [`crate::Ledger`].