1use std::collections::{BTreeMap, BTreeSet};
19use std::fmt;
20
21use rustev_contract::calibration::CalibrationArtifact;
22use rustev_contract::descriptor::BackendDescriptor;
23use rustev_contract::generation_result::TextResult;
24use rustev_contract::ids::{ArtifactId, RequestId};
25use rustev_contract::judgment::{Judgment, Unresolved};
26use rustev_contract::output::{BackendOutputDoc, RawOutput};
27use rustev_contract::plan::{Plan, PlanExecution, PlanStepDetail};
28use rustev_contract::reason::RuntimeReasonDoc;
29use rustev_contract::replay::{
30 BundleError, CaptureStatus, ItemLocation, MAX_BUNDLE_ITEMS, ReplayBundle,
31};
32use rustev_contract::retention::Availability;
33use rustev_contract::run::{CoreEvidence, RequestResult, RunRecord, Termination};
34use rustev_contract::scope::Scope;
35use rustev_contract::snapshot::Snapshot;
36use rustev_contract::time::Timestamp;
37use rustev_contract::{Document, Identified, schema};
38use rustev_core::evaluate::{Evaluation, Supplied};
39use rustev_core::generation_eval::GenerationEvaluation;
40use rustev_core::{Compiled, LoadError};
41
42use crate::acquisition_replay::{MissingAcquisition, PageValue, fill_missing, supply_page};
43
44use crate::resolve::{Budget, ItemFailure, Resolver, check_bytes, item_bytes};
45
46#[derive(Debug, Clone)]
48pub struct ReplayConfig {
49 pub scope: Scope,
52 pub now_ms: Timestamp,
54 pub host_cap_ms: Option<u64>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum Inconsistency {
61 DecisionId,
62 PlanId,
63 SnapshotId,
64 EvaluationTime,
65 TooManySupplies {
67 max: u64,
68 },
69 NotPending,
71 NotInRun,
73 Origin,
75 UnknownTarget,
77 RequestIdentity,
79 Value,
82 OutputBinding,
84 OutputRefused,
86 MissingSupply {
88 step: String,
89 instance: Vec<String>,
90 },
91 RequestOrder,
93 StartRefused,
95 FinishRefused,
97 NoRecordForm,
99}
100
101#[derive(Debug, Clone, PartialEq)]
103pub enum Incomparable {
104 AcquisitionNotCaptured {
105 missing: Vec<MissingAcquisition>,
106 judgment: Box<Judgment>,
107 },
108 ScopeMismatch,
109 InvalidBundle(BundleError),
110 CaptureIncomplete(CaptureStatus),
111 Unavailable {
112 location: ItemLocation,
113 availability: Availability,
114 },
115 Oversized {
116 location: ItemLocation,
117 },
118 NoExpectedJudgment,
120 CompilerChanged {
121 recorded_compiler: String,
122 current_compiler: String,
123 recorded_registry: String,
124 current_registry: String,
125 },
126 MissingDependency {
128 detail: String,
129 },
130 PlanMismatch,
132 Inconsistent {
133 location: ItemLocation,
134 what: Inconsistency,
135 },
136}
137
138impl Incomparable {
139 pub fn code(&self) -> String {
141 match self {
142 Incomparable::AcquisitionNotCaptured { .. } => "acquisition-not-captured".into(),
143 Incomparable::ScopeMismatch => "scope-mismatch".into(),
144 Incomparable::InvalidBundle(_) => "invalid-bundle".into(),
145 Incomparable::CaptureIncomplete(_) => "capture-incomplete".into(),
146 Incomparable::Unavailable {
147 location: ItemLocation::AcquisitionPage(_),
148 availability: Availability::Missing,
149 } => "content-not-retained".into(),
150 Incomparable::Unavailable { availability, .. } => availability.name().into(),
151 Incomparable::Oversized { .. } => "oversized".into(),
152 Incomparable::NoExpectedJudgment => "no-expected-judgment".into(),
153 Incomparable::CompilerChanged { .. } => "compiler-changed".into(),
154 Incomparable::MissingDependency { .. } => "missing-dependency".into(),
155 Incomparable::PlanMismatch => "plan-mismatch".into(),
156 Incomparable::Inconsistent { .. } => "inconsistent".into(),
157 }
158 }
159}
160
161impl fmt::Display for Incomparable {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match self {
164 Incomparable::Unavailable {
165 location,
166 availability,
167 } => write!(f, "{location} is {availability}"),
168 Incomparable::Oversized { location } => write!(f, "{location} is oversized"),
169 Incomparable::Inconsistent { location, what } => {
170 write!(f, "inconsistent at {location}: {what:?}")
171 }
172 Incomparable::InvalidBundle(e) => write!(f, "invalid bundle: {e}"),
173 other => f.write_str(&other.code()),
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq)]
180pub enum RetainedValue {
181 Output {
182 artifact: ArtifactId,
183 output: RawOutput,
184 },
185 Reason(Unresolved),
187}
188
189#[derive(Debug, Clone, PartialEq)]
191pub struct RetainedSupply {
192 pub step: String,
193 pub instance: Vec<String>,
194 pub request: RequestId,
195 pub target: u32,
196 pub value: RetainedValue,
197}
198
199#[derive(Debug, Clone)]
205pub struct Reproduced {
206 pub(crate) decision_id: String,
207 pub(crate) scope: Scope,
208 pub(crate) plan: Plan,
209 pub(crate) calibrations: Vec<CalibrationArtifact>,
210 pub(crate) snapshot: Snapshot,
211 pub(crate) evaluation_time: Timestamp,
212 pub(crate) judgment: Judgment,
213 pub(crate) run: RunRecord,
214 pub(crate) supplies: Vec<RetainedSupply>,
215 pub(crate) pages: Vec<(rustev_contract::ids::AcquisitionRequestId, PageValue)>,
216 pub(crate) text_result: Option<TextResult>,
217}
218
219impl Reproduced {
220 pub fn text_result(&self) -> Option<&TextResult> {
222 self.text_result.as_ref()
223 }
224
225 pub fn decision_id(&self) -> &str {
226 &self.decision_id
227 }
228 pub fn scope(&self) -> &Scope {
229 &self.scope
230 }
231 pub fn plan(&self) -> &Plan {
233 &self.plan
234 }
235 pub fn calibrations(&self) -> &[CalibrationArtifact] {
236 &self.calibrations
237 }
238 pub fn snapshot(&self) -> &Snapshot {
239 &self.snapshot
240 }
241 pub fn evaluation_time(&self) -> Timestamp {
242 self.evaluation_time
243 }
244 pub fn judgment(&self) -> &Judgment {
246 &self.judgment
247 }
248 pub fn run(&self) -> &RunRecord {
250 &self.run
251 }
252 pub fn supplies(&self) -> &[RetainedSupply] {
254 &self.supplies
255 }
256}
257
258#[derive(Debug, Clone)]
259pub enum ReplayOutcome {
260 Reproduced(Box<Reproduced>),
261 Diverged {
262 expected: Box<Judgment>,
263 actual: Box<Judgment>,
264 },
265 Incomparable(Incomparable),
266}
267
268fn inconsistent(location: ItemLocation, what: Inconsistency) -> Incomparable {
269 Incomparable::Inconsistent { location, what }
270}
271
272fn item_failure(location: ItemLocation, f: ItemFailure) -> Incomparable {
273 match f {
274 ItemFailure::Unavailable(availability) => Incomparable::Unavailable {
275 location,
276 availability,
277 },
278 ItemFailure::Oversized => Incomparable::Oversized { location },
279 }
280}
281
282pub fn reproduce(
284 bundle: &ReplayBundle,
285 resolver: &dyn Resolver,
286 config: &ReplayConfig,
287) -> ReplayOutcome {
288 match reproduce_inner(bundle, resolver, config) {
289 Ok(o) => o,
290 Err(i) => ReplayOutcome::Incomparable(i),
291 }
292}
293
294struct Resolved {
295 plan: Plan,
296 descriptors: Vec<BackendDescriptor>,
297 calibrations: Vec<CalibrationArtifact>,
298 snapshot: Snapshot,
299 run: RunRecord,
300 values: Vec<Value>,
301 sources: Vec<rustev_contract::source::SourceDescriptor>,
302 pages: Vec<PageValue>,
303}
304
305enum Value {
306 Output(BackendOutputDoc),
307 Reason(RuntimeReasonDoc),
308}
309
310fn resolve_all(
311 bundle: &ReplayBundle,
312 resolver: &dyn Resolver,
313 now: Timestamp,
314) -> Result<Resolved, Incomparable> {
315 let mut budget = Budget::default();
316 let mut get = |location: ItemLocation, item| {
317 item_bytes(item, now, resolver, &mut budget)
318 .map_err(|f| item_failure(location, f))
319 .map(|b| (location, item, b))
320 };
321 fn typed<T: Document + PartialEq>(
322 (location, item, bytes): (
323 ItemLocation,
324 &rustev_contract::retention::RetentionItem,
325 Vec<u8>,
326 ),
327 ) -> Result<T, Incomparable> {
328 check_bytes::<T>(item, &bytes).map_err(|f| item_failure(location, f))
329 }
330 let plan: Plan = typed(get(ItemLocation::Plan, &bundle.plan)?)?;
331 let mut descriptors = vec![];
332 for (i, d) in bundle.descriptors.iter().enumerate() {
333 descriptors.push(typed(get(ItemLocation::Descriptor(i), d)?)?);
334 }
335 let mut calibrations = vec![];
336 for (i, c) in bundle.calibrations.iter().enumerate() {
337 calibrations.push(typed(get(ItemLocation::Calibration(i), c)?)?);
338 }
339 let snapshot: Snapshot = typed(get(ItemLocation::Snapshot, &bundle.snapshot)?)?;
340 let run: RunRecord = typed(get(ItemLocation::Run, &bundle.run)?)?;
341 let mut values = vec![];
342 for (i, s) in bundle.supplies.iter().enumerate() {
343 let location = ItemLocation::Supply(i);
344 let got = get(location, &s.value)?;
345 values.push(if s.value.document_schema == schema::RUNTIME_REASON {
346 let r: RuntimeReasonDoc = typed(got)?;
347 if r.check().is_err() {
348 return Err(Incomparable::Unavailable {
349 location,
350 availability: Availability::Mismatched,
351 });
352 }
353 Value::Reason(r)
354 } else {
355 Value::Output(typed(got)?)
356 });
357 }
358 let mut sources = vec![];
359 for (i, source) in bundle.sources.iter().flatten().enumerate() {
360 sources.push(typed(get(ItemLocation::Source(i), source)?)?);
361 }
362 let mut pages = vec![];
363 for (i, page) in bundle.acquisition_pages.iter().flatten().enumerate() {
364 let got = get(ItemLocation::AcquisitionPage(i), &page.value)?;
365 pages.push(if page.value.document_schema == schema::RUNTIME_REASON {
366 PageValue::Reason(typed::<RuntimeReasonDoc>(got)?.reason)
367 } else {
368 PageValue::Page(typed(got)?)
369 });
370 }
371 Ok(Resolved {
372 sources,
373 pages,
374 plan,
375 descriptors,
376 calibrations,
377 snapshot,
378 run,
379 values,
380 })
381}
382
383fn target_artifact(plan: &Plan, step: &str, target: u32) -> Option<ArtifactId> {
385 if let Some((_, b)) = rustev_contract::generation::output_screen_binding(plan, step) {
386 return (target == 0).then(|| b.artifact.clone());
387 }
388 let artifact = match &plan.steps.iter().find(|s| s.id == step)?.detail {
389 PlanStepDetail::Semantic(b) => &b.artifact,
390 PlanStepDetail::Generate(b) => &b.artifact,
391 PlanStepDetail::Extract(b) => &b.artifact,
392 _ => return None,
393 };
394 if target == 0 {
395 return Some(artifact.clone());
396 }
397 match &plan.execution {
398 PlanExecution::Declared(d) => d
399 .fallbacks
400 .iter()
401 .find(|f| f.step == step && f.target == target)
402 .map(|f| f.artifact.clone()),
403 PlanExecution::None => None,
404 }
405}
406
407enum ReplayEvaluation<'a> {
409 Decision(Box<Evaluation<'a>>),
410 Generation(Box<GenerationEvaluation<'a>>),
411}
412impl<'a> ReplayEvaluation<'a> {
413 fn advance(self, decision: &str) -> Result<Self, Incomparable> {
414 match self {
419 Self::Decision(ev)
420 if ev.pending().is_empty() && ev.pending_acquisitions().is_empty() =>
421 {
422 ev.begin_generation(decision)
423 .map(Box::new)
424 .map(Self::Generation)
425 .map_err(|_| inconsistent(ItemLocation::Bundle, Inconsistency::FinishRefused))
426 }
427 other => Ok(other),
428 }
429 }
430 fn pending(&self) -> Vec<(String, Vec<String>)> {
431 match self {
432 Self::Decision(ev) => ev
433 .pending()
434 .iter()
435 .map(|r| (r.step.clone(), r.instance.clone()))
436 .collect(),
437 Self::Generation(g) => g
438 .pending()
439 .iter()
440 .map(|r| (r.document.step.clone(), r.document.instance.clone()))
441 .collect(),
442 }
443 }
444 fn request_document(
445 &self,
446 step: &str,
447 instance: &[String],
448 target: u32,
449 scope: &Scope,
450 ) -> Result<rustev_contract::request::RequestDoc, rustev_core::RequestIdentityError> {
451 match self {
452 Self::Decision(ev) => ev.request_document(step, instance, target, scope),
453 Self::Generation(g) => g.request_document(step, instance, target, scope),
454 }
455 }
456 fn accepts(&self, step: &str, instance: &[String], output: &RawOutput) -> bool {
457 match self {
458 Self::Decision(ev) => matches!(ev.check_output(step, instance, output), Ok(Ok(()))),
459 Self::Generation(g) => matches!(g.check_output(step, instance, output), Ok(Ok(()))),
460 }
461 }
462 fn supply(
463 &mut self,
464 step: &str,
465 instance: &[String],
466 value: Supplied,
467 q: &rustev_contract::run::RequestRecord,
468 decision: &str,
469 ) -> bool {
470 match self {
471 Self::Decision(ev) => ev.supply(step, instance, value).is_ok(),
472 Self::Generation(g) => {
473 let from = q.supplied_from().expect("checked supplied request");
476 let attempt = from
477 .attempt
478 .map(|a| a.attempt_id.as_str())
479 .or(from.reused_from);
480 let fallback = format!("{decision}/{step}/refused");
481 let attempt = attempt.unwrap_or(&fallback);
482 if g.supply_attempt(
483 step,
484 instance,
485 value,
486 attempt,
487 (q.attempts.len() as u32).max(1),
488 )
489 .is_err()
490 {
491 return false;
492 }
493 if let Some(source) = from.reused_from {
494 let decision = source
495 .split('/')
496 .next()
497 .unwrap_or("")
498 .replace("%2F", "/")
499 .replace("%2C", ",")
500 .replace("%25", "%");
501 g.note_cached_attempt(attempt, &decision);
502 }
503 true
504 }
505 }
506 }
507}
508
509fn list_new(
511 ev: &ReplayEvaluation<'_>,
512 listed: &mut Vec<(String, Vec<String>)>,
513 seen: &mut BTreeSet<(String, Vec<String>)>,
514) {
515 for key in ev.pending() {
516 if seen.insert(key.clone()) {
517 listed.push(key);
518 }
519 }
520}
521
522fn reproduce_inner(
523 bundle: &ReplayBundle,
524 resolver: &dyn Resolver,
525 config: &ReplayConfig,
526) -> Result<ReplayOutcome, Incomparable> {
527 if config.scope != bundle.scope {
529 return Err(Incomparable::ScopeMismatch);
530 }
531 bundle
532 .check(config.host_cap_ms)
533 .map_err(Incomparable::InvalidBundle)?;
534 if bundle.capture != CaptureStatus::Complete {
535 return Err(Incomparable::CaptureIncomplete(bundle.capture));
536 }
537 let r = resolve_all(bundle, resolver, config.now_ms)?;
539 let run = &r.run;
540 let run_at = ItemLocation::Run;
541 if run.decision_id != bundle.decision_id {
542 return Err(inconsistent(run_at, Inconsistency::DecisionId));
543 }
544 if run.plan_id != bundle.plan_id {
545 return Err(inconsistent(run_at, Inconsistency::PlanId));
546 }
547 let evidence = match (&run.termination, &run.core) {
548 (Termination::Judged, CoreEvidence::Recorded(e)) => e,
549 _ => return Err(Incomparable::NoExpectedJudgment),
550 };
551 if evidence.decision_id != bundle.decision_id {
552 return Err(inconsistent(run_at, Inconsistency::DecisionId));
553 }
554 if evidence.plan_id != bundle.plan_id || evidence.judgment.plan_id != bundle.plan_id {
555 return Err(inconsistent(run_at, Inconsistency::PlanId));
556 }
557 if evidence.snapshot_id != bundle.snapshot_id {
558 return Err(inconsistent(run_at, Inconsistency::SnapshotId));
559 }
560 if evidence.evaluation_time_ms != bundle.evaluation_time_ms {
561 return Err(inconsistent(run_at, Inconsistency::EvaluationTime));
562 }
563 if r.plan.id().ok().as_ref() != Some(&bundle.plan_id) {
564 return Err(inconsistent(ItemLocation::Plan, Inconsistency::PlanId));
565 }
566 if r.snapshot.id().ok().as_ref() != Some(&bundle.snapshot_id) {
567 return Err(inconsistent(
568 ItemLocation::Snapshot,
569 Inconsistency::SnapshotId,
570 ));
571 }
572 let max = r
573 .plan
574 .definition
575 .limits
576 .max_semantic_requests
577 .min(MAX_BUNDLE_ITEMS as u64);
578 if bundle.supplies.len() as u64 > max {
579 return Err(inconsistent(
580 ItemLocation::Bundle,
581 Inconsistency::TooManySupplies { max },
582 ));
583 }
584 let max_pages = r
585 .plan
586 .definition
587 .limits
588 .acquisition
589 .as_ref()
590 .map_or(0, |l| l.max_calls);
591 if r.pages.len() as u64 > max_pages {
592 return Err(inconsistent(
593 ItemLocation::Bundle,
594 Inconsistency::TooManySupplies { max: max_pages },
595 ));
596 }
597 let compiled =
599 Compiled::load_checked_with_sources(&r.plan, &r.descriptors, &r.calibrations, &r.sources)
600 .map_err(|e| match e {
601 LoadError::CompilerChanged {
602 recorded_compiler,
603 current_compiler,
604 recorded_registry,
605 current_registry,
606 } => Incomparable::CompilerChanged {
607 recorded_compiler,
608 current_compiler: current_compiler.into(),
609 recorded_registry,
610 current_registry: current_registry.into(),
611 },
612 e @ (LoadError::MissingDescriptor { .. } | LoadError::MissingCalibration { .. }) => {
613 Incomparable::MissingDependency {
614 detail: e.to_string(),
615 }
616 }
617 LoadError::PlanMismatch { .. } => Incomparable::PlanMismatch,
618 })?;
619 let ev = Evaluation::start(&compiled, &r.snapshot, bundle.evaluation_time_ms)
621 .map_err(|_| inconsistent(ItemLocation::Snapshot, Inconsistency::StartRefused))?;
622 let mut ev = ReplayEvaluation::Decision(Box::new(ev)).advance(&bundle.decision_id)?;
623 let by_key: BTreeMap<(&str, &[String]), usize> = run
624 .requests
625 .iter()
626 .enumerate()
627 .map(|(i, q)| ((q.step.as_str(), q.instance.as_slice()), i))
628 .collect();
629 let mut listed = vec![];
630 let mut seen = BTreeSet::new();
631 list_new(&ev, &mut listed, &mut seen);
632 let mut supplies = vec![];
633 let pages = bundle.acquisition_pages.as_deref().unwrap_or(&[]);
634 let mut missing = vec![];
635 let (mut semantic_index, mut page_index) = (0, 0);
636 loop {
637 if let ReplayEvaluation::Decision(decision) = &mut ev {
638 fill_missing(decision, &pages[page_index..], &mut missing)?;
639 }
640 ev = ev.advance(&bundle.decision_id)?;
641 list_new(&ev, &mut listed, &mut seen);
642 let semantic = bundle.supplies.get(semantic_index);
643 let page = pages.get(page_index);
644 if let Some(page) = page
645 && semantic.is_none_or(|s| page.sequence < s.sequence)
646 {
647 let ReplayEvaluation::Decision(decision) = &mut ev else {
649 if !missing.is_empty() {
650 page_index += 1;
651 continue;
652 }
653 return Err(inconsistent(
654 ItemLocation::AcquisitionPage(page_index),
655 Inconsistency::NotPending,
656 ));
657 };
658 if missing.is_empty()
659 || decision.pending_acquisitions().iter().any(|p| {
660 p.step == page.step && p.tier_index == page.tier && p.page_index == page.page
661 })
662 {
663 supply_page(decision, page, &r.pages[page_index], run, page_index)?;
664 }
665 page_index += 1;
666 continue;
667 }
668 let Some(entry) = semantic else {
669 break;
670 };
671 let i = semantic_index;
672 let value = &r.values[i];
673 semantic_index += 1;
674 if !missing.is_empty()
675 && !ev
676 .pending()
677 .iter()
678 .any(|p| p.0 == entry.step && p.1 == entry.instance)
679 {
680 continue;
681 }
682 let at = ItemLocation::Supply(i);
683 let bad = |what| Err(inconsistent(at, what));
684 let Some(q) = by_key
685 .get(&(entry.step.as_str(), entry.instance.as_slice()))
686 .map(|&k| &run.requests[k])
687 else {
688 return bad(Inconsistency::NotInRun);
689 };
690 let Some(from) = q.supplied_from() else {
691 return bad(Inconsistency::NotInRun);
692 };
693 let origin = from
696 .attempt
697 .map(|a| a.attempt_id.as_str())
698 .or(from.reused_from);
699 let output_target = from
700 .attempt
701 .map(|a| a.target)
702 .or(from.reused_from.map(|_| from.target));
703 if from.target != entry.target
704 || origin != entry.attempt_id.as_deref()
705 || matches!(q.result, RequestResult::Output { .. })
706 && output_target != Some(entry.target)
707 {
708 return bad(Inconsistency::Origin);
709 }
710 let doc =
711 match ev.request_document(&entry.step, &entry.instance, entry.target, &bundle.scope) {
712 Ok(d) => d,
713 Err(rustev_core::RequestIdentityError::UnknownTarget { .. }) => {
714 return bad(Inconsistency::UnknownTarget);
715 }
716 Err(_) => return bad(Inconsistency::NotPending),
717 };
718 if doc.id().ok().as_ref() != Some(&entry.request) {
719 return bad(Inconsistency::RequestIdentity);
720 }
721 let (supplied, retained) = match (value, &q.result) {
722 (Value::Output(o), RequestResult::Output { target }) if *target == entry.target => {
723 let bound = target_artifact(&r.plan, &entry.step, entry.target);
724 if o.step != entry.step
725 || o.instance != entry.instance
726 || bound.as_ref() != Some(&o.artifact)
727 || from.attempt.is_some_and(|a| a.artifact != o.artifact)
728 {
729 return bad(Inconsistency::OutputBinding);
730 }
731 if !ev.accepts(&entry.step, &entry.instance, &o.output) {
733 return bad(Inconsistency::OutputRefused);
734 }
735 (
736 Supplied::Output(o.output.clone()),
737 RetainedValue::Output {
738 artifact: o.artifact.clone(),
739 output: o.output.clone(),
740 },
741 )
742 }
743 (Value::Reason(d), RequestResult::Failed(u)) if &d.reason == u => (
744 Supplied::Failed(d.reason.clone()),
745 RetainedValue::Reason(d.reason.clone()),
746 ),
747 _ => return bad(Inconsistency::Value),
748 };
749 if let Some(context) = entry.failure_context
750 && let ReplayEvaluation::Decision(decision) = &mut ev
751 {
752 decision.note_failure(
753 &entry.step,
754 &entry.instance,
755 rustev_core::FailureContext {
756 origin: context.origin,
757 determinism: context.determinism,
758 },
759 );
760 }
761 if !ev.supply(
762 &entry.step,
763 &entry.instance,
764 supplied,
765 q,
766 &bundle.decision_id,
767 ) {
768 return bad(Inconsistency::NotPending);
769 }
770 ev = ev.advance(&bundle.decision_id)?;
771 list_new(&ev, &mut listed, &mut seen);
772 supplies.push(RetainedSupply {
773 step: entry.step.clone(),
774 instance: entry.instance.clone(),
775 request: entry.request.clone(),
776 target: entry.target,
777 value: retained,
778 });
779 }
780 if let Some(p) = ev.pending().first() {
781 return Err(inconsistent(
782 ItemLocation::Bundle,
783 Inconsistency::MissingSupply {
784 step: p.0.clone(),
785 instance: p.1.clone(),
786 },
787 ));
788 }
789 let recorded: Vec<(String, Vec<String>)> = run
790 .requests
791 .iter()
792 .map(|q| (q.step.clone(), q.instance.clone()))
793 .collect();
794 if missing.is_empty() && recorded != listed {
795 return Err(inconsistent(run_at, Inconsistency::RequestOrder));
796 }
797 let ReplayEvaluation::Generation(stage) = ev else {
799 return Err(inconsistent(
800 ItemLocation::Bundle,
801 Inconsistency::FinishRefused,
802 ));
803 };
804 let (judgment, actual_evidence, text_result) = stage
805 .finish()
806 .map_err(|_| inconsistent(ItemLocation::Bundle, Inconsistency::FinishRefused))?;
807 if !missing.is_empty() {
808 return Err(Incomparable::AcquisitionNotCaptured {
809 missing,
810 judgment: Box::new(judgment),
811 });
812 }
813 let expected = &evidence.judgment;
814 let same = match (judgment.record_canonical(), expected.record_canonical()) {
815 (Ok(a), Ok(b)) => a == b,
816 _ => {
817 return Err(inconsistent(ItemLocation::Run, Inconsistency::NoRecordForm));
818 }
819 };
820 let generation_bytes =
822 |g: &Option<Vec<rustev_contract::generation_result::GenerationEvidence>>| {
823 let mut g = g.clone();
824 if let Some(steps) = &mut g {
825 for step in steps {
826 step.notices.retain(|n| {
827 matches!(
828 n.kind,
829 rustev_contract::generation_result::TextNoticeKind::BackstopTruncated
830 )
831 });
832 }
833 }
834 rustev_contract::canonical::record_canonical_bytes(&g)
835 };
836 let generation_same = match (
837 generation_bytes(&actual_evidence.generation),
838 generation_bytes(&evidence.generation),
839 ) {
840 (Ok(a), Ok(b)) => a == b,
841 _ => return Err(inconsistent(ItemLocation::Run, Inconsistency::NoRecordForm)),
842 };
843 if !same || !generation_same || actual_evidence.screening != evidence.screening {
844 return Ok(ReplayOutcome::Diverged {
845 expected: Box::new(expected.clone()),
846 actual: Box::new(judgment),
847 });
848 }
849 Ok(ReplayOutcome::Reproduced(Box::new(Reproduced {
850 decision_id: bundle.decision_id.clone(),
851 scope: bundle.scope.clone(),
852 plan: r.plan.clone(),
853 calibrations: r.calibrations.clone(),
854 snapshot: r.snapshot,
855 evaluation_time: bundle.evaluation_time_ms,
856 judgment,
857 run: r.run.clone(),
858 supplies,
859 pages: pages
860 .iter()
861 .zip(&r.pages)
862 .map(|(p, v)| (p.request.clone(), v.clone()))
863 .collect(),
864 text_result,
865 })))
866}