///|
/// Replays a complete validated journal from an initial state.
///
/// `payload_fingerprint` and `state_fingerprint` must be deterministic.
/// `reducer` must not depend on time, randomness, I/O, or mutable global state.
pub fn[P, S] replay(
journal : Journal[P],
initial_state : S,
payload_fingerprint : (P) -> String,
state_fingerprint : (S) -> String,
reducer : (S, JournalEvent[P]) -> Transition[S],
) -> ReplayReport[S] {
let initial_hash = fingerprint_text(state_fingerprint(initial_state))
let state_hashes : Array[Int] = [initial_hash]
match journal.validate(payload_fingerprint) {
Invalid(issue) =>
return {
status: InvalidJournal(issue),
initial_sequence: 0,
applied_events: 0,
final_sequence: 0,
final_state: initial_state,
final_state_hash: initial_hash,
journal_tail_hash: journal.tail_hash(),
state_hashes,
}
Valid(_, _) => ()
}
let mut state = initial_state
let mut applied = 0
for index = 0; index < journal.length(); index = index + 1 {
let event = match journal.get(index) {
Some(event) => event
None => abort("validated journal index disappeared")
}
match reducer(state, event) {
Accepted(next_state) => {
state = next_state
applied = applied + 1
state_hashes.push(fingerprint_text(state_fingerprint(state)))
}
Rejected(reason) =>
return {
status: Rejected(event.sequence, reason),
initial_sequence: 0,
applied_events: applied,
final_sequence: event.sequence - 1,
final_state: state,
final_state_hash: fingerprint_text(state_fingerprint(state)),
journal_tail_hash: journal.tail_hash(),
state_hashes,
}
}
}
{
status: Completed,
initial_sequence: 0,
applied_events: applied,
final_sequence: applied,
final_state: state,
final_state_hash: fingerprint_text(state_fingerprint(state)),
journal_tail_hash: journal.tail_hash(),
state_hashes,
}
}
///|
/// Runs the same replay twice and checks the complete transition evidence.
///
/// This catches reducers that accidentally read time, randomness, or mutable
/// ambient state even when their final states happen to match.
pub fn[P, S] verify_determinism(
journal : Journal[P],
initial_state : S,
payload_fingerprint : (P) -> String,
state_fingerprint : (S) -> String,
reducer : (S, JournalEvent[P]) -> Transition[S],
) -> Bool {
let first = replay(
journal, initial_state, payload_fingerprint, state_fingerprint, reducer,
)
let second = replay(
journal, initial_state, payload_fingerprint, state_fingerprint, reducer,
)
if first.status != second.status ||
first.final_state_hash != second.final_state_hash ||
first.state_hashes.length() != second.state_hashes.length() {
return false
}
for index = 0; index < first.state_hashes.length(); index = index + 1 {
if first.state_hashes[index] != second.state_hashes[index] {
return false
}
}
true
}