feat(store): draft states and discards are journaled, posted_seq is kept (P4)
Accounting reason: a draft that waits on its receipt, and one thrown
away, are facts about the books' working papers that must survive a
restart and replay identically, or a discarded draft would come back and
hold its period open. entry.draft_state_set and entry.draft_discarded are
drafts_v2's (an older version stops at the declaration instead of
misreading them); no figure moves. Schema 19 (taken at merge as the next
after the current last): entries.posted_seq, and a draft_states table
(drafts are not rows of entries, so the plan's entries.draft_state column
has nowhere to live). posted_seq is the journal line that posted each
entry (entry.posted, entry.reversed, the line that closed a period or
posted a schedule month), identical live and on replay.
8.0.6: a hand-written line of each new shape in
books_written_under_older_rules_still_open; fixture old-books/p4_before
written by 25b52580 (hand entries posted late, a reversal, a document, a
schedule month, a recurring draft, open drafts, a close) opens to the
same figures (serialized entries omit a building state, so no golden
moved); drafts_v2_is_its_own_feature; the fuzzer and GUI journal know the
types.
Tests: draft_states_replay_and_project, drafts_v2_waits_for_the_upgrade,
posted_seq_is_the_posting_lines_seq, an_older_version_stops_at_the_draft_events.
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
be8bcf340e
commit
28d46332e4
18 files changed
+1455
-24
No files matched your search
@@ -18,7 +18,7 @@
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::arap::{DocumentLine, SendKind, Sent, SentVia};
|
||||
use ledger_core::{AccountType, Currency, Money, Side};
|
||||
use ledger_store::format::{DOCUMENTS_V2, EVENT_TYPES, FORMAT_SET, KNOWN_FEATURES};
|
||||
use ledger_store::format::{DOCUMENTS_V2, DRAFTS_V2, EVENT_TYPES, FORMAT_SET, KNOWN_FEATURES};
|
||||
use ledger_store::journal::{canonical_json, sha256_hex};
|
||||
use ledger_store::{
|
||||
JOURNAL_FILE, JournalFormat, JournalWriter, SUPPORTED_JOURNAL_FORMAT, Snapshot, Store,
|
||||
@@ -1586,3 +1586,68 @@ fn an_unknown_convention_kind_is_newer_for_the_writer_and_the_viewer_alike() {
|
||||
(Err(e), Ok(_)) => panic!("the viewer loads books the writer refuses: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Done-when (plan P4): the two draft events are this version's, they need
|
||||
/// `drafts_v2`, and the books declare that feature before the first such
|
||||
/// line, so a version without it stops at the declaration and reports the
|
||||
/// books as newer, never corrupt, never keeping a discarded draft that
|
||||
/// would block a close (rules.md B-27, B-43).
|
||||
#[test]
|
||||
fn an_older_version_stops_at_the_draft_events() {
|
||||
assert!(EVENT_TYPES.contains(&"entry.draft_state_set"));
|
||||
assert!(EVENT_TYPES.contains(&"entry.draft_discarded"));
|
||||
let older = JournalFormat {
|
||||
format: SUPPORTED_JOURNAL_FORMAT,
|
||||
features: KNOWN_FEATURES
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|f| *f != DRAFTS_V2)
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
};
|
||||
assert!(
|
||||
matches!(
|
||||
older.require_feature(DRAFTS_V2),
|
||||
Err(StoreError::FormatUpgradePending { .. })
|
||||
),
|
||||
"a reader without drafts_v2 waits for it"
|
||||
);
|
||||
let dir = test_dir("drafts-gate");
|
||||
let mut store =
|
||||
Store::create_in_format(&dir, Currency::USD, &JournalFormat::default()).unwrap();
|
||||
let date = NaiveDate::from_ymd_opt(2026, 3, 1).unwrap();
|
||||
let draft = store.create_entry(date, "Waiting").unwrap();
|
||||
store.upgrade_format().unwrap().expect("the go-ahead");
|
||||
store
|
||||
.set_draft_state(
|
||||
draft,
|
||||
ledger_core::DraftState::AwaitingEvidence {
|
||||
note: "receipt".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
store.discard_draft(draft, "not needed").unwrap();
|
||||
let journal = std::fs::read_to_string(dir.join(JOURNAL_FILE)).unwrap();
|
||||
let mut declared = false;
|
||||
let mut seen = 0;
|
||||
for line in journal.lines() {
|
||||
let v: serde_json::Value = serde_json::from_str(line).unwrap();
|
||||
if v["type"] == FORMAT_SET
|
||||
&& v["payload"]["features"]
|
||||
.as_array()
|
||||
.is_some_and(|f| f.iter().any(|x| x == DRAFTS_V2))
|
||||
{
|
||||
declared = true;
|
||||
}
|
||||
if v["type"] == "entry.draft_state_set" || v["type"] == "entry.draft_discarded" {
|
||||
assert!(
|
||||
declared,
|
||||
"drafts_v2 must be declared before the draft events"
|
||||
);
|
||||
seen += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(seen, 2);
|
||||
drop(store);
|
||||
Store::open(&dir).unwrap().verify().unwrap();
|
||||
}
|
||||
Reference in new issue
Block a user