1use super::{
2 BoundaryRecord, BoundaryRequest, COMMITMENT_FORMAT_VERSION, CanwuError, CauseRef,
3 CommandAttemptOutcome, CommandAttemptRecord, CommandIngress, CommandOutcome, CommandRecord,
4 ENGINE_VERSION, ErrorCode, IngressPayload, IngressRecord, PluginArchiveObjectProvider,
5 PluginIngressRequest, PluginRegistry, ReplayJournal, SNAPSHOT_FORMAT_VERSION,
6 STATE_REVISION_FORMAT_VERSION, SimDuration, SimTime, Simulation, SimulationPlugin,
7 authoritative_revision_count, authoritative_run_identity, boundary_state_hash_format,
8 is_canonical_hash, manifest,
9};
10#[cfg(test)]
11use super::{RunConfiguration, RunManifest, Scenario};
12use std::{collections::BTreeSet, rc::Rc};
13
14impl Simulation {
15 #[cfg(test)]
18 pub(crate) fn replay(
19 seed: u64,
20 scenario: Scenario,
21 commands: &[CommandRecord],
22 final_time: SimTime,
23 ) -> Result<Self, CanwuError> {
24 Self::replay_with_plugins(seed, scenario, &[], commands, final_time)
25 }
26
27 #[cfg(test)]
30 pub(crate) fn replay_with_plugins(
31 seed: u64,
32 scenario: Scenario,
33 plugins: &[&dyn SimulationPlugin],
34 commands: &[CommandRecord],
35 final_time: SimTime,
36 ) -> Result<Self, CanwuError> {
37 Self::replay_with_boundaries(seed, scenario, plugins, commands, &[], final_time)
38 }
39
40 #[cfg(test)]
44 pub(crate) fn replay_with_boundaries(
45 seed: u64,
46 scenario: Scenario,
47 plugins: &[&dyn SimulationPlugin],
48 commands: &[CommandRecord],
49 boundaries: &[BoundaryRecord],
50 final_time: SimTime,
51 ) -> Result<Self, CanwuError> {
52 let run_manifest = RunManifest::for_scenario("canwu.inline", "scenario", "1", &scenario)?;
53 Self::replay_with_run_manifest(
54 seed,
55 scenario,
56 run_manifest,
57 plugins,
58 commands,
59 boundaries,
60 final_time,
61 )
62 }
63
64 #[cfg(test)]
67 pub(crate) fn replay_with_run_manifest(
68 seed: u64,
69 scenario: Scenario,
70 run_manifest: RunManifest,
71 plugins: &[&dyn SimulationPlugin],
72 commands: &[CommandRecord],
73 boundaries: &[BoundaryRecord],
74 final_time: SimTime,
75 ) -> Result<Self, CanwuError> {
76 let simulation =
77 Self::new_with_manifest_and_plugins(seed, scenario, run_manifest, plugins)?;
78 Self::replay_records(simulation, commands, &[], &[], boundaries, final_time)
79 }
80
81 #[allow(clippy::too_many_arguments)]
84 #[cfg(test)]
85 pub(crate) fn replay_with_run_configuration(
86 seed: u64,
87 scenario: Scenario,
88 run_manifest: RunManifest,
89 run_configuration: RunConfiguration,
90 plugins: &[&dyn SimulationPlugin],
91 commands: &[CommandRecord],
92 command_attempts: &[CommandAttemptRecord],
93 boundaries: &[BoundaryRecord],
94 final_time: SimTime,
95 ) -> Result<Self, CanwuError> {
96 if command_attempts
97 .iter()
98 .any(|attempt| attempt.ingress == CommandIngress::FrozenReplay)
99 {
100 return Err(CanwuError::new(
101 ErrorCode::ReplayEnvironmentMismatch,
102 "frozen replay attempts require an environment-bound replay journal",
103 ));
104 }
105 let simulation = Self::new_with_run_configuration_and_plugins(
106 seed,
107 scenario,
108 run_manifest,
109 run_configuration,
110 plugins,
111 )?;
112 Self::replay_records(
113 simulation,
114 commands,
115 command_attempts,
116 &[],
117 boundaries,
118 final_time,
119 )
120 }
121
122 pub fn replay_from_journal(
125 plugins: &[&dyn SimulationPlugin],
126 journal: &ReplayJournal,
127 ) -> Result<Self, CanwuError> {
128 Self::replay_from_journal_with_archive_provider(plugins, journal, Rc::new(()))
129 }
130
131 pub fn replay_from_journal_with_archive_provider(
136 plugins: &[&dyn SimulationPlugin],
137 journal: &ReplayJournal,
138 archive_provider: Rc<dyn PluginArchiveObjectProvider>,
139 ) -> Result<Self, CanwuError> {
140 if journal.commitment_format_version != COMMITMENT_FORMAT_VERSION {
141 return Err(CanwuError::new(
142 ErrorCode::ReplayEnvironmentMismatch,
143 format!(
144 "replay journal commitment format {} is unsupported; this engine reads format {COMMITMENT_FORMAT_VERSION}",
145 journal.commitment_format_version
146 ),
147 ));
148 }
149 if journal.revision_format_version != STATE_REVISION_FORMAT_VERSION {
150 return Err(CanwuError::new(
151 ErrorCode::ReplayEnvironmentMismatch,
152 format!(
153 "replay journal revision format {} is unsupported; this engine reads format {STATE_REVISION_FORMAT_VERSION}",
154 journal.revision_format_version
155 ),
156 ));
157 }
158 if journal.authority_root_seed == 0 {
159 return Err(CanwuError::new(
160 ErrorCode::ReplayEnvironmentMismatch,
161 "replay journal is missing its persisted authority root",
162 ));
163 }
164 let expected_final_revision = authoritative_revision_count(
165 journal.commands.len(),
166 journal.command_attempts.len(),
167 journal.boundaries.len(),
168 )?;
169 if journal.final_revision != expected_final_revision {
170 return Err(CanwuError::new(
171 ErrorCode::ReplayEnvironmentMismatch,
172 "replay journal final revision is inconsistent with its committed evidence",
173 ));
174 }
175 let normalized = journal;
176 let scenario = normalized.initial_scenario.clone();
177 manifest::validate(&normalized.run_manifest, Some(&scenario))?;
178 let expected_manifest_hash = manifest::hash(&normalized.run_manifest)?;
179 if normalized.run_manifest_hash != expected_manifest_hash {
180 return Err(CanwuError::new(
181 ErrorCode::ReplayEnvironmentMismatch,
182 "replay journal run manifest hash is inconsistent",
183 ));
184 }
185 manifest::validate_run_configuration(
186 &normalized.run_manifest,
187 &normalized.run_configuration,
188 )?;
189 if normalized.engine_version != ENGINE_VERSION
190 || normalized.snapshot_format_version != SNAPSHOT_FORMAT_VERSION
191 || !is_canonical_hash(&normalized.run_manifest_hash)
192 || manifest::hash(&normalized.run_manifest)? != normalized.run_manifest_hash
193 || !is_canonical_hash(&normalized.checkpoint_hash)
194 {
195 return Err(CanwuError::new(
196 ErrorCode::ReplayEnvironmentMismatch,
197 "replay journal engine, format, or run identity does not match this runtime",
198 ));
199 }
200 let (_, authority_manifest_hash) = authoritative_run_identity(
201 &normalized.run_manifest,
202 &normalized.run_manifest_hash,
203 &normalized.run_configuration,
204 )?;
205 if normalized.authority_root_seed
206 != super::fresh_authority_root_seed(normalized.root_seed, &authority_manifest_hash)?
207 {
208 return Err(CanwuError::new(
209 ErrorCode::ReplayEnvironmentMismatch,
210 "replay journal authority root is not bound to its run identity",
211 ));
212 }
213 PluginRegistry::from_descriptors(normalized.plugin_descriptors.clone()).map_err(
214 |error| {
215 CanwuError::new(
216 ErrorCode::ReplayEnvironmentMismatch,
217 format!("replay journal plugin manifest is invalid: {error}"),
218 )
219 },
220 )?;
221
222 let mut simulation = Self::new_with_configuration_snapshot(
223 normalized.root_seed,
224 scenario,
225 normalized.run_manifest.clone(),
226 normalized.run_configuration.clone(),
227 )?;
228 simulation.set_plugin_archive_object_provider(archive_provider);
229 simulation.state.current.authority_root_seed = normalized.authority_root_seed;
230 let simulation = Self::activate_initial_plugins(simulation, plugins)?;
231 let actual_descriptors: Vec<_> = simulation.plugin_descriptors().cloned().collect();
232 if actual_descriptors != normalized.plugin_descriptors {
233 return Err(CanwuError::new(
234 ErrorCode::ReplayEnvironmentMismatch,
235 "active plugin identities and contracts do not match the replay journal",
236 ));
237 }
238
239 let mut simulation = Self::replay_records(
240 simulation,
241 &normalized.commands,
242 &normalized.command_attempts,
243 &normalized.ingress,
244 &normalized.boundaries,
245 normalized.final_time,
246 )?;
247 if normalized.plugin_registration_closed
248 && !simulation.state.metadata.plugin_registration_closed
249 {
250 simulation.advance(SimDuration::ZERO)?;
251 }
252 if simulation.state.metadata.plugin_registration_closed
253 != normalized.plugin_registration_closed
254 {
255 return Err(CanwuError::new(
256 ErrorCode::ReplayMismatch,
257 "replayed plugin-registration lifecycle does not match the recorded journal",
258 ));
259 }
260 if simulation.revision() != normalized.final_revision {
261 return Err(CanwuError::new(
262 ErrorCode::ReplayMismatch,
263 "replayed final state revision does not match the recorded journal",
264 ));
265 }
266 if simulation.checkpoint_hash() != normalized.checkpoint_hash {
267 return Err(CanwuError::new(
268 ErrorCode::ReplayMismatch,
269 "replayed final checkpoint does not match the recorded journal",
270 ));
271 }
272 Ok(simulation)
273 }
274
275 pub fn replay_from_journal_json(
278 plugins: &[&dyn SimulationPlugin],
279 json: &str,
280 ) -> Result<Self, CanwuError> {
281 let journal: ReplayJournal = super::deserialize_current_json(json, "replay journal")?;
282 Self::replay_from_journal(plugins, &journal)
283 }
284
285 #[cfg(test)]
286 #[allow(clippy::needless_pass_by_value)]
287 pub(crate) fn replay_from_journal_with_scenario(
288 scenario: Scenario,
289 plugins: &[&dyn SimulationPlugin],
290 journal: &ReplayJournal,
291 ) -> Result<Self, CanwuError> {
292 if scenario != journal.initial_scenario {
293 return Err(CanwuError::new(
294 ErrorCode::ReplayEnvironmentMismatch,
295 "test replay scenario disagrees with the self-contained journal scenario",
296 ));
297 }
298 Self::replay_from_journal(plugins, journal)
299 }
300
301 fn replay_records(
302 mut simulation: Self,
303 commands: &[CommandRecord],
304 attempts: &[CommandAttemptRecord],
305 ingress: &[IngressRecord],
306 boundaries: &[BoundaryRecord],
307 final_time: SimTime,
308 ) -> Result<Self, CanwuError> {
309 simulation.ensure_runtime_ready()?;
310 if !attempts.is_empty() {
311 return Self::replay_attempt_records(
312 simulation, commands, attempts, ingress, boundaries, final_time,
313 );
314 }
315 let mut next_ingress = 0;
316 let mut next_command = 0;
317 for (boundary_index, expected_boundary) in boundaries.iter().enumerate() {
318 enqueue_replay_ingress_cut(
319 &mut simulation,
320 ingress,
321 &mut next_ingress,
322 boundary_index,
323 )?;
324 for admitted in &expected_boundary.admitted_commands {
325 let Some(record) = commands.get(next_command) else {
326 return Err(CanwuError::new(
327 ErrorCode::ReplayMismatch,
328 "boundary replay admits a command absent from the journal",
329 ));
330 };
331 if record.id != *admitted {
332 return Err(CanwuError::new(
333 ErrorCode::ReplayMismatch,
334 "boundary replay command admission does not match journal order",
335 ));
336 }
337 replay_command_record(&mut simulation, record, expected_boundary.at)?;
338 next_command += 1;
339 }
340 let receipt = simulation.settle_boundary_with_state_hash_format(
341 BoundaryRequest {
342 at: expected_boundary.at,
343 cadences: expected_boundary.cadences.clone(),
344 },
345 boundary_state_hash_format(expected_boundary.state_hash.as_deref())?,
346 )?;
347 let Some(actual_boundary) = simulation.boundaries().last() else {
348 return Err(CanwuError::new(
349 ErrorCode::ReplayMismatch,
350 "boundary replay did not append settlement evidence",
351 ));
352 };
353 if receipt.boundary_id != expected_boundary.id || actual_boundary != expected_boundary {
354 return Err(CanwuError::new(
355 ErrorCode::ReplayMismatch,
356 format!(
357 "regenerated boundary {} did not match its journal evidence",
358 expected_boundary.id
359 ),
360 ));
361 }
362 }
363 for record in &commands[next_command..] {
364 replay_command_record(&mut simulation, record, final_time)?;
365 }
366 enqueue_replay_ingress_cut(
367 &mut simulation,
368 ingress,
369 &mut next_ingress,
370 boundaries.len(),
371 )?;
372 if next_ingress != ingress.len() {
373 return Err(CanwuError::new(
374 ErrorCode::ReplayMismatch,
375 "ingress journal contains an impossible future boundary issue cut",
376 ));
377 }
378 if final_time < simulation.time() {
379 return Err(CanwuError::new(
380 ErrorCode::InvalidDuration,
381 "replay final time cannot precede the last command",
382 ));
383 }
384 if final_time > simulation.time() {
385 simulation.ensure_legacy_advance_does_not_cross_ingress(final_time)?;
386 simulation.advance_to(final_time)?;
387 }
388 Ok(simulation)
389 }
390
391 fn replay_attempt_records(
392 mut simulation: Self,
393 commands: &[CommandRecord],
394 attempts: &[CommandAttemptRecord],
395 ingress: &[IngressRecord],
396 boundaries: &[BoundaryRecord],
397 final_time: SimTime,
398 ) -> Result<Self, CanwuError> {
399 let command_ingress_requests: BTreeSet<_> = ingress
400 .iter()
401 .filter_map(|record| match &record.payload {
402 IngressPayload::Command { request } => Some(request.request_id),
403 IngressPayload::Decision { request } => {
404 request.command.as_ref().map(|command| command.request_id)
405 }
406 IngressPayload::Plugin { .. }
407 | IngressPayload::Calendar { .. }
408 | IngressPayload::Maintenance { .. }
409 | IngressPayload::PluginCancellation { .. } => None,
410 })
411 .collect();
412 let mut next_ingress = 0;
413 let mut next_attempt = 0;
414 for (boundary_index, expected_boundary) in boundaries.iter().enumerate() {
415 enqueue_replay_ingress_cut(
416 &mut simulation,
417 ingress,
418 &mut next_ingress,
419 boundary_index,
420 )?;
421 let mut admitted_commands = Vec::new();
422 for admitted in &expected_boundary.admitted_attempts {
423 let Some(record) = attempts.get(next_attempt) else {
424 return Err(CanwuError::new(
425 ErrorCode::ReplayMismatch,
426 "boundary replay admits a command attempt absent from the journal",
427 ));
428 };
429 if record.id != *admitted {
430 return Err(CanwuError::new(
431 ErrorCode::ReplayMismatch,
432 "boundary replay attempt admission does not match journal order",
433 ));
434 }
435 let queued = record
436 .request_id
437 .is_some_and(|request| command_ingress_requests.contains(&request));
438 if !queued {
439 replay_attempt_record(&mut simulation, record, commands, expected_boundary.at)?;
440 }
441 if let CommandAttemptOutcome::Accepted { command_id } = record.outcome {
442 admitted_commands.push(command_id);
443 }
444 next_attempt += 1;
445 }
446 if admitted_commands != expected_boundary.admitted_commands {
447 return Err(CanwuError::new(
448 ErrorCode::ReplayMismatch,
449 "boundary replay accepted-command cut disagrees with admitted attempts",
450 ));
451 }
452 let receipt = simulation.settle_boundary_with_state_hash_format(
453 BoundaryRequest {
454 at: expected_boundary.at,
455 cadences: expected_boundary.cadences.clone(),
456 },
457 boundary_state_hash_format(expected_boundary.state_hash.as_deref())?,
458 )?;
459 let Some(actual_boundary) = simulation.boundaries().last() else {
460 return Err(CanwuError::new(
461 ErrorCode::ReplayMismatch,
462 "boundary replay did not append settlement evidence",
463 ));
464 };
465 if receipt.boundary_id != expected_boundary.id || actual_boundary != expected_boundary {
466 return Err(CanwuError::new(
467 ErrorCode::ReplayMismatch,
468 format!(
469 "regenerated boundary {} did not match its journal evidence",
470 expected_boundary.id
471 ),
472 ));
473 }
474 }
475 for record in &attempts[next_attempt..] {
476 replay_attempt_record(&mut simulation, record, commands, final_time)?;
477 }
478 enqueue_replay_ingress_cut(
479 &mut simulation,
480 ingress,
481 &mut next_ingress,
482 boundaries.len(),
483 )?;
484 if next_ingress != ingress.len() {
485 return Err(CanwuError::new(
486 ErrorCode::ReplayMismatch,
487 "ingress journal contains an impossible future boundary issue cut",
488 ));
489 }
490 if simulation.command_log() != commands {
491 return Err(CanwuError::new(
492 ErrorCode::ReplayMismatch,
493 "replayed accepted command journal does not match its recorded evidence",
494 ));
495 }
496 if final_time < simulation.time() {
497 return Err(CanwuError::new(
498 ErrorCode::InvalidDuration,
499 "replay final time cannot precede the last command attempt",
500 ));
501 }
502 if final_time > simulation.time() {
503 simulation.ensure_legacy_advance_does_not_cross_ingress(final_time)?;
504 simulation.advance_to(final_time)?;
505 }
506 Ok(simulation)
507 }
508}
509
510fn enqueue_replay_ingress_cut(
511 simulation: &mut Simulation,
512 ingress: &[IngressRecord],
513 next_ingress: &mut usize,
514 boundary_count: usize,
515) -> Result<(), CanwuError> {
516 let expected_boundary_count = u64::try_from(boundary_count).map_err(|_| {
517 CanwuError::new(
518 ErrorCode::ReplayMismatch,
519 "boundary count exceeds ingress range",
520 )
521 })?;
522 while let Some(record) = ingress.get(*next_ingress) {
523 if record.eligible_boundary_count < expected_boundary_count {
524 return Err(CanwuError::new(
525 ErrorCode::ReplayMismatch,
526 "ingress journal skipped its recorded issue boundary",
527 ));
528 }
529 if record.eligible_boundary_count > expected_boundary_count {
530 break;
531 }
532 if record.issued_at < simulation.time() {
533 return Err(CanwuError::new(
534 ErrorCode::ReplayMismatch,
535 "ingress journal issue time precedes replay state",
536 ));
537 }
538 if record.issued_at > simulation.time() {
539 simulation.ensure_legacy_advance_does_not_cross_ingress(record.issued_at)?;
540 simulation.advance_to(record.issued_at)?;
541 }
542 if let Some(actual) = simulation.state.evidence.ingress.get(*next_ingress) {
543 if actual != record {
544 return Err(CanwuError::new(
545 ErrorCode::ReplayMismatch,
546 "plugin-generated ingress does not match journal evidence",
547 ));
548 }
549 *next_ingress += 1;
550 continue;
551 }
552 if matches!(record.cause, Some(CauseRef::Boundary(_))) {
553 return Err(CanwuError::new(
554 ErrorCode::ReplayMismatch,
555 "recorded boundary-generated ingress was not reproduced by its plugin system",
556 ));
557 }
558 let receipt = match &record.payload {
559 IngressPayload::Command { request } => simulation.enqueue_command(
560 record.due_at,
561 record.priority,
562 request.as_ref().clone(),
563 )?,
564 IngressPayload::Plugin {
565 plugin,
566 packet_type,
567 payload,
568 affected_entities,
569 archive_retention,
570 } => {
571 let mut request = PluginIngressRequest::new(
572 plugin.clone(),
573 packet_type.clone(),
574 record.due_at,
575 payload.clone(),
576 )
577 .with_priority(record.priority);
578 request.affected_entities.clone_from(affected_entities);
579 request.cause.clone_from(&record.cause);
580 request.archive_retention.clone_from(archive_retention);
581 simulation.replay_plugin_ingress(request)?
582 }
583 IngressPayload::Calendar { cadences } => {
584 simulation.schedule_calendar_boundary(record.due_at, cadences.clone())?
585 }
586 IngressPayload::Decision { request } => simulation.enqueue_decision(
587 record.due_at,
588 record.priority,
589 request.as_ref().clone(),
590 )?,
591 IngressPayload::Maintenance { request } => match request.as_ref() {
592 super::MaintenanceIngressRequest::DecisionArchive { commit } => simulation
593 .enqueue_decision_archive_commit(
594 record.due_at,
595 record.priority,
596 commit.clone(),
597 )?,
598 super::MaintenanceIngressRequest::OwnerAuthorized { commit } => simulation
599 .enqueue_owner_authorized_maintenance(
600 record.due_at,
601 record.priority,
602 commit.clone(),
603 )?,
604 },
605 IngressPayload::PluginCancellation {
606 cancelled,
607 authority,
608 reason,
609 } => simulation.replay_plugin_ingress_cancellation(
610 *cancelled,
611 *authority,
612 reason.clone(),
613 )?,
614 };
615 if receipt.ingress_id != record.id
616 || simulation.state.evidence.ingress.last() != Some(record)
617 {
618 return Err(CanwuError::new(
619 ErrorCode::ReplayMismatch,
620 "regenerated ingress record does not match journal evidence",
621 ));
622 }
623 *next_ingress += 1;
624 }
625 Ok(())
626}
627
628fn replay_command_record(
629 simulation: &mut Simulation,
630 record: &CommandRecord,
631 latest_time: SimTime,
632) -> Result<(), CanwuError> {
633 if record.accepted_at < simulation.time() || record.accepted_at > latest_time {
634 return Err(CanwuError::new(
635 ErrorCode::ReplayMismatch,
636 "replay command timestamps do not match authoritative operation order",
637 ));
638 }
639 simulation.ensure_legacy_advance_does_not_cross_ingress(record.accepted_at)?;
640 simulation.advance_to(record.accepted_at)?;
641 let CommandOutcome::Accepted { receipt } = simulation.admit_command(
642 None,
643 None,
644 record.envelope.clone(),
645 CommandIngress::LegacyDirect,
646 None,
647 false,
648 )?
649 else {
650 return Err(CanwuError::new(
651 ErrorCode::ReplayMismatch,
652 "legacy replay command was rejected",
653 ));
654 };
655 if receipt.command_id != record.id {
656 return Err(CanwuError::new(
657 ErrorCode::ReplayMismatch,
658 "replay command IDs did not match the journal",
659 ));
660 }
661 Ok(())
662}
663
664fn replay_attempt_record(
665 simulation: &mut Simulation,
666 record: &CommandAttemptRecord,
667 commands: &[CommandRecord],
668 latest_time: SimTime,
669) -> Result<(), CanwuError> {
670 if record.at < simulation.time() || record.at > latest_time {
671 return Err(CanwuError::new(
672 ErrorCode::ReplayMismatch,
673 "replay command-attempt timestamps do not match authoritative operation order",
674 ));
675 }
676 simulation.ensure_legacy_advance_does_not_cross_ingress(record.at)?;
677 simulation.advance_to(record.at)?;
678 let outcome = simulation.admit_command(
679 record.request_id,
680 record.expected_revision,
681 record.envelope.clone(),
682 record.ingress,
683 None,
684 true,
685 )?;
686 if simulation.command_attempts().last() != Some(record) {
687 return Err(CanwuError::new(
688 ErrorCode::ReplayMismatch,
689 format!(
690 "regenerated command attempt {} did not match its journal evidence",
691 record.id
692 ),
693 ));
694 }
695 match (&record.outcome, outcome) {
696 (CommandAttemptOutcome::Accepted { command_id }, CommandOutcome::Accepted { receipt })
697 if receipt.command_id == *command_id =>
698 {
699 let index = usize::try_from(command_id.get().saturating_sub(1)).map_err(|_| {
700 CanwuError::new(
701 ErrorCode::ReplayMismatch,
702 "replayed command ID exceeds the journal index range",
703 )
704 })?;
705 if simulation.command_log().last() != commands.get(index) {
706 return Err(CanwuError::new(
707 ErrorCode::ReplayMismatch,
708 "regenerated command record did not match its journal evidence",
709 ));
710 }
711 }
712 (
713 CommandAttemptOutcome::Rejected { error: expected },
714 CommandOutcome::Rejected { rejection },
715 ) if rejection.error == *expected => {}
716 _ => {
717 return Err(CanwuError::new(
718 ErrorCode::ReplayMismatch,
719 "replayed command-attempt outcome differs from its journal evidence",
720 ));
721 }
722 }
723 Ok(())
724}