kuatia (ledger::commit, saga)ADR-0002 chose a saga commit pipeline (reserve -> finalize) driven by the
legend crate, and listed as a positive consequence that "the same steps drive
both a single commit and multi-transfer workflows." In the code this did not
hold literally: multi-transfer workflows compose PayMovementStep and
DepositMovementStep, which each wrap commit(), while ReservePostingsStep
and FinalizeTransferStep were used only by the single-commit EnvelopeSaga.
Driving one fixed two-step sequence through legend cost a four-hop call path
(commit_envelope -> drive_envelope_saga -> FinalizeTransferStep::execute
-> Ledger::finalize_envelope) plus a legend! macro in a separate file. The
finalize step was a trampoline that only forwarded to finalize_envelope, and
its compensation was dead code: legend compensates completed steps in LIFO
order, so a failing terminal step never runs its own compensation. Should the
single commit keep going through legend, or is it clearer as a plain function?
recover() (ADR-0003), not legend. The saga
framework contributed only per-step retry and LIFO compensation to the single
commit, and half of that (the terminal step's compensation) was unreachable.legend driving the single commit (status quo)Pros:
Cons:
commit_envelope, saga.rs,
envelope_saga.rs, and the legend! macro for one straight-line concept.legend for multi-transferReplace EnvelopeSaga with a drive_envelope_saga function that runs
reserve -> finalize directly: retry each phase, and on a finalize failure
release the reservation (a no-op past the point of no return, where recovery
rolls forward). Delete ReservePostingsStep, FinalizeTransferStep, and
envelope_saga.rs. Keep PayMovementStep/DepositMovementStep and the
legend!-composed multi-transfer sagas unchanged.
Pros:
finalize_envelope and recover.legend earns its keep across transfers
(real cross-step reversal), not within one commit.Cons:
Chosen option: Option 2. The single commit becomes a plain function; the
legend saga is reserved for multi-transfer composition, which is the only
place its cross-step retry and LIFO compensation are actually exercised. This
refines ADR-0002: the two-phase reserve -> finalize structure, the reservation
protocol, and the write-ahead recovery model are all unchanged. Only the driver
of the single commit changes, from a legend execution to a function that
replicates the same retry and compensation semantics.
commit_envelope -> drive_envelope_saga
(reserve, then finalize) -> finalize_envelope.saga.rs
(verify_postings, verify_posting_states, ensure) and is shared by the
reserve phase and by finalize_envelope.drive_envelope_saga, covered by the recovery tests (which exercise this
function directly): a Reserving saga that fails re-validation releases the
reservation and aborts; a taken posting makes the reserve phase fail without
release.commit() level instead.