fix(store,core): a refused command never reaches the journal; reversal rules
Max-testing pass, step 2 (rank 1 regressions + a rank 2 law). Two ways
to make the books unopenable, both found by test, both fixed:
1. Reversing into a closed period: the store journaled `entry.reversed`,
then the engine refused it at post. Replay could never apply that
line, so Store::open failed ("period 1 is closed"). Regression:
reversal_into_a_closed_period_is_refused_before_journaling.
2. An oversized invoice (or any command whose hand-written store
pre-checks lagged the engine, here the new line range) journaled,
then failed in the engine. Found by the new property
refused_commands_never_reach_the_journal within 4s.
The fix is structural, not another hand-written check: every
journal-first store command now rehearses on a scratch clone of the
ledger (`Store::rehearse`), so whatever the engine refuses is refused
before the journal, now and after any future engine change. The
create_entry rehearsal uses the checked constructor: no draft is ever
dated in a closed period (the Lean invariant Inv.draftsOpen, which the
store had bypassed).
The property (1,000 random command sequences, heavy on refusals)
asserts, after every command: a refusal leaves the journal
byte-identical; every posted entry validates; the trial balance
balances; no draft sits in a closed period; and the books reopen and
verify at the end. Shown to have teeth: removing the create_entry
rehearsal makes it fail and shrink to open-period, close, draft.
Engine (spec confirmed by Stephen, 2026-10-13), Ledger::check_reversal,
a pure pre-check that reverse_entry and the store share:
- an entry is reversed at most once (EntryAlreadyReversed); reversing
the reversal re-instates it;
- a closing entry is never reversed (ClosingEntryIrreversible);
- a refused reversal leaves no trace (it used to leave a draft and
burn an id).
Specs go in doc comments as a ```text GOAL/INVARIANTS/SCENARIOS block.
New dev dependency: proptest for ledger-store (already the workspace's
property library).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
3609bf065e
commit
a09c1fb218
7 files changed
+478
-6
No files matched your search
@@ -65,6 +65,16 @@ pub enum LedgerError {
|
||||
#[error("entry {0} is not posted yet")]
|
||||
EntryNotPosted(u64),
|
||||
|
||||
/// An entry is reversed at most once: a second reversal would undo it
|
||||
/// twice. (Reversing the reversal re-instates it.)
|
||||
#[error("entry {0} is already reversed by entry {1}")]
|
||||
EntryAlreadyReversed(u64, u64),
|
||||
|
||||
/// A period's closing entry is never reversed: that would leave the
|
||||
/// period closed with its earnings unswept.
|
||||
#[error("entry {0} closes a period and cannot be reversed")]
|
||||
ClosingEntryIrreversible(u64),
|
||||
|
||||
/// The referenced journal entry does not exist.
|
||||
#[error("entry {0} does not exist")]
|
||||
UnknownEntry(u64),
|
||||
|
||||
Reference in new issue
Block a user