///|
/// Compares the transition evidence from two replay reports.
pub fn[S] compare_reports(
  left : ReplayReport[S],
  right : ReplayReport[S],
) -> Divergence? {
  if left.initial_sequence != right.initial_sequence {
    return Some({
      sequence: if left.initial_sequence < right.initial_sequence {
        left.initial_sequence
      } else {
        right.initial_sequence
      },
      kind: LengthMismatch,
      left_hash: left.final_state_hash,
      right_hash: right.final_state_hash,
      message: "replays start from different sequence boundaries",
    })
  }
  let common = if left.state_hashes.length() < right.state_hashes.length() {
    left.state_hashes.length()
  } else {
    right.state_hashes.length()
  }
  for index = 0; index < common; index = index + 1 {
    if left.state_hashes[index] != right.state_hashes[index] {
      return Some({
        sequence: left.initial_sequence + index,
        kind: StateMismatch,
        left_hash: left.state_hashes[index],
        right_hash: right.state_hashes[index],
        message: "state fingerprints diverge",
      })
    }
  }
  if left.status != right.status {
    return Some({
      sequence: left.initial_sequence + common,
      kind: StatusMismatch,
      left_hash: left.final_state_hash,
      right_hash: right.final_state_hash,
      message: "replay completion statuses differ",
    })
  }
  if left.state_hashes.length() != right.state_hashes.length() {
    return Some({
      sequence: left.initial_sequence + common,
      kind: LengthMismatch,
      left_hash: left.final_state_hash,
      right_hash: right.final_state_hash,
      message: "replays applied different numbers of transitions",
    })
  }
  None
}

///|
/// Replays one journal through old and new reducer versions.
///
/// Compatibility means both versions completed and every state boundary has
/// the same fingerprint.
pub fn[P, S] compare_reducer_versions(
  journal : Journal[P],
  initial_state : S,
  payload_fingerprint : (P) -> String,
  state_fingerprint : (S) -> String,
  old_reducer : (S, JournalEvent[P]) -> Transition[S],
  new_reducer : (S, JournalEvent[P]) -> Transition[S],
) -> MigrationReport[S] {
  let old_report = replay(
    journal, initial_state, payload_fingerprint, state_fingerprint, old_reducer,
  )
  let new_report = replay(
    journal, initial_state, payload_fingerprint, state_fingerprint, new_reducer,
  )
  let first_divergence = compare_reports(old_report, new_report)
  let compatible = first_divergence is None &&
    old_report.status == Completed &&
    new_report.status == Completed
  { compatible, old_report, new_report, first_divergence }
}